alchemy 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/lib/ai/ark.d.ts +11 -0
  2. package/lib/ai/ark.js +86 -0
  3. package/lib/ai/client.d.ts +51 -0
  4. package/lib/ai/client.js +29 -0
  5. package/lib/ai/document.d.ts +97 -0
  6. package/lib/ai/document.js +82 -0
  7. package/lib/ai/index.d.ts +2 -0
  8. package/lib/ai/index.js +2 -0
  9. package/lib/ai/object.d.ts +101 -0
  10. package/lib/ai/object.js +70 -0
  11. package/lib/alchemy.d.ts +84 -2
  12. package/lib/alchemy.js +114 -10
  13. package/lib/apply.d.ts +1 -1
  14. package/lib/apply.js +13 -10
  15. package/lib/aws/bucket.d.ts +87 -1
  16. package/lib/aws/bucket.js +48 -1
  17. package/lib/aws/function.d.ts +186 -1
  18. package/lib/aws/function.js +62 -8
  19. package/lib/aws/oidc/github-oidc-provider.d.ts +81 -0
  20. package/lib/aws/oidc/github-oidc-provider.js +68 -0
  21. package/lib/aws/oidc/oidc-provider.d.ts +14 -6
  22. package/lib/aws/oidc/oidc-provider.js +26 -5
  23. package/lib/aws/policy-attachment.d.ts +44 -1
  24. package/lib/aws/policy-attachment.js +31 -0
  25. package/lib/aws/policy.d.ts +160 -1
  26. package/lib/aws/policy.js +78 -0
  27. package/lib/aws/queue.d.ts +83 -1
  28. package/lib/aws/queue.js +39 -0
  29. package/lib/aws/role.d.ts +162 -1
  30. package/lib/aws/role.js +114 -0
  31. package/lib/aws/ses.d.ts +57 -2
  32. package/lib/aws/ses.js +42 -1
  33. package/lib/aws/table.d.ts +96 -1
  34. package/lib/aws/table.js +36 -0
  35. package/lib/cloudflare/asset-manifest.js +0 -1
  36. package/lib/cloudflare/bound.js +0 -1
  37. package/lib/cloudflare/bucket.d.ts +37 -1
  38. package/lib/cloudflare/bucket.js +36 -0
  39. package/lib/cloudflare/durable-object-namespace.d.ts +25 -0
  40. package/lib/cloudflare/durable-object-namespace.js +22 -0
  41. package/lib/cloudflare/kv-namespace.d.ts +37 -1
  42. package/lib/cloudflare/kv-namespace.js +36 -0
  43. package/lib/cloudflare/static-site.d.ts +53 -1
  44. package/lib/cloudflare/static-site.js +53 -1
  45. package/lib/cloudflare/worker-metadata.js +0 -1
  46. package/lib/cloudflare/worker.d.ts +64 -1
  47. package/lib/cloudflare/worker.js +63 -0
  48. package/lib/cloudflare/wrangler.json.d.ts +1 -1
  49. package/lib/cloudflare/zone-settings.js +0 -1
  50. package/lib/cloudflare/zone.d.ts +56 -1
  51. package/lib/cloudflare/zone.js +55 -0
  52. package/lib/context.d.ts +9 -4
  53. package/lib/context.js +4 -2
  54. package/lib/destroy.js +9 -8
  55. package/lib/esbuild/bundle.d.ts +43 -4
  56. package/lib/esbuild/bundle.js +16 -0
  57. package/lib/fs/file-collection.d.ts +16 -0
  58. package/lib/fs/file-collection.js +6 -0
  59. package/lib/fs/file-ref.d.ts +14 -0
  60. package/lib/fs/file-ref.js +6 -0
  61. package/lib/fs/file.d.ts +78 -5
  62. package/lib/fs/file.js +39 -3
  63. package/lib/fs/folder.d.ts +37 -5
  64. package/lib/fs/folder.js +28 -3
  65. package/lib/fs/index.d.ts +6 -0
  66. package/lib/fs/index.js +6 -0
  67. package/lib/fs/json-file.d.ts +16 -0
  68. package/lib/fs/json-file.js +7 -0
  69. package/lib/fs/text-file.d.ts +12 -0
  70. package/lib/fs/text-file.js +7 -0
  71. package/lib/fs/typescript-file.d.ts +19 -0
  72. package/lib/fs/typescript-file.js +14 -0
  73. package/lib/fs/yaml-file.d.ts +19 -0
  74. package/lib/fs/yaml-file.js +8 -0
  75. package/lib/github/secret.d.ts +63 -2
  76. package/lib/github/secret.js +61 -1
  77. package/lib/internal/docs.d.ts +5 -0
  78. package/lib/internal/docs.js +324 -0
  79. package/lib/resource.d.ts +2 -2
  80. package/lib/resource.js +4 -0
  81. package/lib/scope.d.ts +1 -1
  82. package/lib/scope.js +6 -6
  83. package/lib/secret.d.ts +67 -0
  84. package/lib/secret.js +67 -0
  85. package/lib/shadcn/component.d.ts +1 -1
  86. package/lib/state.d.ts +1 -1
  87. package/lib/state.js +8 -1
  88. package/lib/stripe/price.d.ts +51 -1
  89. package/lib/stripe/price.js +50 -0
  90. package/lib/stripe/product.d.ts +39 -1
  91. package/lib/stripe/product.js +38 -0
  92. package/lib/stripe/webhook.d.ts +46 -1
  93. package/lib/stripe/webhook.js +45 -0
  94. package/lib/test/bun.d.ts +64 -0
  95. package/lib/test/bun.js +35 -3
  96. package/lib/util/serde.d.ts +3 -1
  97. package/lib/util/serde.js +20 -4
  98. package/lib/vite/vite.d.ts +1 -1
  99. package/lib/vitepress/dependencies.d.ts +12 -0
  100. package/lib/vitepress/dependencies.js +35 -0
  101. package/lib/vitepress/home-page.d.ts +133 -0
  102. package/lib/vitepress/home-page.js +11 -0
  103. package/lib/vitepress/index.d.ts +2 -0
  104. package/lib/vitepress/index.js +2 -0
  105. package/lib/vitepress/vitepress.d.ts +71 -0
  106. package/lib/vitepress/vitepress.js +266 -0
  107. package/package.json +22 -10
  108. package/src/ai/ark.ts +118 -0
  109. package/src/ai/client.ts +78 -0
  110. package/src/ai/document.ts +160 -0
  111. package/src/ai/index.ts +2 -0
  112. package/src/ai/object.ts +149 -0
  113. package/src/alchemy.ts +192 -17
  114. package/src/apply.ts +25 -13
  115. package/src/aws/bucket.ts +111 -7
  116. package/src/aws/function.ts +231 -10
  117. package/src/aws/oidc/github-oidc-provider.ts +83 -0
  118. package/src/aws/oidc/oidc-provider.ts +39 -7
  119. package/src/aws/policy-attachment.ts +44 -1
  120. package/src/aws/policy.ts +177 -2
  121. package/src/aws/queue.ts +89 -0
  122. package/src/aws/role.ts +174 -2
  123. package/src/aws/ses.ts +56 -1
  124. package/src/aws/table.ts +103 -0
  125. package/src/cloudflare/bucket.ts +36 -0
  126. package/src/cloudflare/durable-object-namespace.ts +25 -0
  127. package/src/cloudflare/kv-namespace.ts +36 -0
  128. package/src/cloudflare/static-site.ts +53 -1
  129. package/src/cloudflare/worker.ts +63 -0
  130. package/src/cloudflare/zone.ts +55 -0
  131. package/src/context.ts +30 -10
  132. package/src/destroy.ts +11 -8
  133. package/src/esbuild/bundle.ts +53 -3
  134. package/src/fs/file-collection.ts +24 -0
  135. package/src/fs/file-ref.ts +22 -0
  136. package/src/fs/file.ts +107 -6
  137. package/src/fs/folder.ts +45 -5
  138. package/src/fs/index.ts +6 -0
  139. package/src/fs/json-file.ts +23 -0
  140. package/src/fs/text-file.ts +19 -0
  141. package/src/fs/typescript-file.ts +36 -0
  142. package/src/fs/yaml-file.ts +26 -0
  143. package/src/github/secret.ts +65 -2
  144. package/src/internal/docs.ts +369 -0
  145. package/src/resource.ts +7 -2
  146. package/src/scope.ts +9 -9
  147. package/src/secret.ts +67 -0
  148. package/src/state.ts +9 -2
  149. package/src/stripe/price.ts +50 -0
  150. package/src/stripe/product.ts +38 -0
  151. package/src/stripe/webhook.ts +45 -0
  152. package/src/test/bun.ts +83 -3
  153. package/src/util/serde.ts +30 -4
  154. package/src/vitepress/dependencies.ts +68 -0
  155. package/src/vitepress/home-page.ts +166 -0
  156. package/src/vitepress/index.md +75 -0
  157. package/src/vitepress/index.ts +2 -0
  158. package/src/vitepress/vitepress.ts +382 -0
  159. package/lib/project/vite.d.ts +0 -90
  160. package/lib/project/vite.js +0 -228
  161. package/src/project/vite.ts +0 -406
@@ -0,0 +1,369 @@
1
+ import { type } from "arktype";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { Document } from "../ai/document";
5
+ import { Object } from "../ai/object";
6
+ import { alchemy } from "../alchemy";
7
+ import { Folder } from "../fs/folder";
8
+ import { VitePressProject } from "../vitepress/vitepress";
9
+
10
+ export interface DocsProps {
11
+ docs?: boolean | number;
12
+ }
13
+
14
+ export type AlchemyDocs = Awaited<ReturnType<typeof AlchemyDocs>>;
15
+
16
+ export async function AlchemyDocs({ docs: isDocsEnabled }: DocsProps) {
17
+ const root = (await Folder("alchemy-web")).path;
18
+
19
+ const docs = (await Folder(path.join(root, "docs"))).path;
20
+
21
+ const providersDir = (await Folder(path.join(docs, "providers"))).path;
22
+
23
+ const exclude = ["util", "test", "vitepress", "vite", "shadcn", "internal"];
24
+
25
+ // Get all folders in the alchemy/src directory
26
+ let providers = (
27
+ await fs.readdir(path.resolve("alchemy", "src"), {
28
+ withFileTypes: true,
29
+ })
30
+ )
31
+ .filter((dirent) => dirent.isDirectory() && !exclude.includes(dirent.name))
32
+ .map((dirent) => path.join(dirent.parentPath, dirent.name));
33
+
34
+ // For each provider, list all files
35
+ if (isDocsEnabled === false) {
36
+ return;
37
+ } else if (typeof isDocsEnabled === "number") {
38
+ providers = providers.slice(0, isDocsEnabled);
39
+ }
40
+
41
+ await Promise.all([
42
+ ...providers.map(async (provider) => {
43
+ const providerName = path.basename(provider);
44
+ const files = (
45
+ await fs.readdir(path.resolve(provider), {
46
+ withFileTypes: true,
47
+ })
48
+ )
49
+ .filter((dirent) => dirent.isFile())
50
+ .map((dirent) =>
51
+ path.relative(process.cwd(), path.resolve(provider, dirent.name)),
52
+ )
53
+ .filter((file) => file.endsWith(".ts") && !file.endsWith("index.ts"));
54
+
55
+ const {
56
+ object: { groups },
57
+ } = await Object(`docs/${providerName}`, {
58
+ schema: type({
59
+ groups: type({
60
+ title: type("string").describe(
61
+ "The title of the group, should be the Resource Name exactly without spaces, e.g. Bucket or Static Site.",
62
+ ),
63
+ filename: type("string").describe(
64
+ "The filename of the Resource's Document, e.g. bucket.md or static-site.md",
65
+ ),
66
+ category: type("'Resource'|'Client'|'Utility'|'Types'").describe(
67
+ "The classification of the Resource's Document, one of: Resource, Client, Utility, or Types.",
68
+ ),
69
+ }).array(),
70
+ }),
71
+ system: await alchemy`
72
+ You are a technical writer tasked with identifying the distinct documents that need to be written for a document group (folder) in a documentation site.
73
+ You will be provided with a list of documents and instructions on how to classify them.
74
+ Each document has a title, file name, and category.
75
+ `,
76
+ prompt: await alchemy`
77
+ Identify and classify the documents that need to be written for the '${provider}' Service's Alchemy Resources.
78
+ For background knowledge on Alchemy, see ${alchemy.file("./README.md")}.
79
+ For background knowledge on the structure of an Alchemy Resource, see ${alchemy.file("./.cursorrules")}.
80
+
81
+ The ${provider} Service has the following resources:
82
+ ${alchemy.files(files)}
83
+
84
+ A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...).
85
+ A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
86
+ A file is considered a "Utility" if it contains utility functions that are not resources or clients.
87
+ A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
88
+
89
+ The title should be simply the name of the resource, e.g. "Bucket" or "Function", except with spaces, e.g. "Static Site" instead of "StaticSite". Maintain all other casing.
90
+ `,
91
+ });
92
+
93
+ const providerDocsDir = (
94
+ await Folder(path.join(providersDir, providerName))
95
+ ).path;
96
+
97
+ await Promise.all(
98
+ groups
99
+ .filter((g) => g.category === "Resource")
100
+ .map(async (g) => {
101
+ await Document(`docs/${providerName}/${g.title}`, {
102
+ path: path.join(
103
+ providerDocsDir,
104
+ `${g.filename.replace(".ts", "").replace(".md", "")}.md`,
105
+ ),
106
+ prompt: await alchemy`
107
+ You are a technical writer writing API documentation for an Alchemy IaC Resource.
108
+ See ${alchemy.file("./README.md")} to understand the overview of Alchemy.
109
+ See ${alchemy.file("./.cursorrules")} to better understand the structure and convention of an Alchemy Resource.
110
+
111
+ Relevant files for the ${providerName} Service:
112
+ ${alchemy.files(files)}
113
+
114
+ Write concise documentation for the "${g.title}" Resource.
115
+
116
+ > [!CAUTION]
117
+ > Avoid the temptation to over explain or over describe. Focus on concise, simple, high value snippets. One heading and 0-1 descriptions per snippet.
118
+
119
+ > [!TIP]
120
+ > Make sure the examples follow a natural progression from the minimal example to logical next steps of how the Resource might be used.
121
+
122
+ Each document must follow the following format:
123
+
124
+ # ${g.title}
125
+
126
+ (simple description with an external link to the provider's website)
127
+ e.g.
128
+ The Efs component lets you add [Amazon Elastic File System (EFS)](https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html) to your app.
129
+
130
+ # Minimal Example
131
+
132
+ \`\`\`ts
133
+ import { ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
134
+
135
+ (example)
136
+ \`\`\`
137
+
138
+
139
+ # Create the ${g.title}
140
+
141
+ \`\`\`ts
142
+ import { ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
143
+
144
+ (example)
145
+ \`\`\`
146
+
147
+ ${
148
+ providerName === "cloudflare"
149
+ ? await alchemy`# Bind to a Worker
150
+ (if it is a Cloudflare Resource)
151
+
152
+ \`\`\`ts
153
+ import { Worker, ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
154
+
155
+ const myResource = await ${g.title.replaceAll(" ", "")}("my-resource", {
156
+ // ...
157
+ });
158
+
159
+ await Worker("my-worker", {
160
+ name: "my-worker",
161
+ script: "console.log('Hello, world!')",
162
+ bindings: {
163
+ myResource,
164
+ },
165
+ });
166
+ \`\`\``
167
+ : ""
168
+ }
169
+ `,
170
+ });
171
+ // Each code snippet should use twoslash syntax for proper highlighting.
172
+
173
+ // E.g.
174
+ // \`\`\`ts twoslash
175
+ // import alchemy from "alchemy";
176
+
177
+ // alchemy
178
+ // // ^?
179
+
180
+ // // it needs to be placed under the symbol like so:
181
+ // const foo = "string";
182
+ // // ^?
183
+
184
+ // const basicBucket = await Bucket("my-app-storage", {
185
+ // // ^?
186
+ // bucketName: "my-app-storage",
187
+ // tags: {
188
+ // Environment: "production",
189
+ // Project: "my-app"
190
+ // }
191
+ // });
192
+
193
+ // alchemy.ru
194
+ // // ^|
195
+ // \`\`\`
196
+
197
+ // The \`^?\` syntax is for displaying the type of an expression.
198
+ // The \`^|\` syntax is for displaying auto-completions after a dot and (optional prefix)
199
+ }),
200
+ );
201
+ }),
202
+
203
+ VitePressProject("docs", {
204
+ name: "alchemy-web",
205
+ title: "Alchemy",
206
+ description: "Alchemy is an TypeScript-native, embeddable IaC library",
207
+ overwrite: true,
208
+ delete: false,
209
+ tsconfig: {
210
+ extends: "../tsconfig.base.json",
211
+ references: ["../alchemy/tsconfig.json"],
212
+ },
213
+ devDependencies: {
214
+ alchemy: "workspace:*",
215
+ },
216
+ theme: {
217
+ light: "light-plus",
218
+ dark: "dark-plus",
219
+ },
220
+ home: {
221
+ layout: "home",
222
+ hero: {
223
+ text: "Alchemy",
224
+ tagline: "Alchemy is a TypeScript-native, embeddable IaC library",
225
+ actions: [
226
+ {
227
+ text: "Get Started",
228
+ link: "/docs",
229
+ theme: "brand",
230
+ },
231
+ ],
232
+ },
233
+ features: [
234
+ {
235
+ title: "Easy to use",
236
+ details: "Alchemy is easy to use and understand",
237
+ },
238
+ ],
239
+ },
240
+ themeConfig: {
241
+ sidebar: {
242
+ "/blog/": [
243
+ { text: "Blog", items: [{ text: "Blog", link: "/blog/" }] },
244
+ ],
245
+ "/docs/": [
246
+ {
247
+ text: "Getting Started",
248
+ items: [
249
+ { text: "Install", link: "/docs/getting-started/install" },
250
+ ],
251
+ },
252
+ {
253
+ text: "Guides",
254
+ items: [
255
+ {
256
+ text: "Custom Resource",
257
+ link: "/docs/guides/custom-resource",
258
+ },
259
+ {
260
+ text: "Automating with LLMs",
261
+ link: "/docs/guides/llms",
262
+ },
263
+ ],
264
+ },
265
+ {
266
+ text: "Core",
267
+ collapsed: true,
268
+ items: [
269
+ { text: "App", link: "/docs/core/app" },
270
+ { text: "Resource", link: "/docs/core/resource" },
271
+ { text: "Scope", link: "/docs/core/scope" },
272
+ { text: "Phase", link: "/docs/core/phase" },
273
+ { text: "Finalize", link: "/docs/core/finalize" },
274
+ { text: "State", link: "/docs/core/state" },
275
+ { text: "Secret", link: "/docs/core/secret" },
276
+ { text: "Context", link: "/docs/core/context" },
277
+ ],
278
+ },
279
+ {
280
+ text: "Resources",
281
+ items: [
282
+ {
283
+ text: "AWS",
284
+ link: "/docs/aws",
285
+ collapsed: true,
286
+ items: [
287
+ { text: "Bucket", link: "/docs/aws/bucket" },
288
+ { text: "Function", link: "/docs/aws/function" },
289
+ { text: "Policy", link: "/docs/aws/policy" },
290
+ { text: "Queue", link: "/docs/aws/queue" },
291
+ { text: "Table", link: "/docs/aws/table" },
292
+ { text: "Simple Email Service", link: "/docs/aws/ses" },
293
+ ].sort((a, b) => a.text.localeCompare(b.text)),
294
+ },
295
+ {
296
+ text: "Cloudflare",
297
+ link: "/docs/cloudflare",
298
+ collapsed: true,
299
+ items: [
300
+ { text: "Bucket", link: "/docs/cloudflare/bucket" },
301
+ {
302
+ text: "Durable Object",
303
+ link: "/docs/cloudflare/durable-object",
304
+ },
305
+ {
306
+ text: "Static Site",
307
+ link: "/docs/cloudflare/static-site",
308
+ },
309
+ {
310
+ text: "KV Namespace",
311
+ link: "/docs/cloudflare/kv-namespace",
312
+ },
313
+ { text: "Worker", link: "/docs/cloudflare/worker" },
314
+ { text: "Zone", link: "/docs/cloudflare/zone" },
315
+ ].sort((a, b) => a.text.localeCompare(b.text)),
316
+ },
317
+ {
318
+ text: "Stripe",
319
+ link: "/docs/stripe",
320
+ collapsed: true,
321
+ items: [
322
+ { text: "Product", link: "/docs/stripe/product" },
323
+ { text: "Price", link: "/docs/stripe/price" },
324
+ ],
325
+ },
326
+ {
327
+ text: "GitHub",
328
+ link: "/docs/github",
329
+ collapsed: true,
330
+ items: [{ text: "Secret", link: "/docs/github/secret" }],
331
+ },
332
+ {
333
+ text: "File System",
334
+ link: "/docs/fs",
335
+ collapsed: true,
336
+ items: [
337
+ { text: "File", link: "/docs/fs/file" },
338
+ { text: "Folder", link: "/docs/fs/folder" },
339
+ ],
340
+ },
341
+ ].sort((a, b) => a.text.localeCompare(b.text)),
342
+ },
343
+ ],
344
+ "/examples/": [
345
+ {
346
+ text: "Examples",
347
+ items: [{ text: "Foo", link: "/examples/foo" }],
348
+ },
349
+ ],
350
+ "/": [
351
+ {
352
+ text: "Home",
353
+ items: [
354
+ { text: "Markdown Examples", link: "/markdown-examples" },
355
+ { text: "Runtime API Examples", link: "/api-examples" },
356
+ ],
357
+ },
358
+ ],
359
+ },
360
+ socialLinks: [
361
+ {
362
+ icon: "github",
363
+ link: "https://github.com/sam-goodwin/alchemy",
364
+ },
365
+ ],
366
+ },
367
+ }),
368
+ ]);
369
+ }
package/src/resource.ts CHANGED
@@ -67,7 +67,7 @@ type IsClass = {
67
67
  };
68
68
 
69
69
  type ResourceLifecycleHandler = (
70
- this: Context<any>,
70
+ this: Context<any, any>,
71
71
  id: string,
72
72
  props: any,
73
73
  ) => Promise<Resource<string>>;
@@ -75,7 +75,7 @@ type ResourceLifecycleHandler = (
75
75
  // see: https://x.com/samgoodwin89/status/1904640134097887653
76
76
  type Handler<F extends (...args: any[]) => any> =
77
77
  | F
78
- | (((this: any, id: string, props: Parameters<F>[1]) => never) & IsClass);
78
+ | (((this: any, id: string, props?: {}) => never) & IsClass);
79
79
 
80
80
  export function Resource<
81
81
  const Type extends string,
@@ -104,6 +104,11 @@ export function Resource<
104
104
  ): Promise<Resource<string>> => {
105
105
  const scope = _Scope.current;
106
106
 
107
+ if (resourceID.includes(":")) {
108
+ // we want to use : as an internal separator for resources
109
+ throw new Error(`ID cannot include colons: ${resourceID}`);
110
+ }
111
+
107
112
  if (scope.resources.has(resourceID)) {
108
113
  // TODO(sam): do we want to throw?
109
114
  // it's kind of awesome that you can re-create a resource and call apply
package/src/scope.ts CHANGED
@@ -98,6 +98,14 @@ export class Scope {
98
98
  return [...this.chain, resourceID].join("/");
99
99
  }
100
100
 
101
+ public async run<T>(fn: (scope: Scope) => Promise<T>): Promise<T> {
102
+ return scopeStorage.run(this, () => fn(this));
103
+ }
104
+
105
+ [Symbol.asyncDispose]() {
106
+ return this.finalize();
107
+ }
108
+
101
109
  public async finalize() {
102
110
  if (!this.isErrored) {
103
111
  // TODO: need to detect if it is in error
@@ -118,18 +126,10 @@ export class Scope {
118
126
  }
119
127
  }
120
128
 
121
- public async run<T>(fn: (scope: Scope) => Promise<T>): Promise<T> {
122
- return scopeStorage.run(this, () => fn(this));
123
- }
124
-
125
- [Symbol.asyncDispose]() {
126
- return this.finalize();
127
- }
128
-
129
129
  /**
130
130
  * Returns a string representation of the scope.
131
131
  */
132
- toString() {
132
+ public toString() {
133
133
  return `Scope(
134
134
  chain=${this.chain.join("/")},
135
135
  resources=[${Array.from(this.resources.values())
package/src/secret.ts CHANGED
@@ -1,8 +1,46 @@
1
+ /**
2
+ * Internal wrapper for sensitive values like API keys and credentials.
3
+ * When stored in alchemy state files, the value is automatically encrypted
4
+ * using the application's password. The password can be provided either:
5
+ *
6
+ * 1. Globally when initializing the alchemy application:
7
+ * ```ts
8
+ * const app = alchemy("my-app", {
9
+ * password: process.env.SECRET_PASSPHRASE
10
+ * });
11
+ * ```
12
+ *
13
+ * 2. For a specific scope using alchemy.run:
14
+ * ```ts
15
+ * await alchemy.run("scope-name", {
16
+ * password: process.env.SECRET_PASSPHRASE
17
+ * }, async () => {
18
+ * // Secrets in this scope will use this password
19
+ * alchemy.secret(process.env.MY_SECRET)
20
+ * });
21
+ * ```
22
+ *
23
+ * Without a password, secrets cannot be encrypted or decrypted, and operations
24
+ * involving sensitive values will fail.
25
+ *
26
+ * @example
27
+ * // In state file (.alchemy/app/prod/resource.json):
28
+ * {
29
+ * "props": {
30
+ * "apiKey": {
31
+ * "@secret": "encrypted-value-here..." // encrypted using app password
32
+ * }
33
+ * }
34
+ * }
35
+ */
1
36
  export class Secret {
2
37
  public readonly type = "secret";
3
38
  constructor(readonly unencrypted: string) {}
4
39
  }
5
40
 
41
+ /**
42
+ * Type guard to check if a value is a Secret wrapper
43
+ */
6
44
  export function isSecret(binding: any): binding is Secret {
7
45
  return (
8
46
  binding instanceof Secret ||
@@ -10,6 +48,35 @@ export function isSecret(binding: any): binding is Secret {
10
48
  );
11
49
  }
12
50
 
51
+ /**
52
+ * Wraps a sensitive value so it will be encrypted when stored in state files.
53
+ * Requires a password to be set either globally in the alchemy application options
54
+ * or locally in an alchemy.run scope.
55
+ *
56
+ * @example
57
+ * // Global password for all secrets
58
+ * const app = alchemy("my-app", {
59
+ * password: process.env.SECRET_PASSPHRASE
60
+ * });
61
+ *
62
+ * const resource = await Resource("my-resource", {
63
+ * apiKey: alchemy.secret(process.env.API_KEY)
64
+ * });
65
+ *
66
+ * @example
67
+ * // Scoped password for specific secrets
68
+ * await alchemy.run("secure-scope", {
69
+ * password: process.env.SCOPE_SECRET_PASSPHRASE
70
+ * }, async () => {
71
+ * const resource = await Resource("my-resource", {
72
+ * apiKey: alchemy.secret(process.env.API_KEY)
73
+ * });
74
+ * });
75
+ *
76
+ * @param unencrypted The sensitive value to encrypt in state files
77
+ * @throws {Error} If the value is undefined
78
+ * @throws {Error} If no password is set in the alchemy application options or current scope
79
+ */
13
80
  export function secret<S extends string | undefined>(unencrypted: S): Secret {
14
81
  if (unencrypted === undefined) {
15
82
  throw new Error("Secret cannot be undefined");
package/src/state.ts CHANGED
@@ -7,7 +7,7 @@ import { deserialize, serialize } from "./util/serde";
7
7
 
8
8
  export interface State<
9
9
  Kind extends string = string,
10
- Props extends ResourceProps = ResourceProps,
10
+ Props extends ResourceProps | undefined = ResourceProps | undefined,
11
11
  Out extends Resource = Resource,
12
12
  > {
13
13
  status:
@@ -74,7 +74,8 @@ export class FileSystemStateStore implements StateStore {
74
74
  });
75
75
  return files
76
76
  .filter((dirent) => dirent.isFile() && dirent.name.endsWith(".json"))
77
- .map((dirent) => dirent.name.replace(/\.json$/, ""));
77
+ .map((dirent) => dirent.name.replace(/\.json$/, ""))
78
+ .map((key) => key.replaceAll(":", "/"));
78
79
  } catch (error: any) {
79
80
  if (error.code === "ENOENT") {
80
81
  return [];
@@ -138,6 +139,12 @@ export class FileSystemStateStore implements StateStore {
138
139
  }
139
140
 
140
141
  private async getPath(key: string): Promise<string> {
142
+ if (key.includes(":")) {
143
+ throw new Error(`ID cannot include colons: ${key}`);
144
+ }
145
+ if (key.includes("/")) {
146
+ key = key.replaceAll("/", ":");
147
+ }
141
148
  const file = path.join(this.dir, `${key}.json`);
142
149
  const dir = path.dirname(file);
143
150
  await fs.promises.mkdir(dir, { recursive: true });
@@ -125,6 +125,56 @@ export interface Price extends Resource<"stripe::Price">, PriceProps {
125
125
  lookupKey?: string;
126
126
  }
127
127
 
128
+ /**
129
+ * Create and manage Stripe prices for products
130
+ *
131
+ * @example
132
+ * // Create a one-time fixed price for a product
133
+ * const oneTimePrice = await Price("basic-license", {
134
+ * currency: "usd",
135
+ * unitAmount: 2999, // $29.99
136
+ * product: "prod_xyz"
137
+ * });
138
+ *
139
+ * @example
140
+ * // Create a recurring subscription price with fixed monthly billing
141
+ * const subscriptionPrice = await Price("pro-monthly", {
142
+ * currency: "usd",
143
+ * unitAmount: 1499, // $14.99/month
144
+ * product: "prod_xyz",
145
+ * recurring: {
146
+ * interval: "month",
147
+ * usageType: "licensed"
148
+ * }
149
+ * });
150
+ *
151
+ * @example
152
+ * // Create a metered price for usage-based billing
153
+ * const meteredPrice = await Price("storage", {
154
+ * currency: "usd",
155
+ * unitAmount: 25, // $0.25 per GB
156
+ * product: "prod_xyz",
157
+ * recurring: {
158
+ * interval: "month",
159
+ * usageType: "metered",
160
+ * aggregateUsage: "sum"
161
+ * }
162
+ * });
163
+ *
164
+ * @example
165
+ * // Create a tiered price with tax behavior
166
+ * const tieredPrice = await Price("enterprise", {
167
+ * currency: "usd",
168
+ * unitAmount: 10000, // $100.00
169
+ * product: "prod_xyz",
170
+ * billingScheme: "tiered",
171
+ * taxBehavior: "exclusive",
172
+ * metadata: {
173
+ * tier: "enterprise",
174
+ * features: "all"
175
+ * }
176
+ * });
177
+ */
128
178
  export const Price = Resource(
129
179
  "stripe::Price",
130
180
  async function (
@@ -99,6 +99,44 @@ export interface Product extends Resource<"stripe::Product">, ProductProps {
99
99
  };
100
100
  }
101
101
 
102
+ /**
103
+ * Create and manage Stripe products
104
+ *
105
+ * @example
106
+ * // Create a basic digital product
107
+ * const digitalProduct = await Product("basic-software", {
108
+ * name: "Basic Software License",
109
+ * description: "Single-user license for basic software package",
110
+ * metadata: {
111
+ * type: "digital",
112
+ * features: "basic"
113
+ * }
114
+ * });
115
+ *
116
+ * @example
117
+ * // Create a physical product with shipping details
118
+ * const physicalProduct = await Product("premium-hardware", {
119
+ * name: "Premium Hardware Kit",
120
+ * description: "Complete hardware kit with premium components",
121
+ * shippable: true,
122
+ * images: ["https://example.com/hardware-kit.jpg"],
123
+ * unitLabel: "kit",
124
+ * statementDescriptor: "PREMIUM HW KIT"
125
+ * });
126
+ *
127
+ * @example
128
+ * // Create a service product with tax code
129
+ * const serviceProduct = await Product("consulting", {
130
+ * name: "Professional Consulting",
131
+ * description: "Expert consulting services",
132
+ * type: "service",
133
+ * taxCode: "txcd_10000000",
134
+ * metadata: {
135
+ * industry: "technology",
136
+ * expertise: "cloud"
137
+ * }
138
+ * });
139
+ */
102
140
  export const Product = Resource(
103
141
  "stripe::Product",
104
142
  async function (