alchemy 0.1.13 → 0.1.15

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/lib/aws/account-id.d.ts +5 -0
  2. package/lib/aws/account-id.d.ts.map +1 -0
  3. package/lib/aws/account-id.js +10 -0
  4. package/lib/aws/account-id.js.map +1 -0
  5. package/lib/aws/index.d.ts +1 -0
  6. package/lib/aws/index.d.ts.map +1 -1
  7. package/lib/aws/index.js +1 -0
  8. package/lib/aws/index.js.map +1 -1
  9. package/lib/aws/oidc/github-oidc-provider.d.ts +11 -0
  10. package/lib/aws/oidc/github-oidc-provider.d.ts.map +1 -0
  11. package/lib/aws/oidc/github-oidc-provider.js +22 -0
  12. package/lib/aws/oidc/github-oidc-provider.js.map +1 -0
  13. package/lib/aws/oidc/index.d.ts +3 -0
  14. package/lib/aws/oidc/index.d.ts.map +1 -0
  15. package/lib/aws/oidc/index.js +3 -0
  16. package/lib/aws/oidc/index.js.map +1 -0
  17. package/lib/aws/oidc/oidc-provider.d.ts +60 -0
  18. package/lib/aws/oidc/oidc-provider.d.ts.map +1 -0
  19. package/lib/aws/oidc/oidc-provider.js +140 -0
  20. package/lib/aws/oidc/oidc-provider.js.map +1 -0
  21. package/lib/aws/queue.d.ts +24 -0
  22. package/lib/aws/queue.d.ts.map +1 -1
  23. package/lib/aws/queue.js +6 -1
  24. package/lib/aws/queue.js.map +1 -1
  25. package/lib/aws/role.d.ts.map +1 -1
  26. package/lib/aws/role.js +49 -1
  27. package/lib/aws/role.js.map +1 -1
  28. package/lib/aws/ses.d.ts.map +1 -1
  29. package/lib/aws/ses.js +20 -28
  30. package/lib/aws/ses.js.map +1 -1
  31. package/lib/aws/table.d.ts.map +1 -1
  32. package/lib/aws/table.js +113 -79
  33. package/lib/aws/table.js.map +1 -1
  34. package/lib/cloudflare/bindings.d.ts +7 -2
  35. package/lib/cloudflare/bindings.d.ts.map +1 -1
  36. package/lib/cloudflare/durable-object-namespace.d.ts +2 -5
  37. package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
  38. package/lib/cloudflare/durable-object-namespace.js +1 -6
  39. package/lib/cloudflare/durable-object-namespace.js.map +1 -1
  40. package/lib/cloudflare/kv-namespace.d.ts +3 -0
  41. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  42. package/lib/cloudflare/kv-namespace.js +28 -7
  43. package/lib/cloudflare/kv-namespace.js.map +1 -1
  44. package/lib/cloudflare/static-site.d.ts.map +1 -1
  45. package/lib/cloudflare/static-site.js +12 -30
  46. package/lib/cloudflare/static-site.js.map +1 -1
  47. package/lib/cloudflare/worker.d.ts +6 -3
  48. package/lib/cloudflare/worker.d.ts.map +1 -1
  49. package/lib/cloudflare/worker.js +82 -41
  50. package/lib/cloudflare/worker.js.map +1 -1
  51. package/lib/github/client.d.ts +34 -0
  52. package/lib/github/client.d.ts.map +1 -0
  53. package/lib/github/client.js +91 -0
  54. package/lib/github/client.js.map +1 -0
  55. package/lib/github/index.d.ts +2 -0
  56. package/lib/github/index.d.ts.map +1 -0
  57. package/lib/github/index.js +2 -0
  58. package/lib/github/index.js.map +1 -0
  59. package/lib/github/secret.d.ts +46 -0
  60. package/lib/github/secret.d.ts.map +1 -0
  61. package/lib/github/secret.js +103 -0
  62. package/lib/github/secret.js.map +1 -0
  63. package/lib/utils/retry.d.ts +13 -0
  64. package/lib/utils/retry.d.ts.map +1 -0
  65. package/lib/utils/retry.js +32 -0
  66. package/lib/utils/retry.js.map +1 -0
  67. package/package.json +10 -3
  68. package/src/aws/account-id.ts +11 -0
  69. package/src/aws/index.ts +1 -0
  70. package/src/aws/oidc/github-oidc-provider.ts +34 -0
  71. package/src/aws/oidc/index.ts +2 -0
  72. package/src/aws/oidc/oidc-provider.ts +253 -0
  73. package/src/aws/queue.ts +23 -1
  74. package/src/aws/role.ts +69 -0
  75. package/src/aws/ses.ts +27 -31
  76. package/src/aws/table.ts +143 -90
  77. package/src/cloudflare/bindings.ts +8 -2
  78. package/src/cloudflare/durable-object-namespace.ts +4 -10
  79. package/src/cloudflare/kv-namespace.ts +45 -15
  80. package/src/cloudflare/static-site.ts +13 -32
  81. package/src/cloudflare/worker.ts +117 -62
  82. package/src/github/client.ts +115 -0
  83. package/src/github/index.ts +1 -0
  84. package/src/github/secret.ts +172 -0
  85. package/src/utils/retry.ts +37 -0
  86. package/README.md +0 -324
package/src/aws/ses.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- AlreadyExistsException,
3
2
  CreateConfigurationSetCommand,
4
3
  CreateEmailIdentityCommand,
5
4
  DeleteConfigurationSetCommand,
@@ -8,6 +7,11 @@ import {
8
7
  GetConfigurationSetCommand,
9
8
  GetEmailIdentityCommand,
10
9
  NotFoundException,
10
+ PutConfigurationSetDeliveryOptionsCommand,
11
+ PutConfigurationSetReputationOptionsCommand,
12
+ PutConfigurationSetSendingOptionsCommand,
13
+ PutConfigurationSetSuppressionOptionsCommand,
14
+ PutConfigurationSetTrackingOptionsCommand,
11
15
  PutEmailIdentityDkimAttributesCommand,
12
16
  type ReputationOptions,
13
17
  SESv2Client,
@@ -103,7 +107,7 @@ export interface SESOutput extends SESProps {
103
107
  */
104
108
  export class SES extends Resource(
105
109
  "aws::SES",
106
- async (ctx: Context<SESOutput>, props: SESProps): Promise<SESOutput> => {
110
+ async (ctx: Context<SESOutput>, props: SESProps) => {
107
111
  // Create SES client
108
112
  const client = new SESv2Client({});
109
113
 
@@ -138,10 +142,7 @@ export class SES extends Resource(
138
142
  }
139
143
 
140
144
  // Return empty output for delete
141
- return {
142
- ...props,
143
- id,
144
- };
145
+ return;
145
146
  }
146
147
 
147
148
  // Created resources
@@ -154,66 +155,68 @@ export class SES extends Resource(
154
155
  if (props.configurationSetName) {
155
156
  try {
156
157
  // Check if configuration set exists
157
- let getConfigResult;
158
+ let configSetExists = false;
158
159
  try {
159
- getConfigResult = await client.send(
160
+ await client.send(
160
161
  new GetConfigurationSetCommand({
161
162
  ConfigurationSetName: props.configurationSetName,
162
163
  }),
163
164
  );
165
+ configSetExists = true;
164
166
  } catch (error) {
165
167
  if (error instanceof NotFoundException) {
166
- getConfigResult = null;
168
+ configSetExists = false;
167
169
  } else {
168
170
  throw error;
169
171
  }
170
172
  }
171
173
 
172
- if (getConfigResult) {
173
- // For updating configuration set, we need to use individual commands
174
- // since there's no unified UpdateConfigurationSetCommand
175
-
174
+ if (configSetExists) {
175
+ // Update existing configuration set using appropriate update commands
176
176
  if (props.sendingOptions) {
177
177
  await client.send(
178
- new CreateConfigurationSetCommand({
178
+ new PutConfigurationSetSendingOptionsCommand({
179
179
  ConfigurationSetName: props.configurationSetName,
180
- SendingOptions: props.sendingOptions,
180
+ SendingEnabled: props.sendingOptions.SendingEnabled,
181
181
  }),
182
182
  );
183
183
  }
184
184
 
185
185
  if (props.reputationOptions) {
186
186
  await client.send(
187
- new CreateConfigurationSetCommand({
187
+ new PutConfigurationSetReputationOptionsCommand({
188
188
  ConfigurationSetName: props.configurationSetName,
189
- ReputationOptions: props.reputationOptions,
189
+ ReputationMetricsEnabled:
190
+ props.reputationOptions.ReputationMetricsEnabled,
190
191
  }),
191
192
  );
192
193
  }
193
194
 
194
195
  if (props.trackingOptions) {
195
196
  await client.send(
196
- new CreateConfigurationSetCommand({
197
+ new PutConfigurationSetTrackingOptionsCommand({
197
198
  ConfigurationSetName: props.configurationSetName,
198
- TrackingOptions: props.trackingOptions,
199
+ CustomRedirectDomain:
200
+ props.trackingOptions.CustomRedirectDomain,
199
201
  }),
200
202
  );
201
203
  }
202
204
 
203
205
  if (props.suppressionOptions) {
204
206
  await client.send(
205
- new CreateConfigurationSetCommand({
207
+ new PutConfigurationSetSuppressionOptionsCommand({
206
208
  ConfigurationSetName: props.configurationSetName,
207
- SuppressionOptions: props.suppressionOptions,
209
+ SuppressedReasons: props.suppressionOptions.SuppressedReasons,
208
210
  }),
209
211
  );
210
212
  }
211
213
 
212
214
  if (props.deliveryOptions) {
213
215
  await client.send(
214
- new CreateConfigurationSetCommand({
216
+ new PutConfigurationSetDeliveryOptionsCommand({
215
217
  ConfigurationSetName: props.configurationSetName,
216
- DeliveryOptions: props.deliveryOptions,
218
+ TlsPolicy: props.deliveryOptions.TlsPolicy,
219
+ SendingPoolName: props.deliveryOptions.SendingPoolName,
217
220
  }),
218
221
  );
219
222
  }
@@ -241,14 +244,7 @@ export class SES extends Resource(
241
244
  configurationSetArn = `arn:aws:ses:${process.env.AWS_REGION}:${process.env.AWS_ACCOUNT_ID}:configuration-set/${props.configurationSetName}`;
242
245
  }
243
246
  } catch (error: any) {
244
- if (error instanceof AlreadyExistsException) {
245
- // If it already exists but we couldn't get it (permissions issue), continue
246
- console.warn(
247
- `Configuration set ${props.configurationSetName} already exists but cannot be retrieved`,
248
- );
249
- } else {
250
- throw error;
251
- }
247
+ throw error;
252
248
  }
253
249
  }
254
250
 
package/src/aws/table.ts CHANGED
@@ -3,12 +3,14 @@ import {
3
3
  DeleteTableCommand,
4
4
  DescribeTableCommand,
5
5
  DynamoDBClient,
6
+ InternalServerError,
6
7
  type KeySchemaElement,
7
8
  ResourceInUseException,
8
9
  ResourceNotFoundException,
9
10
  } from "@aws-sdk/client-dynamodb";
10
11
  import { ignore } from "../error";
11
12
  import { type Context, Resource } from "../resource";
13
+ import { withExponentialBackoff } from "../utils/retry";
12
14
 
13
15
  export interface TableProps {
14
16
  tableName: string;
@@ -38,21 +40,33 @@ export class Table extends Resource(
38
40
  async (
39
41
  ctx: Context<TableOutput>,
40
42
  props: TableProps,
41
- ): Promise<TableOutput> => {
43
+ ): Promise<TableOutput | void> => {
42
44
  const client = new DynamoDBClient({});
43
45
 
44
46
  if (ctx.event === "delete") {
45
- await ignore(ResourceNotFoundException.name, () =>
46
- client.send(
47
- new DeleteTableCommand({
48
- TableName: props.tableName,
49
- }),
50
- ),
47
+ await withExponentialBackoff(
48
+ async () => {
49
+ await ignore(ResourceNotFoundException.name, () =>
50
+ client.send(
51
+ new DeleteTableCommand({
52
+ TableName: props.tableName,
53
+ }),
54
+ ),
55
+ );
56
+ },
57
+ (error) =>
58
+ error instanceof ResourceInUseException ||
59
+ error instanceof InternalServerError,
60
+ 10, // Max attempts
61
+ 200, // Initial delay in ms
51
62
  );
52
63
 
53
64
  // Wait for table to be deleted
54
65
  let tableDeleted = false;
55
- while (!tableDeleted) {
66
+ let retryCount = 0;
67
+ const maxRetries = 60; // Wait up to 60 seconds
68
+
69
+ while (!tableDeleted && retryCount < maxRetries) {
56
70
  try {
57
71
  await client.send(
58
72
  new DescribeTableCommand({
@@ -60,7 +74,10 @@ export class Table extends Resource(
60
74
  }),
61
75
  );
62
76
  // If we get here, table still exists
63
- await new Promise((resolve) => setTimeout(resolve, 1000));
77
+ retryCount++;
78
+ // Increasing delay for each retry with some jitter
79
+ const delay = Math.min(1000 * (1 + 0.1 * Math.random()), 5000);
80
+ await new Promise((resolve) => setTimeout(resolve, delay));
64
81
  } catch (error) {
65
82
  if (error instanceof ResourceNotFoundException) {
66
83
  tableDeleted = true;
@@ -70,103 +87,139 @@ export class Table extends Resource(
70
87
  }
71
88
  }
72
89
 
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
- };
90
+ if (!tableDeleted) {
91
+ throw new Error(
92
+ `Timed out waiting for table ${props.tableName} to be deleted`,
93
+ );
94
+ }
95
+
96
+ return;
79
97
  } 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
- }
98
+ // Setup for table creation
99
+ const attributeDefinitions = [
100
+ {
101
+ AttributeName: props.partitionKey.name,
102
+ AttributeType: props.partitionKey.type,
103
+ },
104
+ ];
105
105
 
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
- );
106
+ const keySchema: KeySchemaElement[] = [
107
+ {
108
+ AttributeName: props.partitionKey.name,
109
+ KeyType: "HASH",
110
+ },
111
+ ];
112
+
113
+ if (props.sortKey) {
114
+ attributeDefinitions.push({
115
+ AttributeName: props.sortKey.name,
116
+ AttributeType: props.sortKey.type,
117
+ });
118
+ keySchema.push({
119
+ AttributeName: props.sortKey.name,
120
+ KeyType: "RANGE",
121
+ });
122
+ }
123
+
124
+ // Attempt to create the table with exponential backoff for ResourceInUseException
125
+ await withExponentialBackoff(
126
+ async () => {
127
+ try {
128
+ // First check if table already exists
129
+ const describeResponse = await client.send(
130
+ new DescribeTableCommand({
131
+ TableName: props.tableName,
132
+ }),
133
+ );
134
+
135
+ // If table exists and is ACTIVE, no need to create it
136
+ if (describeResponse.Table?.TableStatus === "ACTIVE") {
137
+ return;
138
+ }
139
+
140
+ // If table exists but not ACTIVE, wait for it in the polling loop below
141
+ if (describeResponse.Table) {
142
+ return;
143
+ }
144
+ } catch (error) {
145
+ if (error instanceof ResourceNotFoundException) {
146
+ // Table doesn't exist, try to create it
147
+ await client.send(
148
+ new CreateTableCommand({
149
+ TableName: props.tableName,
150
+ AttributeDefinitions: attributeDefinitions,
151
+ KeySchema: keySchema,
152
+ BillingMode: props.billingMode || "PAY_PER_REQUEST",
153
+ ProvisionedThroughput:
154
+ props.billingMode === "PROVISIONED"
155
+ ? {
156
+ ReadCapacityUnits: props.readCapacity || 5,
157
+ WriteCapacityUnits: props.writeCapacity || 5,
158
+ }
159
+ : undefined,
160
+ Tags: props.tags
161
+ ? Object.entries(props.tags).map(([Key, Value]) => ({
162
+ Key,
163
+ Value,
164
+ }))
165
+ : undefined,
166
+ }),
167
+ );
168
+ } else {
169
+ throw error;
170
+ }
171
+ }
172
+ },
173
+ (error) => error instanceof ResourceInUseException,
174
+ 10, // Max attempts
175
+ 200, // Initial delay in ms
176
+ );
177
+
178
+ // Wait for table to be active with timeout
179
+ let tableActive = false;
180
+ let tableDescription;
181
+ let retryCount = 0;
182
+ const maxRetries = 60; // Wait up to 60 seconds
127
183
 
128
- // Wait for table to be active
129
- let tableActive = false;
130
- let tableDescription;
131
- while (!tableActive) {
184
+ while (!tableActive && retryCount < maxRetries) {
185
+ try {
132
186
  const response = await client.send(
133
187
  new DescribeTableCommand({
134
188
  TableName: props.tableName,
135
189
  }),
136
190
  );
191
+
137
192
  tableActive = response.Table?.TableStatus === "ACTIVE";
138
193
  if (tableActive) {
139
194
  tableDescription = response.Table;
140
195
  } else {
141
- await new Promise((resolve) => setTimeout(resolve, 1000));
196
+ retryCount++;
197
+ // Increasing delay for each retry with some jitter
198
+ const delay = Math.min(1000 * (1 + 0.1 * Math.random()), 5000);
199
+ await new Promise((resolve) => setTimeout(resolve, delay));
200
+ }
201
+ } catch (error) {
202
+ retryCount++;
203
+ if (!(error instanceof ResourceNotFoundException)) {
204
+ throw error;
142
205
  }
206
+ await new Promise((resolve) => setTimeout(resolve, 1000));
143
207
  }
208
+ }
144
209
 
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;
210
+ if (!tableActive) {
211
+ throw new Error(
212
+ `Timed out waiting for table ${props.tableName} to become active`,
213
+ );
169
214
  }
215
+
216
+ return {
217
+ ...props,
218
+ id: props.tableName,
219
+ arn: tableDescription!.TableArn!,
220
+ streamArn: tableDescription!.LatestStreamArn,
221
+ tableId: tableDescription!.TableId!,
222
+ };
170
223
  }
171
224
  },
172
225
  ) {}
@@ -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,4 +1,6 @@
1
+ import type { Resolved } from "../output";
1
2
  import { type Context, Resource } from "../resource";
3
+ import { withExponentialBackoff } from "../utils/retry";
2
4
  import { createCloudflareApi } from "./api";
3
5
 
4
6
  /**
@@ -51,6 +53,7 @@ export interface KVPair {
51
53
  * Output returned after KV Namespace creation/update
52
54
  */
53
55
  export interface KVNamespaceOutput extends KVNamespaceProps {
56
+ type: "kv_namespace";
54
57
  /**
55
58
  * The ID of the namespace
56
59
  */
@@ -67,6 +70,14 @@ export interface KVNamespaceOutput extends KVNamespaceProps {
67
70
  modifiedAt: number;
68
71
  }
69
72
 
73
+ export function isKVNamespace(
74
+ resource: any,
75
+ ): resource is Resolved<KVNamespace> {
76
+ return (
77
+ resource && typeof resource === "object" && resource.type === "kv_namespace"
78
+ );
79
+ }
80
+
70
81
  export class KVNamespace extends Resource(
71
82
  "cloudflare::KVNamespace",
72
83
  async (ctx: Context<KVNamespaceOutput>, props: KVNamespaceProps) => {
@@ -76,7 +87,6 @@ export class KVNamespace extends Resource(
76
87
  if (ctx.event === "delete") {
77
88
  // For delete operations, we need to check if the namespace ID exists in the output
78
89
  const namespaceId = ctx.output?.id;
79
-
80
90
  if (namespaceId) {
81
91
  // Delete KV namespace
82
92
  const deleteResponse = await api.delete(
@@ -87,9 +97,8 @@ export class KVNamespace extends Resource(
87
97
  const errorData: any = await deleteResponse.json().catch(() => ({
88
98
  errors: [{ message: deleteResponse.statusText }],
89
99
  }));
90
- console.error(
91
- "Error deleting KV namespace:",
92
- errorData.errors?.[0]?.message || deleteResponse.statusText,
100
+ throw new Error(
101
+ `Error deleting KV namespace '${props.title}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
93
102
  );
94
103
  }
95
104
  }
@@ -162,18 +171,38 @@ export class KVNamespace extends Resource(
162
171
  return item;
163
172
  });
164
173
 
165
- const bulkResponse = await api.put(
166
- `/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/bulk`,
167
- bulkPayload,
168
- );
169
-
170
- if (!bulkResponse.ok) {
171
- const errorData: any = await bulkResponse.json().catch(() => ({
172
- errors: [{ message: bulkResponse.statusText }],
173
- }));
174
- console.warn(
175
- `Error writing KV batch: ${errorData.errors?.[0]?.message || bulkResponse.statusText}`,
174
+ try {
175
+ await withExponentialBackoff(
176
+ async () => {
177
+ const bulkResponse = await api.put(
178
+ `/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/bulk`,
179
+ bulkPayload,
180
+ );
181
+
182
+ if (!bulkResponse.ok) {
183
+ const errorData: any = await bulkResponse
184
+ .json()
185
+ .catch(() => ({
186
+ errors: [{ message: bulkResponse.statusText }],
187
+ }));
188
+ const errorMessage =
189
+ errorData.errors?.[0]?.message || bulkResponse.statusText;
190
+
191
+ // Throw error to trigger retry
192
+ throw new Error(`Error writing KV batch: ${errorMessage}`);
193
+ }
194
+
195
+ return bulkResponse;
196
+ },
197
+ (error) => {
198
+ // Retry on "namespace not found" errors as they're likely propagation delays
199
+ return error.message?.includes("not found");
200
+ },
201
+ 5, // 5 retry attempts
202
+ 1000, // Start with 1 second delay
176
203
  );
204
+ } catch (error: any) {
205
+ console.warn(error.message);
177
206
  }
178
207
  }
179
208
  }
@@ -182,6 +211,7 @@ export class KVNamespace extends Resource(
182
211
 
183
212
  // Construct the output
184
213
  const output: KVNamespaceOutput = {
214
+ type: "kv_namespace",
185
215
  id: namespaceId,
186
216
  title: props.title,
187
217
  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