From 335a386aa730a6ddb1d5173dd0c643360f8ba8d7 Mon Sep 17 00:00:00 2001 From: Kirk Mitchener Date: Sat, 30 Aug 2025 16:38:56 -0400 Subject: [PATCH 01/11] add effect tooling, link itty, create policy resource --- .vscode/settings.json | 3 +- alchemy/package.json | 9 ++ alchemy/src/aws/itty/iam/policy.ts | 243 +++++++++++++++++++++++++++++ alchemy/src/aws/itty/index.ts | 1 + alchemy/tsconfig.json | 9 +- bun.lock | 80 ++++++---- 6 files changed, 313 insertions(+), 32 deletions(-) create mode 100644 alchemy/src/aws/itty/iam/policy.ts create mode 100644 alchemy/src/aws/itty/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 23d0e5a01..0777a1789 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -50,5 +50,6 @@ "[jsx]": { "editor.indentSize": 2, "editor.defaultFormatter": "biomejs.biome" - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/alchemy/package.json b/alchemy/package.json index c8353be5a..9a616fb89 100644 --- a/alchemy/package.json +++ b/alchemy/package.json @@ -15,6 +15,7 @@ "alchemy": "bin/alchemy.js" }, "scripts": { + "prepare": "ts-patch install -s", "build:cli": "tsdown", "dev:cli": "tsdown --watch", "build:workers": "bun ./scripts/build-workers.ts", @@ -48,6 +49,10 @@ "bun": "./src/aws/control/index.ts", "import": "./lib/aws/control/index.js" }, + "./aws/itty": { + "bun": "./src/aws/itty/index.ts", + "import": "./lib/aws/itty/index.js" + }, "./aws/oidc": { "bun": "./src/aws/oidc/index.ts", "import": "./lib/aws/oidc/index.js" @@ -151,6 +156,7 @@ "@iarna/toml": "^2.2.5", "@smithy/node-config-provider": "^4.0.0", "aws4fetch": "^1.0.20", + "effect": "3.16.12", "env-paths": "^3.0.0", "esbuild": "^0.25.1", "execa": "^9.6.0", @@ -158,6 +164,7 @@ "fast-xml-parser": "^5.2.5", "find-process": "^2.0.0", "glob": "^10.0.0", + "itty-aws": "link:itty-aws", "jszip": "^3.0.0", "libsodium-wrappers": "^0.7.15", "miniflare": "^4.20250712.0", @@ -259,6 +266,7 @@ "@cloudflare/puppeteer": "^1.0.2", "@cloudflare/vite-plugin": "catalog:", "@cloudflare/workers-types": "catalog:", + "@effect/language-service": "^0.36.0", "@libsql/client": "^0.15.12", "@octokit/rest": "^21.1.1", "@sentry/cloudflare": "^9.43.0", @@ -288,6 +296,7 @@ "stripe": "^17.0.0", "trpc-cli": "^0.10.2", "ts-morph": "^26.0.0", + "ts-patch": "^3.3.0", "tsdown": "^0.14.2", "typescript": "catalog:", "undici": "^7.14.0", diff --git a/alchemy/src/aws/itty/iam/policy.ts b/alchemy/src/aws/itty/iam/policy.ts new file mode 100644 index 000000000..6217aa24a --- /dev/null +++ b/alchemy/src/aws/itty/iam/policy.ts @@ -0,0 +1,243 @@ +import * as Effect from "effect/Effect"; +import * as Match from "effect/Match"; +import type { Context } from "../../../context.ts"; +import { Resource } from "../../../resource.ts"; + +import { IAM } from "itty-aws/iam"; + +/** + * Properties for creating or updating an IAM policy + */ +export interface PolicyProps { + /** + * The name of the policy + * + * @default ${app}-${stage}-${id} + * + */ + name: string; + + /** + * The path for the policy + * @default "/" + */ + path?: string; + + /** + * The policy document as a JSON string + */ + policy: string; + + /** + * Description of the policy + */ + description?: string; + + /** + * Key-value mapping of resource tags + */ + tags?: Record; +} + +/** + * Output returned after IAM policy creation/update + */ +export interface Policy extends Resource<"AWS::IAM::Policy">, PolicyProps { + /** + * The Amazon Resource Name (ARN) of the policy + */ + arn: string; + + /** + * The unique identifier of the policy + */ + policyId: string; +} + +/** + * AWS IAM Policy Resource + * + * Creates and manages IAM policies that define permissions for AWS services and resources. + * Supports automatic versioning and updates when policy content changes. + * + * @example + * ## Basic S3 Access Policy + * + * Create a policy that allows S3 bucket access with read and write permissions. + * + * ```ts + * const s3Policy = await Policy("s3-access", { + * name: "s3-bucket-access", + * policy: JSON.stringify({ + * Version: "2012-10-17", + * Statement: [{ + * Effect: "Allow", + * Action: [ + * "s3:GetObject", + * "s3:PutObject" + * ], + * Resource: "arn:aws:s3:::my-bucket/*" + * }] + * }), + * description: "Allows read/write access to S3 bucket" + * }); + * ``` + * + * @example + * ## Policy with Multiple Statements + * + * Create a comprehensive policy with multiple statements and conditions. + * + * ```ts + * const apiPolicy = await Policy("api-access", { + * name: "api-gateway-access", + * policy: JSON.stringify({ + * Version: "2012-10-17", + * Statement: [ + * { + * Sid: "InvokeAPI", + * Effect: "Allow", + * Action: "execute-api:Invoke", + * Resource: "arn:aws:execute-api:*:*:*\/prod/*", + * Condition: { + * StringEquals: { + * "aws:SourceVpc": "vpc-12345" + * } + * } + * }, + * { + * Sid: "ReadLogs", + * Effect: "Allow", + * Action: [ + * "logs:GetLogEvents", + * "logs:FilterLogEvents" + * ], + * Resource: "arn:aws:logs:*:*:*" + * } + * ] + * }), + * description: "API Gateway access with logging permissions", + * tags: { + * Service: "API Gateway", + * Environment: "production" + * } + * }); + * ``` + * + */ +export const Policy = Resource( + "AWS::IAM::Policy", + async function ( + this: Context, + _id: string, + props: PolicyProps, + ): Promise { + const iam = new IAM({}); + + if (this.phase === "delete") { + + } + + Match.value(this.phase).pipe( + Match.when("create", () => { + console.log("do create"); + + const policyArn = this.output?.arn; + + // if there's an existing ARN for this resource, we should do nothing on create + if (!policyArn) { + // no policy ARN in state means we haven't created it yet + const createEffect = Effect.gen(function* () { + const tags = props.tags + ? Object.entries(props.tags).map(([Key, Value]) => ({ + Key, + Value, + })) + : undefined; + + const createResult = yield* iam.createPolicy({ + PolicyName: props.name, + PolicyDocument: props.policy, + Path: props.path, + Description: props.description, + Tags: tags, + }); + // FIXME: what error handling / retry logic should we have here? + // no retry logic, that should be in itty + // this should return a policy if it succeeded -- what's in the return below should move up here (the data at least) + // and error if not + return createResult; + }); + + Effect.runPromise(createEffect).then((resultPolicy) => { + console.log("created iam policy!"); + return this({ + ...props, + arn: resultPolicy.Policy?.Arn || "", + policyId: resultPolicy.Policy?.PolicyId || "", + }); + }); + } + }), + Match.when("update", () => { + console.log("do update"); + // ensure the input properties are consistent with the existing policy + }), + Match.when("delete", () => { + console.log("do delete"); + + const policyArn = this.output?.arn; + + // if there's no ARN, there's nothing to delete in AWS + // just destroy the local state + if (!policyArn) { + return this.destroy(); + } + + // Execute deletion with proper error handling + const deleteEffect = Effect.gen(function* () { + // List and delete all non-default versions first + const versionsResult = yield* iam.listPolicyVersions({ + PolicyArn: policyArn, + }); + + const versions = versionsResult.Versions || []; + + // Delete non-default versions + for (const version of versions) { + if (!version.IsDefaultVersion && version.VersionId) { + yield* iam.deletePolicyVersion({ + PolicyArn: policyArn, + VersionId: version.VersionId, + }); + } + } + + // // Delete the policy itself + yield* iam + .deletePolicy({ + PolicyArn: policyArn, + }) + .pipe( + Effect.catchTag("NoSuchEntityException", () => + Effect.succeed("Policy doesn't exist"), + ), + ); + }); + + // FIXME: how should we be handling errors? this can fail for example if the policy is attached to a user or role + Effect.runPromise(deleteEffect).then(() => + console.log("deleted iam policy!"), + ); + + return this.destroy(); + }), + ); + + // FIXME: adding this just to satisfy this promise + return this({ + ...props, + policy. + }); + }, +); diff --git a/alchemy/src/aws/itty/index.ts b/alchemy/src/aws/itty/index.ts new file mode 100644 index 000000000..97f7194c3 --- /dev/null +++ b/alchemy/src/aws/itty/index.ts @@ -0,0 +1 @@ +export * from "./iam/policy.ts"; diff --git a/alchemy/tsconfig.json b/alchemy/tsconfig.json index fcd6f89cc..4314bbc54 100644 --- a/alchemy/tsconfig.json +++ b/alchemy/tsconfig.json @@ -15,6 +15,13 @@ "target": "ESNext", "allowJs": true, "allowImportingTsExtensions": true, - "rewriteRelativeImportExtensions": true + "rewriteRelativeImportExtensions": true, + "plugins": [ + { + "name": "@effect/language-service", + "transform": "@effect/language-service/transform", + "namespaceImportPackages": ["effect", "@effect/platform"], + } + ] } } diff --git a/bun.lock b/bun.lock index 586797a66..9633ce1f5 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,7 @@ "@iarna/toml": "^2.2.5", "@smithy/node-config-provider": "^4.0.0", "aws4fetch": "^1.0.20", + "effect": "3.16.12", "env-paths": "^3.0.0", "esbuild": "^0.25.1", "execa": "^9.6.0", @@ -42,6 +43,7 @@ "fast-xml-parser": "^5.2.5", "find-process": "^2.0.0", "glob": "^10.0.0", + "itty-aws": "link:itty-aws", "jszip": "^3.0.0", "libsodium-wrappers": "^0.7.15", "miniflare": "^4.20250712.0", @@ -71,6 +73,7 @@ "@cloudflare/puppeteer": "^1.0.2", "@cloudflare/vite-plugin": "catalog:", "@cloudflare/workers-types": "catalog:", + "@effect/language-service": "^0.36.0", "@libsql/client": "^0.15.12", "@octokit/rest": "^21.1.1", "@sentry/cloudflare": "^9.43.0", @@ -100,6 +103,7 @@ "stripe": "^17.0.0", "trpc-cli": "^0.10.2", "ts-morph": "^26.0.0", + "ts-patch": "^3.3.0", "tsdown": "^0.14.2", "typescript": "catalog:", "undici": "^7.14.0", @@ -1028,6 +1032,8 @@ "@effect/experimental": ["@effect/experimental@0.46.4", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.82.4", "effect": "^3.15.2", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-jKym3mkV7Z1fK7Kg+Wuv3XnV/DqjSRBNdWFCAz7NHAn8noGccXp3eWVui5JP/7KOZJ9gXj52ESJzkgk9PBy1rw=="], + "@effect/language-service": ["@effect/language-service@0.36.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-KoE5e+7vhcd29fBTDBvttWHv1z1KPg9Ji2DyhD0hETBaoM9IsFymboD6WqpDoGH1QZRr6CmTD3DfaZUs8K6Mrg=="], + "@effect/opentelemetry": ["@effect/opentelemetry@0.48.4", "", { "peerDependencies": { "@effect/platform": "^0.82.4", "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^3.15.2" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-f84T7ZJHLtZKGjyjjJxJReTrLLJTcXw+YFDGpCxB/rt0a1xzX/tcjY37pel3Fu0eL3cR5unmhirTSZZoGzkyAg=="], "@effect/platform": ["@effect/platform@0.82.4", "", { "dependencies": { "find-my-way-ts": "^0.1.5", "msgpackr": "^1.11.2", "multipasta": "^0.2.5" }, "peerDependencies": { "effect": "^3.15.2" } }, "sha512-og5DIzx4wz7nIXd/loDfenXvV3c/NT+NDG+YJsi3g6a5Xb6xwrNJuX97sDaV2LJ29G3LroeVJCmYUmfdf/h5Vg=="], @@ -2356,7 +2362,7 @@ "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], - "ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -2916,7 +2922,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@3.15.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-0JNwvfs4Wwbo3f6IOydBFlp+zxuO8Iny2UAWNW3+FNn9x8FJf7q67QnQagUZgPl/BLl/xuPLVksrmNyIrJ8k/Q=="], + "effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], "electron-to-chromium": ["electron-to-chromium@1.5.209", "", {}, "sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A=="], @@ -3208,6 +3214,8 @@ "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], + "global-prefix": ["global-prefix@4.0.0", "", { "dependencies": { "ini": "^4.1.3", "kind-of": "^6.0.3", "which": "^4.0.0" } }, "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA=="], + "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], "globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="], @@ -3450,6 +3458,8 @@ "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + "itty-aws": ["itty-aws@link:itty-aws", {}], + "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], @@ -4532,7 +4542,7 @@ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -4664,6 +4674,8 @@ "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], + "ts-patch": ["ts-patch@3.3.0", "", { "dependencies": { "chalk": "^4.1.2", "global-prefix": "^4.0.0", "minimist": "^1.2.8", "resolve": "^1.22.2", "semver": "^7.6.3", "strip-ansi": "^6.0.1" }, "bin": { "ts-patch": "bin/ts-patch.js", "tspc": "bin/tspc.js" } }, "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg=="], + "ts-pattern": ["ts-pattern@5.8.0", "", {}, "sha512-kIjN2qmWiHnhgr5DAkAafF9fwb0T5OhMVSWrm8XEdTFnX6+wfXwYOFjeF86UZ54vduqiR7BfqScFmXSzSaH8oA=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], @@ -5098,16 +5110,8 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@effect/cluster/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - "@effect/platform-node/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - "@effect/platform-node-shared/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - - "@effect/rpc/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - - "@effect/sql/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -5132,14 +5136,20 @@ "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], "@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "@livestore/devtools-vite/vite": ["vite@7.1.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw=="], + "@livestore/peer-deps/effect": ["effect@3.15.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-0JNwvfs4Wwbo3f6IOydBFlp+zxuO8Iny2UAWNW3+FNn9x8FJf7q67QnQagUZgPl/BLl/xuPLVksrmNyIrJ8k/Q=="], + "@livestore/react/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], + "@livestore/utils/effect": ["effect@3.15.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-0JNwvfs4Wwbo3f6IOydBFlp+zxuO8Iny2UAWNW3+FNn9x8FJf7q67QnQagUZgPl/BLl/xuPLVksrmNyIrJ8k/Q=="], + "@livestore/utils/nanoid": ["nanoid@5.1.3", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ=="], "@netlify/dev-utils/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], @@ -5378,8 +5388,6 @@ "clipboardy/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "cloudflare/@types/node": ["@types/node@18.19.123", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg=="], @@ -5490,6 +5498,8 @@ "global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + "global-prefix/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "graphology-dag/mnemonist": ["mnemonist@0.39.8", "", { "dependencies": { "obliterator": "^2.0.1" } }, "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ=="], "gray-matter/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -5526,6 +5536,8 @@ "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], + "log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "merge-options/is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], "micromark-extension-mdxjs/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -5680,14 +5692,8 @@ "static-extend/define-property": ["define-property@0.2.5", "", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA=="], - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "stylehacks/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="], "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], @@ -5716,6 +5722,8 @@ "trpc-cli/commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + "ts-patch/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "tsdown/diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], "tsdown/tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], @@ -5740,6 +5748,8 @@ "vite-node/vite": ["vite@7.1.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw=="], + "vite-plugin-checker/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "vite-plugin-inspect/perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="], "vite-plugin-inspect/unplugin-utils": ["unplugin-utils@0.3.0", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg=="], @@ -5774,7 +5784,7 @@ "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "xmlbuilder2/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -5898,6 +5908,8 @@ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "@netlify/dev-utils/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], @@ -6088,6 +6100,8 @@ "boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + "boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "braintrust/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "changelogen/c12/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -6110,6 +6124,8 @@ "cli-truncate/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "clipboardy/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], "clipboardy/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], @@ -6120,8 +6136,6 @@ "clipboardy/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "cloudflare-orange/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], "cloudflare-orange/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], @@ -6248,6 +6262,8 @@ "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "object-copy/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="], @@ -6306,10 +6322,6 @@ "static-extend/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -6328,12 +6340,16 @@ "unset-value/has-value/isobject": ["isobject@2.1.0", "", { "dependencies": { "isarray": "1.0.0" } }, "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA=="], + "vite-plugin-checker/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "vite-project/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], "widest-line/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "winston-transport/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "winston/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -6388,10 +6404,10 @@ "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "xmlbuilder2/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "yaml-language-server/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -6476,6 +6492,8 @@ "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "changelogen/c12/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "changelogen/c12/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -6486,14 +6504,14 @@ "changelogen/c12/giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "clipboardy/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "cloudflare-prisma/@prisma/adapter-d1/@prisma/driver-adapter-utils/@prisma/debug": ["@prisma/debug@6.14.0", "", {}, "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug=="], "cloudflare-prisma/prisma/@prisma/config/c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], - "cloudflare-prisma/prisma/@prisma/config/effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], - "cloudflare-prisma/prisma/@prisma/engines/@prisma/debug": ["@prisma/debug@6.14.0", "", {}, "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug=="], "cloudflare-prisma/prisma/@prisma/engines/@prisma/engines-version": ["@prisma/engines-version@6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", "", {}, "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA=="], @@ -6550,6 +6568,8 @@ "tanstack-start-example-basic/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@6.0.0", "", {}, "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg=="], "yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.16.0", "", {}, "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="], From f2776bc4e6e1bcc9f0dced344bd5e1330e314212 Mon Sep 17 00:00:00 2001 From: Kirk Mitchener Date: Sun, 31 Aug 2025 08:37:58 -0400 Subject: [PATCH 02/11] simplify --- alchemy/src/aws/itty/iam/policy.ts | 136 +++++++++++++++++------------ 1 file changed, 80 insertions(+), 56 deletions(-) diff --git a/alchemy/src/aws/itty/iam/policy.ts b/alchemy/src/aws/itty/iam/policy.ts index 6217aa24a..4a9b8c406 100644 --- a/alchemy/src/aws/itty/iam/policy.ts +++ b/alchemy/src/aws/itty/iam/policy.ts @@ -1,5 +1,4 @@ import * as Effect from "effect/Effect"; -import * as Match from "effect/Match"; import type { Context } from "../../../context.ts"; import { Resource } from "../../../resource.ts"; @@ -49,9 +48,34 @@ export interface Policy extends Resource<"AWS::IAM::Policy">, PolicyProps { arn: string; /** - * The unique identifier of the policy + * Name of the Policy. */ - policyId: string; + name: string; + + /** + * ID of the default policy version + */ + defaultVersionId: string; + + /** + * Number of entities the policy is attached to + */ + attachmentCount: number; + + /** + * When the policy was created + */ + createDate: Date; + + /** + * When the policy was last updated + */ + updateDate: Date; + + /** + * Whether the policy can be attached to IAM users/roles + */ + isAttachable: boolean; } /** @@ -134,57 +158,16 @@ export const Policy = Resource( ): Promise { const iam = new IAM({}); - if (this.phase === "delete") { - + // if a resource's immutable property is updated, it needs to trigger a replacement of the resource + // https://alchemy.run/concepts/resource/#trigger-replacement + if (this.phase === "update" && this.output.name !== this.props.name) { + // calling this.replace() will terminate this run and re-invoke it + // with the "create" phase + this.replace(); } - Match.value(this.phase).pipe( - Match.when("create", () => { - console.log("do create"); - - const policyArn = this.output?.arn; - - // if there's an existing ARN for this resource, we should do nothing on create - if (!policyArn) { - // no policy ARN in state means we haven't created it yet - const createEffect = Effect.gen(function* () { - const tags = props.tags - ? Object.entries(props.tags).map(([Key, Value]) => ({ - Key, - Value, - })) - : undefined; - - const createResult = yield* iam.createPolicy({ - PolicyName: props.name, - PolicyDocument: props.policy, - Path: props.path, - Description: props.description, - Tags: tags, - }); - // FIXME: what error handling / retry logic should we have here? - // no retry logic, that should be in itty - // this should return a policy if it succeeded -- what's in the return below should move up here (the data at least) - // and error if not - return createResult; - }); - - Effect.runPromise(createEffect).then((resultPolicy) => { - console.log("created iam policy!"); - return this({ - ...props, - arn: resultPolicy.Policy?.Arn || "", - policyId: resultPolicy.Policy?.PolicyId || "", - }); - }); - } - }), - Match.when("update", () => { - console.log("do update"); - // ensure the input properties are consistent with the existing policy - }), - Match.when("delete", () => { - console.log("do delete"); + if (this.phase === "delete") { + console.log("do delete"); const policyArn = this.output?.arn; @@ -231,13 +214,54 @@ export const Policy = Resource( ); return this.destroy(); - }), - ); + } + + if (this.phase === "create") { + console.log("do create"); + + const createEffect = Effect.gen(function* () { + const tags = props.tags + ? Object.entries(props.tags).map(([Key, Value]) => ({ + Key, + Value, + })) + : undefined; + + const createResult = yield* iam.createPolicy({ + PolicyName: props.name, + PolicyDocument: props.policy, + Path: props.path, + Description: props.description, + Tags: tags, + }); + // FIXME: what error handling / retry logic should we have here? + // no retry logic, that should be in itty + // this should return a policy if it succeeded -- what's in the return below should move up here (the data at least) + // and error if not + return createResult; + }); + + Effect.runPromise(createEffect).then((resultPolicy) => { + console.log("created iam policy!"); + return this({ + ...props, + arn: resultPolicy.Policy?.Arn || "", + policyId: resultPolicy.Policy?.PolicyId || "", + }); + }); + } + + if (this.phase === "update") { + console.log("do update"); + // ensure the input properties are consistent with the existing policy + const policyArn = this.output?.arn; + + } - // FIXME: adding this just to satisfy this promise return this({ ...props, - policy. + arn: policyArn!, + policyId: role.Role.RoleId!, }); }, ); From 488f06dd5424336693c55c7c40651c01d559d49b Mon Sep 17 00:00:00 2001 From: Kirk Mitchener Date: Sun, 31 Aug 2025 10:31:02 -0400 Subject: [PATCH 03/11] this crap --- alchemy/src/aws/itty/iam/policy.ts | 65 +++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/alchemy/src/aws/itty/iam/policy.ts b/alchemy/src/aws/itty/iam/policy.ts index 4a9b8c406..9cad4c05f 100644 --- a/alchemy/src/aws/itty/iam/policy.ts +++ b/alchemy/src/aws/itty/iam/policy.ts @@ -1,3 +1,4 @@ +import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import type { Context } from "../../../context.ts"; import { Resource } from "../../../resource.ts"; @@ -65,12 +66,12 @@ export interface Policy extends Resource<"AWS::IAM::Policy">, PolicyProps { /** * When the policy was created */ - createDate: Date; + createDate: string; /** * When the policy was last updated */ - updateDate: Date; + updateDate: string; /** * Whether the policy can be attached to IAM users/roles @@ -169,13 +170,13 @@ export const Policy = Resource( if (this.phase === "delete") { console.log("do delete"); - const policyArn = this.output?.arn; + const policyArn = this.output.arn; // if there's no ARN, there's nothing to delete in AWS // just destroy the local state - if (!policyArn) { - return this.destroy(); - } + // if (!policyArn) { + // return this.destroy(); + // } // Execute deletion with proper error handling const deleteEffect = Effect.gen(function* () { @@ -213,13 +214,17 @@ export const Policy = Resource( console.log("deleted iam policy!"), ); + // this.destroy() should really only be called if the deletePolicy call was successful return this.destroy(); } + let policyArn: string = ""; + if (this.phase === "create") { console.log("do create"); const createEffect = Effect.gen(function* () { + yield* Console.log("actually running the effect"); const tags = props.tags ? Object.entries(props.tags).map(([Key, Value]) => ({ Key, @@ -233,7 +238,10 @@ export const Policy = Resource( Path: props.path, Description: props.description, Tags: tags, - }); + }).pipe( + Effect.tap((response) => Console.log(`got successful response: ${response}`)), + Effect.catchAllCause((err) => Effect.fail(new Error(`failing from error: ${err}`))), + ); // FIXME: what error handling / retry logic should we have here? // no retry logic, that should be in itty // this should return a policy if it succeeded -- what's in the return below should move up here (the data at least) @@ -241,27 +249,46 @@ export const Policy = Resource( return createResult; }); + console.log("running create promise"); + Effect.runPromise(createEffect).then((resultPolicy) => { console.log("created iam policy!"); - return this({ - ...props, - arn: resultPolicy.Policy?.Arn || "", - policyId: resultPolicy.Policy?.PolicyId || "", - }); - }); + const p = resultPolicy!.Policy!; + policyArn = p.Arn!; + }).catch(console.error).finally(() => console.log("finally on create")); } if (this.phase === "update") { console.log("do update"); + policyArn = this.props.arn; // ensure the input properties are consistent with the existing policy - const policyArn = this.output?.arn; - + // const policyArn = this.output?.arn; } + console.log(`policy arn: ${policyArn}`); + + Effect.runPromise(iam.getPolicy({ PolicyArn: policyArn })).then((policy) => { + console.log("successfully retrieved policy"); + const p = policy.Policy!; + return this({ + ...props, + arn: p.Arn!, + defaultVersionId: p.DefaultVersionId!, + attachmentCount: p.AttachmentCount!, + createDate: p.CreateDate!.toString(), + updateDate: p.UpdateDate!.toString(), + isAttachable: p.IsAttachable!, + }); + }).catch(console.error); + return this({ - ...props, - arn: policyArn!, - policyId: role.Role.RoleId!, - }); + ...props, + arn: "arn", + defaultVersionId: "default version", + attachmentCount: 0, + createDate: "some date", + updateDate: "some other date", + isAttachable: true, + }); }, ); From b7c2ed78bdd5310fb666b2dc9b59abf0eea8a06b Mon Sep 17 00:00:00 2001 From: Kirk Mitchener Date: Sun, 31 Aug 2025 11:01:39 -0400 Subject: [PATCH 04/11] working --- alchemy/src/aws/itty/iam/policy.ts | 59 +++++++++++------------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/alchemy/src/aws/itty/iam/policy.ts b/alchemy/src/aws/itty/iam/policy.ts index 9cad4c05f..17fb1ea96 100644 --- a/alchemy/src/aws/itty/iam/policy.ts +++ b/alchemy/src/aws/itty/iam/policy.ts @@ -1,4 +1,3 @@ -import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import type { Context } from "../../../context.ts"; import { Resource } from "../../../resource.ts"; @@ -168,16 +167,10 @@ export const Policy = Resource( } if (this.phase === "delete") { - console.log("do delete"); + //console.log("do delete"); const policyArn = this.output.arn; - // if there's no ARN, there's nothing to delete in AWS - // just destroy the local state - // if (!policyArn) { - // return this.destroy(); - // } - // Execute deletion with proper error handling const deleteEffect = Effect.gen(function* () { // List and delete all non-default versions first @@ -210,21 +203,16 @@ export const Policy = Resource( }); // FIXME: how should we be handling errors? this can fail for example if the policy is attached to a user or role - Effect.runPromise(deleteEffect).then(() => - console.log("deleted iam policy!"), - ); + Effect.runPromise(deleteEffect); // this.destroy() should really only be called if the deletePolicy call was successful return this.destroy(); } - let policyArn: string = ""; - if (this.phase === "create") { - console.log("do create"); + //console.log("do create"); const createEffect = Effect.gen(function* () { - yield* Console.log("actually running the effect"); const tags = props.tags ? Object.entries(props.tags).map(([Key, Value]) => ({ Key, @@ -239,7 +227,11 @@ export const Policy = Resource( Description: props.description, Tags: tags, }).pipe( - Effect.tap((response) => Console.log(`got successful response: ${response}`)), + // Effect.tap((response) => Console.log(`got successful response: ${JSON.stringify(response, null, 2)}`)), + Effect.flatMap((createPolicyResponse) => { + return iam.getPolicy({ PolicyArn: createPolicyResponse!.Policy!.Arn!}); + }), + // FIXME: too broad Effect.catchAllCause((err) => Effect.fail(new Error(`failing from error: ${err}`))), ); // FIXME: what error handling / retry logic should we have here? @@ -249,38 +241,29 @@ export const Policy = Resource( return createResult; }); - console.log("running create promise"); - Effect.runPromise(createEffect).then((resultPolicy) => { - console.log("created iam policy!"); const p = resultPolicy!.Policy!; - policyArn = p.Arn!; - }).catch(console.error).finally(() => console.log("finally on create")); + return this({ + ...props, + arn: p.Arn!, + defaultVersionId: p.DefaultVersionId!, + attachmentCount: p.AttachmentCount!, + createDate: p.CreateDate!.toString(), + updateDate: p.UpdateDate!.toString(), + isAttachable: p.IsAttachable!, + }); + }); } if (this.phase === "update") { console.log("do update"); - policyArn = this.props.arn; + //policyArn = this.props.arn; // ensure the input properties are consistent with the existing policy // const policyArn = this.output?.arn; } - console.log(`policy arn: ${policyArn}`); - - Effect.runPromise(iam.getPolicy({ PolicyArn: policyArn })).then((policy) => { - console.log("successfully retrieved policy"); - const p = policy.Policy!; - return this({ - ...props, - arn: p.Arn!, - defaultVersionId: p.DefaultVersionId!, - attachmentCount: p.AttachmentCount!, - createDate: p.CreateDate!.toString(), - updateDate: p.UpdateDate!.toString(), - isAttachable: p.IsAttachable!, - }); - }).catch(console.error); - + // this shouldn't ever return but we need to satisfy the return promise on this method. + // can this be improved?? return this({ ...props, arn: "arn", From 73ae7fb90d8a0c74c52edf49b08d910735423970 Mon Sep 17 00:00:00 2001 From: Kirk Mitchener Date: Sun, 31 Aug 2025 11:15:40 -0400 Subject: [PATCH 05/11] working better --- alchemy/src/aws/itty/iam/policy.ts | 103 ++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/alchemy/src/aws/itty/iam/policy.ts b/alchemy/src/aws/itty/iam/policy.ts index 17fb1ea96..f1ba0d3e6 100644 --- a/alchemy/src/aws/itty/iam/policy.ts +++ b/alchemy/src/aws/itty/iam/policy.ts @@ -160,10 +160,14 @@ export const Policy = Resource( // if a resource's immutable property is updated, it needs to trigger a replacement of the resource // https://alchemy.run/concepts/resource/#trigger-replacement - if (this.phase === "update" && this.output.name !== this.props.name) { - // calling this.replace() will terminate this run and re-invoke it + // NOTE: in update phase, `this.props` are the OLD props; compare against incoming `props` instead. + if ( + this.phase === "update" && + (this.output.name !== props.name || this.output.path !== props.path) + ) { + // calling this.replace() will terminate this run and re-invoke it // with the "create" phase - this.replace(); + return this.replace(); } if (this.phase === "delete") { @@ -203,14 +207,11 @@ export const Policy = Resource( }); // FIXME: how should we be handling errors? this can fail for example if the policy is attached to a user or role - Effect.runPromise(deleteEffect); - - // this.destroy() should really only be called if the deletePolicy call was successful + await Effect.runPromise(deleteEffect); return this.destroy(); } if (this.phase === "create") { - //console.log("do create"); const createEffect = Effect.gen(function* () { const tags = props.tags @@ -241,37 +242,75 @@ export const Policy = Resource( return createResult; }); - Effect.runPromise(createEffect).then((resultPolicy) => { - const p = resultPolicy!.Policy!; - return this({ - ...props, - arn: p.Arn!, - defaultVersionId: p.DefaultVersionId!, - attachmentCount: p.AttachmentCount!, - createDate: p.CreateDate!.toString(), - updateDate: p.UpdateDate!.toString(), - isAttachable: p.IsAttachable!, - }); + const resultPolicy = await Effect.runPromise(createEffect); + const p = resultPolicy!.Policy!; + if (!this.quiet) { + console.log(`policy: ${JSON.stringify(p, null, 2)}`); + } + return this({ + ...props, + arn: p.Arn!, + defaultVersionId: p.DefaultVersionId!, + attachmentCount: p.AttachmentCount!, + createDate: p.CreateDate!.toString(), + updateDate: p.UpdateDate!.toString(), + isAttachable: p.IsAttachable!, }); } if (this.phase === "update") { - console.log("do update"); - //policyArn = this.props.arn; - // ensure the input properties are consistent with the existing policy - // const policyArn = this.output?.arn; - } + // Update policy document by creating a new default version when content changes + const policyArn = this.output.arn; + const currentDefaultVersionId = this.output.defaultVersionId; + + // If policy JSON changed, create a new version and set as default + const newDoc = props.policy; + // We can't easily diff normalized JSON reliably here; optimistically create a new version. + // Optionally, callers can avoid unnecessary updates by keeping props stable. + const updateEffect = Effect.gen(function* () { + const versionsResult = yield* iam.listPolicyVersions({ PolicyArn: policyArn }); + const versions = versionsResult.Versions ?? []; + + // Create a new version as default + const created = yield* iam.createPolicyVersion({ + PolicyArn: policyArn, + PolicyDocument: newDoc, + SetAsDefault: true, + }); - // this shouldn't ever return but we need to satisfy the return promise on this method. - // can this be improved?? - return this({ + // If we exceed AWS limit (5), prune the oldest non-default version + const nonDefault = versions.filter((v) => !v.IsDefaultVersion && v.VersionId); + if (nonDefault.length >= 4) { + // Sort by CreateDate asc and delete the oldest + nonDefault.sort((a, b) => + (new Date(a.CreateDate ?? 0).getTime()) - (new Date(b.CreateDate ?? 0).getTime()), + ); + const oldest = nonDefault[0]; + if (oldest?.VersionId) { + yield* iam.deletePolicyVersion({ PolicyArn: policyArn, VersionId: oldest.VersionId }); + } + } + + // Fetch updated policy metadata + const updatedPolicy = yield* iam.getPolicy({ PolicyArn: policyArn }); + return updatedPolicy; + }); + + const updated = await Effect.runPromise(updateEffect); + const p = updated!.Policy!; + return this({ ...props, - arn: "arn", - defaultVersionId: "default version", - attachmentCount: 0, - createDate: "some date", - updateDate: "some other date", - isAttachable: true, + arn: p.Arn!, + defaultVersionId: p.DefaultVersionId ?? currentDefaultVersionId, + attachmentCount: p.AttachmentCount!, + createDate: p.CreateDate!.toString(), + updateDate: p.UpdateDate!.toString(), + isAttachable: p.IsAttachable!, }); + } + + // Should never reach here; all phases handled above. + // If it does, consider it a logic error. + throw new Error("Unhandled resource phase"); }, ); From c7258f1b7856efeda68e6ced1d973e714779b42d Mon Sep 17 00:00:00 2001 From: Kirk Mitchener Date: Mon, 1 Sep 2025 14:14:02 -0400 Subject: [PATCH 06/11] minor changes --- alchemy/src/aws/itty/iam/policy.ts | 32 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/alchemy/src/aws/itty/iam/policy.ts b/alchemy/src/aws/itty/iam/policy.ts index f1ba0d3e6..dada427da 100644 --- a/alchemy/src/aws/itty/iam/policy.ts +++ b/alchemy/src/aws/itty/iam/policy.ts @@ -14,7 +14,7 @@ export interface PolicyProps { * @default ${app}-${stage}-${id} * */ - name: string; + name?: string; /** * The path for the policy @@ -25,7 +25,7 @@ export interface PolicyProps { /** * The policy document as a JSON string */ - policy: string; + policy: string | object; /** * Description of the policy @@ -163,15 +163,15 @@ export const Policy = Resource( // NOTE: in update phase, `this.props` are the OLD props; compare against incoming `props` instead. if ( this.phase === "update" && - (this.output.name !== props.name || this.output.path !== props.path) + ((props.name !== undefined && this.output.name !== props.name) || + (props.path !== undefined && this.output.path !== props.path)) ) { - // calling this.replace() will terminate this run and re-invoke it + // calling this.replace() will terminate this run and re-invoke this method // with the "create" phase return this.replace(); } if (this.phase === "delete") { - //console.log("do delete"); const policyArn = this.output.arn; @@ -194,7 +194,7 @@ export const Policy = Resource( } } - // // Delete the policy itself + // Delete the policy itself yield* iam .deletePolicy({ PolicyArn: policyArn, @@ -212,6 +212,11 @@ export const Policy = Resource( } if (this.phase === "create") { + // Resolve defaults + // FIXME: should this use scope.createPhysicalName()? + const resolvedName = props.name ?? `${this.scope.appName}-${this.stage}-${_id}`; + const resolvedPath = props.path ?? "/"; + const policyDoc = typeof props.policy === "string" ? props.policy : JSON.stringify(props.policy); const createEffect = Effect.gen(function* () { const tags = props.tags @@ -222,14 +227,16 @@ export const Policy = Resource( : undefined; const createResult = yield* iam.createPolicy({ - PolicyName: props.name, - PolicyDocument: props.policy, - Path: props.path, + PolicyName: resolvedName, + PolicyDocument: policyDoc, + Path: resolvedPath, Description: props.description, Tags: tags, }).pipe( // Effect.tap((response) => Console.log(`got successful response: ${JSON.stringify(response, null, 2)}`)), Effect.flatMap((createPolicyResponse) => { + + // FIXME: the policy response may have all the metadata we need return iam.getPolicy({ PolicyArn: createPolicyResponse!.Policy!.Arn!}); }), // FIXME: too broad @@ -249,6 +256,8 @@ export const Policy = Resource( } return this({ ...props, + name: resolvedName, + path: resolvedPath, arn: p.Arn!, defaultVersionId: p.DefaultVersionId!, attachmentCount: p.AttachmentCount!, @@ -264,10 +273,11 @@ export const Policy = Resource( const currentDefaultVersionId = this.output.defaultVersionId; // If policy JSON changed, create a new version and set as default - const newDoc = props.policy; + const newDoc = typeof props.policy === "string" ? props.policy : JSON.stringify(props.policy); // We can't easily diff normalized JSON reliably here; optimistically create a new version. // Optionally, callers can avoid unnecessary updates by keeping props stable. const updateEffect = Effect.gen(function* () { + // Fetch updated policy metadata const versionsResult = yield* iam.listPolicyVersions({ PolicyArn: policyArn }); const versions = versionsResult.Versions ?? []; @@ -300,6 +310,8 @@ export const Policy = Resource( const p = updated!.Policy!; return this({ ...props, + name: this.output.name, + path: this.output.path, arn: p.Arn!, defaultVersionId: p.DefaultVersionId ?? currentDefaultVersionId, attachmentCount: p.AttachmentCount!, From 8331a773fcf41385fe42001da7aef2b5f0023212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20=E2=80=98TK=E2=80=99=20Taylor?= Date: Fri, 29 Aug 2025 23:14:23 +0100 Subject: [PATCH 07/11] fix(cloudflare): enforce stateToken requirement in CloudflareStateStore (#927) --- alchemy/src/state/cloudflare-state-store.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/alchemy/src/state/cloudflare-state-store.ts b/alchemy/src/state/cloudflare-state-store.ts index 4033df399..1c3650d69 100644 --- a/alchemy/src/state/cloudflare-state-store.ts +++ b/alchemy/src/state/cloudflare-state-store.ts @@ -47,13 +47,13 @@ export class CloudflareStateStore extends StateStoreProxy { options: CloudflareStateStoreOptions & { stateToken: Secret }; constructor(scope: Scope, options: CloudflareStateStoreOptions = {}) { super(scope); - const stateToken = - options.stateToken ?? alchemy.secret(process.env.ALCHEMY_STATE_TOKEN); - if (!stateToken) { + if (!options.stateToken && !process.env.ALCHEMY_STATE_TOKEN) { throw new Error( - "Missing token for DOStateStore. Please set ALCHEMY_STATE_TOKEN in the environment or set the `stateToken` option in the DOStateStore constructor.", + "Missing token for CloudflareStateStore. Please set ALCHEMY_STATE_TOKEN in the environment or set the `stateToken` option in the CloudflareStateStore constructor. See https://alchemy.run/guides/cloudflare-state-store/", ); } + const stateToken = + options.stateToken ?? alchemy.secret(process.env.ALCHEMY_STATE_TOKEN); this.options = { ...options, stateToken: stateToken, From c50f02520ba0db4844fbdc6eb9e113c863c52a6e Mon Sep 17 00:00:00 2001 From: John Royal <34844819+john-royal@users.noreply.github.com> Date: Mon, 1 Sep 2025 18:37:56 -0400 Subject: [PATCH 08/11] fix(core): deterministic cache keys for function memoization (#929) --- alchemy/src/util/memoize.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/alchemy/src/util/memoize.ts b/alchemy/src/util/memoize.ts index 3b525c00f..ae5861d36 100644 --- a/alchemy/src/util/memoize.ts +++ b/alchemy/src/util/memoize.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import { isSecret } from "../secret.ts"; type AsyncReturnType = T extends (...args: any[]) => Promise ? R @@ -25,5 +26,16 @@ export function memoize Promise>( } export const defaultKeyFn = (...args: any[]) => { - return createHash("sha256").update(JSON.stringify(args)).digest("hex"); + return createHash("sha256") + .update( + JSON.stringify(args, (_, value) => { + // Secret names may differ between instantiations, + // so we unwrap it to make the key deterministic. + if (isSecret(value)) { + return value.unencrypted; + } + return value; + }), + ) + .digest("hex"); }; From 2b747da52fb7d24b41227d0dbcedf3aceaa4aa96 Mon Sep 17 00:00:00 2001 From: John Royal <34844819+john-royal@users.noreply.github.com> Date: Mon, 1 Sep 2025 18:39:12 -0400 Subject: [PATCH 09/11] fix(cloudflare): use "text/javascript" mime type for assets (#931) --- alchemy/src/cloudflare/worker-assets.ts | 25 ++++++++++++------------- alchemy/src/util/content-type.ts | 4 ++-- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/alchemy/src/cloudflare/worker-assets.ts b/alchemy/src/cloudflare/worker-assets.ts index 6d40693dc..2be574b53 100644 --- a/alchemy/src/cloudflare/worker-assets.ts +++ b/alchemy/src/cloudflare/worker-assets.ts @@ -2,10 +2,9 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { AsyncQueue } from "../util/async-queue.ts"; -import { getContentType } from "../util/content-type.ts"; import { CloudflareApiError } from "./api-error.ts"; import type { CloudflareApi } from "./api.ts"; -import type { Assets } from "./assets.ts"; +import type { AssetFile, Assets } from "./assets.ts"; import type { AssetsConfig, WorkerProps } from "./worker.ts"; export interface AssetUploadResult { @@ -73,7 +72,7 @@ export async function uploadAssets( // Process the assets configuration once at the beginning const processedConfig = createAssetConfig(assetConfig); - const { manifest, filePathsByHash } = await prepareAssetManifest(assets); + const { manifest, filesByHash } = await prepareAssetManifest(assets); // Start the upload session const uploadSessionResponse = await api.post( @@ -130,7 +129,7 @@ export async function uploadAssets( api, sessionData.result.jwt, bucket, - filePathsByHash, + filesByHash, ); if (jwt) { completionToken = jwt; @@ -183,38 +182,38 @@ export function createAssetConfig(config?: AssetsConfig): AssetsConfig { * Prepares the asset manifest for the assets upload session * * @param assets Assets resource containing files to upload - * @returns Asset manifest and file paths by hash + * @returns Asset manifest and files by hash */ export async function prepareAssetManifest(assets: Assets) { const manifest: Record = {}; - const filePathsByHash = new Map(); + const filesByHash = new Map(); await Promise.all( assets.files.map(async (file) => { const { hash, size } = await calculateFileMetadata(file.filePath); const key = file.path.startsWith("/") ? file.path : `/${file.path}`; manifest[key] = { hash, size }; - filePathsByHash.set(hash, file.filePath); + filesByHash.set(hash, file); }), ); - return { manifest, filePathsByHash }; + return { manifest, filesByHash }; } async function uploadBucket( api: CloudflareApi, jwt: string, bucket: string[], - files: Map, + filesByHash: Map, ) { const formData = new FormData(); await Promise.all( bucket.map(async (fileHash) => { - const filePath = files.get(fileHash); - if (!filePath) { + const file = filesByHash.get(fileHash); + if (!file) { throw new Error(`Could not find file with hash ${fileHash}`); } - const fileContent = await fs.readFile(filePath); + const fileContent = await fs.readFile(file.filePath); const blob = new Blob([fileContent.toString("base64")], { - type: getContentType(filePath) ?? "application/null", + type: file.contentType, }); formData.append(fileHash, blob, fileHash); }), diff --git a/alchemy/src/util/content-type.ts b/alchemy/src/util/content-type.ts index fea9ba73c..4119b4a4f 100644 --- a/alchemy/src/util/content-type.ts +++ b/alchemy/src/util/content-type.ts @@ -11,10 +11,10 @@ const mimeTypes: Record = { ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".js.map": "application/source-map", - ".js": "application/javascript", + ".js": "text/javascript", ".json": "application/json", ".md": "text/markdown", - ".mjs": "application/javascript+module", + ".mjs": "text/javascript", ".mp3": "audio/mpeg", ".mp4": "video/mp4", ".otf": "font/otf", From 13690c43919fe7810d840b761c52b3da1485099f Mon Sep 17 00:00:00 2001 From: Kirk Mitchener Date: Thu, 4 Sep 2025 07:20:43 -0400 Subject: [PATCH 10/11] remove extra unneeded call --- alchemy/src/aws/itty/iam/policy.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/alchemy/src/aws/itty/iam/policy.ts b/alchemy/src/aws/itty/iam/policy.ts index dada427da..88bd73520 100644 --- a/alchemy/src/aws/itty/iam/policy.ts +++ b/alchemy/src/aws/itty/iam/policy.ts @@ -233,14 +233,8 @@ export const Policy = Resource( Description: props.description, Tags: tags, }).pipe( - // Effect.tap((response) => Console.log(`got successful response: ${JSON.stringify(response, null, 2)}`)), - Effect.flatMap((createPolicyResponse) => { - - // FIXME: the policy response may have all the metadata we need - return iam.getPolicy({ PolicyArn: createPolicyResponse!.Policy!.Arn!}); - }), - // FIXME: too broad - Effect.catchAllCause((err) => Effect.fail(new Error(`failing from error: ${err}`))), + // FIXME: too broad? + Effect.catchAll((err) => Effect.fail(new Error(`failing from error: ${err}`))), ); // FIXME: what error handling / retry logic should we have here? // no retry logic, that should be in itty @@ -251,9 +245,6 @@ export const Policy = Resource( const resultPolicy = await Effect.runPromise(createEffect); const p = resultPolicy!.Policy!; - if (!this.quiet) { - console.log(`policy: ${JSON.stringify(p, null, 2)}`); - } return this({ ...props, name: resolvedName, From 5f4deb9674f77b1d1308c818dab53a6613082330 Mon Sep 17 00:00:00 2001 From: Kirk Mitchener Date: Thu, 4 Sep 2025 08:23:55 -0400 Subject: [PATCH 11/11] improved error handling --- alchemy/src/aws/itty/iam/policy.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/alchemy/src/aws/itty/iam/policy.ts b/alchemy/src/aws/itty/iam/policy.ts index 88bd73520..6e52ac2be 100644 --- a/alchemy/src/aws/itty/iam/policy.ts +++ b/alchemy/src/aws/itty/iam/policy.ts @@ -168,6 +168,7 @@ export const Policy = Resource( ) { // calling this.replace() will terminate this run and re-invoke this method // with the "create" phase + // the "old" policy being replaced will be cleaned up by app.finalize() return this.replace(); } @@ -180,9 +181,11 @@ export const Policy = Resource( // List and delete all non-default versions first const versionsResult = yield* iam.listPolicyVersions({ PolicyArn: policyArn, - }); + }).pipe( + Effect.catchTag("NoSuchEntityException", () => Effect.succeed(undefined)) + ); - const versions = versionsResult.Versions || []; + const versions = versionsResult?.Versions || []; // Delete non-default versions for (const version of versions) { @@ -190,7 +193,9 @@ export const Policy = Resource( yield* iam.deletePolicyVersion({ PolicyArn: policyArn, VersionId: version.VersionId, - }); + }).pipe( + Effect.catchTag("NoSuchEntityException", () => Effect.succeed(undefined)) + ); } } @@ -201,14 +206,17 @@ export const Policy = Resource( }) .pipe( Effect.catchTag("NoSuchEntityException", () => - Effect.succeed("Policy doesn't exist"), + Effect.succeed(undefined), ), ); }); - // FIXME: how should we be handling errors? this can fail for example if the policy is attached to a user or role - await Effect.runPromise(deleteEffect); - return this.destroy(); + try { + await Effect.runPromise(deleteEffect); + return this.destroy(); + } catch (err) { + throw new Error(`Delete failed: ${String(err)}`); + } } if (this.phase === "create") {