alchemy 0.1.13 → 0.1.14

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 (86) hide show
  1. package/README.md +12 -2
  2. package/lib/aws/account-id.d.ts +5 -0
  3. package/lib/aws/account-id.d.ts.map +1 -0
  4. package/lib/aws/account-id.js +10 -0
  5. package/lib/aws/account-id.js.map +1 -0
  6. package/lib/aws/index.d.ts +1 -0
  7. package/lib/aws/index.d.ts.map +1 -1
  8. package/lib/aws/index.js +1 -0
  9. package/lib/aws/index.js.map +1 -1
  10. package/lib/aws/oidc/github-oidc-provider.d.ts +11 -0
  11. package/lib/aws/oidc/github-oidc-provider.d.ts.map +1 -0
  12. package/lib/aws/oidc/github-oidc-provider.js +22 -0
  13. package/lib/aws/oidc/github-oidc-provider.js.map +1 -0
  14. package/lib/aws/oidc/index.d.ts +3 -0
  15. package/lib/aws/oidc/index.d.ts.map +1 -0
  16. package/lib/aws/oidc/index.js +3 -0
  17. package/lib/aws/oidc/index.js.map +1 -0
  18. package/lib/aws/oidc/oidc-provider.d.ts +60 -0
  19. package/lib/aws/oidc/oidc-provider.d.ts.map +1 -0
  20. package/lib/aws/oidc/oidc-provider.js +140 -0
  21. package/lib/aws/oidc/oidc-provider.js.map +1 -0
  22. package/lib/aws/queue.d.ts +24 -0
  23. package/lib/aws/queue.d.ts.map +1 -1
  24. package/lib/aws/queue.js +6 -1
  25. package/lib/aws/queue.js.map +1 -1
  26. package/lib/aws/role.d.ts.map +1 -1
  27. package/lib/aws/role.js +49 -1
  28. package/lib/aws/role.js.map +1 -1
  29. package/lib/aws/ses.d.ts.map +1 -1
  30. package/lib/aws/ses.js +20 -28
  31. package/lib/aws/ses.js.map +1 -1
  32. package/lib/aws/table.d.ts.map +1 -1
  33. package/lib/aws/table.js +95 -73
  34. package/lib/aws/table.js.map +1 -1
  35. package/lib/cloudflare/bindings.d.ts +7 -2
  36. package/lib/cloudflare/bindings.d.ts.map +1 -1
  37. package/lib/cloudflare/durable-object-namespace.d.ts +2 -5
  38. package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
  39. package/lib/cloudflare/durable-object-namespace.js +1 -6
  40. package/lib/cloudflare/durable-object-namespace.js.map +1 -1
  41. package/lib/cloudflare/kv-namespace.d.ts +3 -0
  42. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  43. package/lib/cloudflare/kv-namespace.js +4 -0
  44. package/lib/cloudflare/kv-namespace.js.map +1 -1
  45. package/lib/cloudflare/static-site.d.ts.map +1 -1
  46. package/lib/cloudflare/static-site.js +12 -30
  47. package/lib/cloudflare/static-site.js.map +1 -1
  48. package/lib/cloudflare/worker.d.ts +6 -3
  49. package/lib/cloudflare/worker.d.ts.map +1 -1
  50. package/lib/cloudflare/worker.js +41 -15
  51. package/lib/cloudflare/worker.js.map +1 -1
  52. package/lib/github/client.d.ts +34 -0
  53. package/lib/github/client.d.ts.map +1 -0
  54. package/lib/github/client.js +91 -0
  55. package/lib/github/client.js.map +1 -0
  56. package/lib/github/index.d.ts +2 -0
  57. package/lib/github/index.d.ts.map +1 -0
  58. package/lib/github/index.js +2 -0
  59. package/lib/github/index.js.map +1 -0
  60. package/lib/github/secret.d.ts +46 -0
  61. package/lib/github/secret.d.ts.map +1 -0
  62. package/lib/github/secret.js +103 -0
  63. package/lib/github/secret.js.map +1 -0
  64. package/lib/utils/retry.d.ts +13 -0
  65. package/lib/utils/retry.d.ts.map +1 -0
  66. package/lib/utils/retry.js +31 -0
  67. package/lib/utils/retry.js.map +1 -0
  68. package/package.json +10 -2
  69. package/src/aws/account-id.ts +11 -0
  70. package/src/aws/index.ts +1 -0
  71. package/src/aws/oidc/github-oidc-provider.ts +34 -0
  72. package/src/aws/oidc/index.ts +2 -0
  73. package/src/aws/oidc/oidc-provider.ts +253 -0
  74. package/src/aws/queue.ts +23 -1
  75. package/src/aws/role.ts +69 -0
  76. package/src/aws/ses.ts +27 -31
  77. package/src/aws/table.ts +113 -82
  78. package/src/cloudflare/bindings.ts +8 -2
  79. package/src/cloudflare/durable-object-namespace.ts +4 -10
  80. package/src/cloudflare/kv-namespace.ts +11 -0
  81. package/src/cloudflare/static-site.ts +13 -32
  82. package/src/cloudflare/worker.ts +52 -22
  83. package/src/github/client.ts +115 -0
  84. package/src/github/index.ts +1 -0
  85. package/src/github/secret.ts +172 -0
  86. package/src/utils/retry.ts +36 -0
package/src/aws/table.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  } from "@aws-sdk/client-dynamodb";
10
10
  import { ignore } from "../error";
11
11
  import { type Context, Resource } from "../resource";
12
+ import { withExponentialBackoff } from "../utils/retry";
12
13
 
13
14
  export interface TableProps {
14
15
  tableName: string;
@@ -38,7 +39,7 @@ export class Table extends Resource(
38
39
  async (
39
40
  ctx: Context<TableOutput>,
40
41
  props: TableProps,
41
- ): Promise<TableOutput> => {
42
+ ): Promise<TableOutput | void> => {
42
43
  const client = new DynamoDBClient({});
43
44
 
44
45
  if (ctx.event === "delete") {
@@ -70,103 +71,133 @@ export class Table extends Resource(
70
71
  }
71
72
  }
72
73
 
73
- return {
74
- ...props,
75
- id: props.tableName,
76
- arn: `arn:aws:dynamodb:${client.config.region}:${process.env.AWS_ACCOUNT_ID}:table/${props.tableName}`,
77
- tableId: "",
78
- };
74
+ return;
79
75
  } else {
80
- try {
81
- const attributeDefinitions = [
82
- {
83
- AttributeName: props.partitionKey.name,
84
- AttributeType: props.partitionKey.type,
85
- },
86
- ];
87
-
88
- const keySchema: KeySchemaElement[] = [
89
- {
90
- AttributeName: props.partitionKey.name,
91
- KeyType: "HASH",
92
- },
93
- ];
94
-
95
- if (props.sortKey) {
96
- attributeDefinitions.push({
97
- AttributeName: props.sortKey.name,
98
- AttributeType: props.sortKey.type,
99
- });
100
- keySchema.push({
101
- AttributeName: props.sortKey.name,
102
- KeyType: "RANGE",
103
- });
104
- }
76
+ // Setup for table creation
77
+ const attributeDefinitions = [
78
+ {
79
+ AttributeName: props.partitionKey.name,
80
+ AttributeType: props.partitionKey.type,
81
+ },
82
+ ];
105
83
 
106
- await client.send(
107
- new CreateTableCommand({
108
- TableName: props.tableName,
109
- AttributeDefinitions: attributeDefinitions,
110
- KeySchema: keySchema,
111
- BillingMode: props.billingMode || "PAY_PER_REQUEST",
112
- ProvisionedThroughput:
113
- props.billingMode === "PROVISIONED"
114
- ? {
115
- ReadCapacityUnits: props.readCapacity || 5,
116
- WriteCapacityUnits: props.writeCapacity || 5,
117
- }
118
- : undefined,
119
- Tags: props.tags
120
- ? Object.entries(props.tags).map(([Key, Value]) => ({
121
- Key,
122
- Value,
123
- }))
124
- : undefined,
125
- }),
126
- );
84
+ const keySchema: KeySchemaElement[] = [
85
+ {
86
+ AttributeName: props.partitionKey.name,
87
+ KeyType: "HASH",
88
+ },
89
+ ];
90
+
91
+ if (props.sortKey) {
92
+ attributeDefinitions.push({
93
+ AttributeName: props.sortKey.name,
94
+ AttributeType: props.sortKey.type,
95
+ });
96
+ keySchema.push({
97
+ AttributeName: props.sortKey.name,
98
+ KeyType: "RANGE",
99
+ });
100
+ }
101
+
102
+ // Attempt to create the table with exponential backoff for ResourceInUseException
103
+ await withExponentialBackoff(
104
+ async () => {
105
+ try {
106
+ // First check if table already exists
107
+ const describeResponse = await client.send(
108
+ new DescribeTableCommand({
109
+ TableName: props.tableName,
110
+ }),
111
+ );
112
+
113
+ // If table exists and is ACTIVE, no need to create it
114
+ if (describeResponse.Table?.TableStatus === "ACTIVE") {
115
+ return;
116
+ }
127
117
 
128
- // Wait for table to be active
129
- let tableActive = false;
130
- let tableDescription;
131
- while (!tableActive) {
118
+ // If table exists but not ACTIVE, wait for it in the polling loop below
119
+ if (describeResponse.Table) {
120
+ return;
121
+ }
122
+ } catch (error) {
123
+ if (error instanceof ResourceNotFoundException) {
124
+ // Table doesn't exist, try to create it
125
+ await client.send(
126
+ new CreateTableCommand({
127
+ TableName: props.tableName,
128
+ AttributeDefinitions: attributeDefinitions,
129
+ KeySchema: keySchema,
130
+ BillingMode: props.billingMode || "PAY_PER_REQUEST",
131
+ ProvisionedThroughput:
132
+ props.billingMode === "PROVISIONED"
133
+ ? {
134
+ ReadCapacityUnits: props.readCapacity || 5,
135
+ WriteCapacityUnits: props.writeCapacity || 5,
136
+ }
137
+ : undefined,
138
+ Tags: props.tags
139
+ ? Object.entries(props.tags).map(([Key, Value]) => ({
140
+ Key,
141
+ Value,
142
+ }))
143
+ : undefined,
144
+ }),
145
+ );
146
+ } else {
147
+ throw error;
148
+ }
149
+ }
150
+ },
151
+ (error) => error instanceof ResourceInUseException,
152
+ 10, // Max attempts
153
+ 200, // Initial delay in ms
154
+ );
155
+
156
+ // Wait for table to be active with timeout
157
+ let tableActive = false;
158
+ let tableDescription;
159
+ let retryCount = 0;
160
+ const maxRetries = 60; // Wait up to 60 seconds
161
+
162
+ while (!tableActive && retryCount < maxRetries) {
163
+ try {
132
164
  const response = await client.send(
133
165
  new DescribeTableCommand({
134
166
  TableName: props.tableName,
135
167
  }),
136
168
  );
169
+
137
170
  tableActive = response.Table?.TableStatus === "ACTIVE";
138
171
  if (tableActive) {
139
172
  tableDescription = response.Table;
140
173
  } else {
141
- await new Promise((resolve) => setTimeout(resolve, 1000));
174
+ retryCount++;
175
+ // Increasing delay for each retry with some jitter
176
+ const delay = Math.min(1000 * (1 + 0.1 * Math.random()), 5000);
177
+ await new Promise((resolve) => setTimeout(resolve, delay));
142
178
  }
179
+ } catch (error) {
180
+ retryCount++;
181
+ if (!(error instanceof ResourceNotFoundException)) {
182
+ throw error;
183
+ }
184
+ await new Promise((resolve) => setTimeout(resolve, 1000));
143
185
  }
186
+ }
144
187
 
145
- return {
146
- ...props,
147
- id: props.tableName,
148
- arn: tableDescription!.TableArn!,
149
- streamArn: tableDescription!.LatestStreamArn,
150
- tableId: tableDescription!.TableId!,
151
- };
152
- } catch (error) {
153
- if (error instanceof ResourceInUseException) {
154
- // Table already exists, get its details
155
- const response = await client.send(
156
- new DescribeTableCommand({
157
- TableName: props.tableName,
158
- }),
159
- );
160
- return {
161
- ...props,
162
- id: props.tableName,
163
- arn: response.Table!.TableArn!,
164
- streamArn: response.Table!.LatestStreamArn,
165
- tableId: response.Table!.TableId!,
166
- };
167
- }
168
- throw error;
188
+ if (!tableActive) {
189
+ throw new Error(
190
+ `Timed out waiting for table ${props.tableName} to become active`,
191
+ );
169
192
  }
193
+
194
+ return {
195
+ ...props,
196
+ id: props.tableName,
197
+ arn: tableDescription!.TableArn!,
198
+ streamArn: tableDescription!.LatestStreamArn,
199
+ tableId: tableDescription!.TableId!,
200
+ };
170
201
  }
171
202
  },
172
203
  ) {}
@@ -4,11 +4,17 @@
4
4
  * https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/
5
5
  */
6
6
  import type { DurableObjectNamespace } from "./durable-object-namespace";
7
+ import type { KVNamespace } from "./kv-namespace";
8
+ import type { Worker } from "./worker";
9
+
10
+ export type Bindings = {
11
+ [bindingName: string]: Binding;
12
+ };
7
13
 
8
14
  /**
9
- * Union type for all Worker binding types (Alchemy abstraction)
15
+ * L2 Binding Resources.
10
16
  */
11
- export type WorkerBinding = WorkerBindingSpec | DurableObjectNamespace;
17
+ export type Binding = DurableObjectNamespace | KVNamespace | Worker | string;
12
18
 
13
19
  /**
14
20
  * Union type for all Worker binding types (API spec)
@@ -1,7 +1,6 @@
1
- import type { WorkerBinding } from "./bindings";
1
+ import type { Resolved } from "../output";
2
2
 
3
3
  export interface DurableObjectNamespaceInput {
4
- bindingName: string;
5
4
  className: string;
6
5
  scriptName?: string | undefined;
7
6
  environment?: string | undefined;
@@ -10,19 +9,16 @@ export interface DurableObjectNamespaceInput {
10
9
  }
11
10
 
12
11
  export function isDurableObjectNamespace(
13
- binding: WorkerBinding,
14
- ): binding is DurableObjectNamespace {
12
+ binding: any,
13
+ ): binding is Resolved<DurableObjectNamespace> {
15
14
  return (
16
- binding.type === "durable_object_namespace" &&
17
- typeof (binding as any).bindingName === "string"
15
+ typeof binding === "object" && binding.type === "durable_object_namespace"
18
16
  );
19
17
  }
20
18
 
21
19
  export class DurableObjectNamespace implements DurableObjectNamespaceInput {
22
20
  public readonly type = "durable_object_namespace" as const;
23
21
  // alias for bindingName to be consistent with other bindings
24
- public readonly name: string;
25
- public readonly bindingName: string;
26
22
  public readonly className: string;
27
23
  public readonly scriptName?: string | undefined;
28
24
  public readonly environment?: string | undefined;
@@ -33,8 +29,6 @@ export class DurableObjectNamespace implements DurableObjectNamespaceInput {
33
29
  public readonly id: string,
34
30
  input: DurableObjectNamespaceInput,
35
31
  ) {
36
- this.bindingName = input.bindingName;
37
- this.name = this.bindingName;
38
32
  this.className = input.className;
39
33
  this.scriptName = input.scriptName;
40
34
  this.environment = input.environment;
@@ -1,3 +1,4 @@
1
+ import type { Resolved } from "../output";
1
2
  import { type Context, Resource } from "../resource";
2
3
  import { createCloudflareApi } from "./api";
3
4
 
@@ -51,6 +52,7 @@ export interface KVPair {
51
52
  * Output returned after KV Namespace creation/update
52
53
  */
53
54
  export interface KVNamespaceOutput extends KVNamespaceProps {
55
+ type: "kv_namespace";
54
56
  /**
55
57
  * The ID of the namespace
56
58
  */
@@ -67,6 +69,14 @@ export interface KVNamespaceOutput extends KVNamespaceProps {
67
69
  modifiedAt: number;
68
70
  }
69
71
 
72
+ export function isKVNamespace(
73
+ resource: any,
74
+ ): resource is Resolved<KVNamespace> {
75
+ return (
76
+ resource && typeof resource === "object" && resource.type === "kv_namespace"
77
+ );
78
+ }
79
+
70
80
  export class KVNamespace extends Resource(
71
81
  "cloudflare::KVNamespace",
72
82
  async (ctx: Context<KVNamespaceOutput>, props: KVNamespaceProps) => {
@@ -182,6 +192,7 @@ export class KVNamespace extends Resource(
182
192
 
183
193
  // Construct the output
184
194
  const output: KVNamespaceOutput = {
195
+ type: "kv_namespace",
185
196
  id: namespaceId,
186
197
  title: props.title,
187
198
  values: props.values,
@@ -7,7 +7,7 @@ import type { BundleProps } from "../esbuild";
7
7
  import type { Input } from "../input";
8
8
  import { type Context, Resource } from "../resource";
9
9
  import { createCloudflareApi } from "./api";
10
- import type { WorkerBindingSpec } from "./bindings";
10
+ import type { Bindings } from "./bindings";
11
11
  import { generateAssetManifest } from "./generate-asset-manifest";
12
12
  import { KVNamespace } from "./kv-namespace";
13
13
  import { uploadAssetManifest } from "./upload-asset-manifest";
@@ -276,12 +276,12 @@ export class StaticSite extends Resource(
276
276
  const api = await createCloudflareApi();
277
277
 
278
278
  // Step 1: Create or get the KV namespace for assets
279
- const namespace = new KVNamespace("assets", {
279
+ const kv = new KVNamespace("assets", {
280
280
  title: `${siteName}-assets`,
281
281
  });
282
282
 
283
283
  const [{ id: kvNamespaceId }, assetManifest] = await Promise.all([
284
- apply(namespace, {
284
+ apply(kv, {
285
285
  quiet: ctx.quiet,
286
286
  }),
287
287
  generateAssetManifest(props.dir),
@@ -291,39 +291,20 @@ export class StaticSite extends Resource(
291
291
  await uploadAssetManifest(api, kvNamespaceId, assetManifest);
292
292
 
293
293
  // Prepare the bindings for the worker
294
- const bindings: Input<WorkerBindingSpec>[] = [
295
- {
296
- name: "ASSETS",
297
- type: "kv_namespace",
298
- namespace_id: kvNamespaceId,
299
- },
300
- {
301
- name: "INDEX_PAGE",
302
- type: "plain_text",
303
- text: indexPage,
304
- },
305
- ];
306
-
307
- // Add error page binding if specified
308
- if (props.errorPage) {
309
- bindings.push({
310
- name: "ERROR_PAGE",
311
- type: "plain_text",
312
- text: props.errorPage,
313
- });
314
- }
294
+ const bindings: Input<Bindings> = {
295
+ ASSETS: kv,
296
+ INDEX_PAGE: indexPage,
297
+ // Add error page binding if specified
298
+ ...(props.errorPage ? { ERROR_PAGE: props.errorPage } : {}),
299
+ };
315
300
 
316
301
  const routes: Record<string, string> = {};
317
302
  // Create backend worker if configured
318
303
  if (props.routes) {
319
- for (const [path, entrypoint] of Object.entries(props.routes)) {
304
+ for (const [path, worker] of Object.entries(props.routes)) {
320
305
  // @ts-ignore - TODO: need to use Resolved<Worker> to get the string ...
321
- routes[path] = entrypoint.id;
322
- bindings.push({
323
- type: "service",
324
- name: `ROUTE_${entrypoint.id}`,
325
- service: entrypoint.id,
326
- });
306
+ routes[path] = worker.id;
307
+ bindings[`ROUTE_${worker.id}`] = worker;
327
308
  }
328
309
  }
329
310
 
@@ -366,7 +347,7 @@ export class StaticSite extends Resource(
366
347
  },
367
348
  },
368
349
  // place a dependency on the namespace
369
- namespace.id,
350
+ kv.id,
370
351
  );
371
352
 
372
353
  // Get current timestamp
@@ -1,11 +1,13 @@
1
1
  import * as fs from "fs/promises";
2
2
  import { apply } from "../apply";
3
3
  import { Bundle, type BundleProps } from "../esbuild";
4
+ import type { Resolved } from "../output";
4
5
  import { type Context, Resource } from "../resource";
5
6
  import { type CloudflareApi, createCloudflareApi } from "./api";
6
- import type { WorkerBinding, WorkerBindingSpec } from "./bindings";
7
+ import type { Bindings, WorkerBindingSpec } from "./bindings";
7
8
  import type { DurableObjectNamespace } from "./durable-object-namespace";
8
9
  import { isDurableObjectNamespace } from "./durable-object-namespace";
10
+ import { isKVNamespace } from "./kv-namespace";
9
11
  import type { WorkerScriptMetadata } from "./worker-metadata";
10
12
  import type { SingleStepMigration } from "./worker-migration";
11
13
  /**
@@ -54,7 +56,7 @@ export interface WorkerProps {
54
56
  /**
55
57
  * Bindings to attach to the worker
56
58
  */
57
- bindings?: WorkerBinding[];
59
+ bindings?: Bindings;
58
60
 
59
61
  /**
60
62
  * Environment variables to attach to the worker
@@ -94,6 +96,8 @@ export interface WorkerProps {
94
96
  * Output returned after Worker creation/update
95
97
  */
96
98
  export interface WorkerOutput extends Omit<WorkerProps, "url"> {
99
+ type: "service";
100
+
97
101
  /**
98
102
  * The ID of the worker
99
103
  */
@@ -118,7 +122,11 @@ export interface WorkerOutput extends Omit<WorkerProps, "url"> {
118
122
  /**
119
123
  * The bindings that were created
120
124
  */
121
- bindings: WorkerBinding[];
125
+ bindings: Bindings;
126
+ }
127
+
128
+ export function isWorker(resource: any): resource is Resolved<Worker> {
129
+ return resource && typeof resource === "object" && resource.type === "worker";
122
130
  }
123
131
 
124
132
  export class Worker extends Resource(
@@ -144,7 +152,7 @@ export class Worker extends Resource(
144
152
  await assertWorkerDoesNotExist(ctx, api, workerName);
145
153
  }
146
154
 
147
- const oldBindings = await ctx.get<WorkerBinding[]>("bindings");
155
+ const oldBindings = await ctx.get<Bindings>("bindings");
148
156
 
149
157
  const scriptMetadata = await prepareWorkerMetadata(ctx, oldBindings, props);
150
158
 
@@ -174,12 +182,13 @@ export class Worker extends Resource(
174
182
 
175
183
  // Construct the output
176
184
  const output: WorkerOutput = {
185
+ type: "service",
177
186
  id: workerName,
178
187
  name: workerName,
179
188
  script: scriptContent,
180
189
  format: props.format || "esm", // Include format in the output
181
190
  routes: props.routes || [],
182
- bindings: props.bindings || [],
191
+ bindings: props.bindings ?? {},
183
192
  env: props.env,
184
193
  observability: scriptMetadata.observability,
185
194
  createdAt: now,
@@ -283,7 +292,7 @@ async function putWorker(
283
292
  scriptName,
284
293
  );
285
294
 
286
- console.log("Script Metadata:", JSON.stringify(scriptMetadata, null, 2));
295
+ // console.log("Script Metadata:", JSON.stringify(scriptMetadata, null, 2));
287
296
 
288
297
  // Add metadata as JSON
289
298
  formData.append(
@@ -318,7 +327,7 @@ async function putWorker(
318
327
  }
319
328
 
320
329
  interface WorkerMetadata {
321
- bindings: WorkerBinding[];
330
+ bindings: WorkerBindingSpec[];
322
331
  observability: {
323
332
  enabled: boolean;
324
333
  };
@@ -330,12 +339,12 @@ interface WorkerMetadata {
330
339
 
331
340
  async function prepareWorkerMetadata(
332
341
  ctx: Context<WorkerOutput>,
333
- oldBindings: WorkerBinding[] | undefined,
342
+ oldBindings: Bindings | undefined,
334
343
  props: WorkerProps,
335
344
  ): Promise<WorkerMetadata> {
336
345
  // Prepare metadata with bindings
337
346
  const meta: WorkerMetadata = {
338
- bindings: [] as WorkerBinding[],
347
+ bindings: [],
339
348
  observability: {
340
349
  enabled: props.observability?.enabled !== false,
341
350
  },
@@ -349,30 +358,51 @@ async function prepareWorkerMetadata(
349
358
  },
350
359
  };
351
360
 
361
+ const bindings = (props.bindings ?? {}) as Resolved<Bindings>;
362
+
352
363
  // Convert bindings to the format expected by the API
353
- for (const binding of props.bindings ?? []) {
364
+ for (const [bindingName, binding] of Object.entries(bindings)) {
354
365
  // Create a copy of the binding to avoid modifying the original
355
- const bindingData = { ...binding };
356
366
 
357
- if (isDurableObjectNamespace(bindingData)) {
358
- const stableId = bindingData.id;
359
- const className = bindingData.className;
367
+ if (isKVNamespace(binding)) {
368
+ meta.bindings.push({
369
+ type: "kv_namespace",
370
+ name: bindingName,
371
+ namespace_id: binding.id,
372
+ });
373
+ } else if (typeof binding === "string") {
374
+ meta.bindings.push({
375
+ type: "plain_text",
376
+ name: bindingName,
377
+ text: binding,
378
+ });
379
+ } else if (binding.type === "service") {
380
+ meta.bindings.push({
381
+ type: "service",
382
+ name: bindingName,
383
+ service: binding.id,
384
+ });
385
+ } else if (isDurableObjectNamespace(binding)) {
386
+ const stableId = binding.id;
387
+ const className = binding.className;
360
388
 
361
389
  meta.bindings.push({
362
390
  type: "durable_object_namespace",
363
- name: bindingData.bindingName,
391
+ name: bindingName,
364
392
  class_name: className,
365
- script_name: bindingData.scriptName,
366
- environment: bindingData.environment,
367
- namespace_id: bindingData.namespaceId,
393
+ script_name: binding.scriptName,
394
+ environment: binding.environment,
395
+ namespace_id: binding.namespaceId,
368
396
  });
369
397
 
370
- const oldBinding: DurableObjectNamespace | undefined = oldBindings
398
+ const oldBinding: DurableObjectNamespace | undefined = Object.values(
399
+ oldBindings ?? {},
400
+ )
371
401
  ?.filter(isDurableObjectNamespace)
372
402
  ?.find((b) => b.id === stableId);
373
403
 
374
404
  if (!oldBinding) {
375
- if (bindingData.sqlite) {
405
+ if (binding.sqlite) {
376
406
  meta.migrations!.new_sqlite_classes!.push(className);
377
407
  } else {
378
408
  meta.migrations!.new_classes!.push(className);
@@ -384,8 +414,8 @@ async function prepareWorkerMetadata(
384
414
  });
385
415
  }
386
416
  } else {
387
- // For all other binding types, use as-is
388
- meta.bindings.push(bindingData as WorkerBindingSpec);
417
+ // @ts-expect-error - we should never reach here
418
+ throw new Error(`Unsupported binding type: ${binding.type}`);
389
419
  }
390
420
  }
391
421