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
@@ -1,11 +1,14 @@
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";
6
+ import { withExponentialBackoff } from "../utils/retry";
5
7
  import { type CloudflareApi, createCloudflareApi } from "./api";
6
- import type { WorkerBinding, WorkerBindingSpec } from "./bindings";
8
+ import type { Bindings, WorkerBindingSpec } from "./bindings";
7
9
  import type { DurableObjectNamespace } from "./durable-object-namespace";
8
10
  import { isDurableObjectNamespace } from "./durable-object-namespace";
11
+ import { isKVNamespace } from "./kv-namespace";
9
12
  import type { WorkerScriptMetadata } from "./worker-metadata";
10
13
  import type { SingleStepMigration } from "./worker-migration";
11
14
  /**
@@ -54,7 +57,7 @@ export interface WorkerProps {
54
57
  /**
55
58
  * Bindings to attach to the worker
56
59
  */
57
- bindings?: WorkerBinding[];
60
+ bindings?: Bindings;
58
61
 
59
62
  /**
60
63
  * Environment variables to attach to the worker
@@ -94,6 +97,8 @@ export interface WorkerProps {
94
97
  * Output returned after Worker creation/update
95
98
  */
96
99
  export interface WorkerOutput extends Omit<WorkerProps, "url"> {
100
+ type: "service";
101
+
97
102
  /**
98
103
  * The ID of the worker
99
104
  */
@@ -118,7 +123,11 @@ export interface WorkerOutput extends Omit<WorkerProps, "url"> {
118
123
  /**
119
124
  * The bindings that were created
120
125
  */
121
- bindings: WorkerBinding[];
126
+ bindings: Bindings;
127
+ }
128
+
129
+ export function isWorker(resource: any): resource is Resolved<Worker> {
130
+ return resource && typeof resource === "object" && resource.type === "worker";
122
131
  }
123
132
 
124
133
  export class Worker extends Resource(
@@ -144,7 +153,7 @@ export class Worker extends Resource(
144
153
  await assertWorkerDoesNotExist(ctx, api, workerName);
145
154
  }
146
155
 
147
- const oldBindings = await ctx.get<WorkerBinding[]>("bindings");
156
+ const oldBindings = await ctx.get<Bindings>("bindings");
148
157
 
149
158
  const scriptMetadata = await prepareWorkerMetadata(ctx, oldBindings, props);
150
159
 
@@ -174,12 +183,13 @@ export class Worker extends Resource(
174
183
 
175
184
  // Construct the output
176
185
  const output: WorkerOutput = {
186
+ type: "service",
177
187
  id: workerName,
178
188
  name: workerName,
179
189
  script: scriptContent,
180
190
  format: props.format || "esm", // Include format in the output
181
191
  routes: props.routes || [],
182
- bindings: props.bindings || [],
192
+ bindings: props.bindings ?? {},
183
193
  env: props.env,
184
194
  observability: scriptMetadata.observability,
185
195
  createdAt: now,
@@ -267,58 +277,82 @@ async function putWorker(
267
277
  scriptContent: string,
268
278
  scriptMetadata: WorkerMetadata,
269
279
  ) {
270
- const scriptName = scriptMetadata.main_module ?? scriptMetadata.body_part!;
271
-
272
- // Create FormData for the upload
273
- const formData = new FormData();
274
-
275
- // Add the actual script content as a named file part
276
- formData.append(
277
- scriptName,
278
- new Blob([scriptContent], {
279
- type: scriptMetadata.main_module
280
- ? "application/javascript+module"
281
- : "application/javascript",
282
- }),
283
- scriptName,
284
- );
280
+ return withExponentialBackoff(
281
+ async () => {
282
+ const scriptName =
283
+ scriptMetadata.main_module ?? scriptMetadata.body_part!;
284
+
285
+ // Create FormData for the upload
286
+ const formData = new FormData();
287
+
288
+ // Add the actual script content as a named file part
289
+ formData.append(
290
+ scriptName,
291
+ new Blob([scriptContent], {
292
+ type: scriptMetadata.main_module
293
+ ? "application/javascript+module"
294
+ : "application/javascript",
295
+ }),
296
+ scriptName,
297
+ );
285
298
 
286
- console.log("Script Metadata:", JSON.stringify(scriptMetadata, null, 2));
299
+ // console.log("Script Metadata:", JSON.stringify(scriptMetadata, null, 2));
287
300
 
288
- // Add metadata as JSON
289
- formData.append(
290
- "metadata",
291
- new Blob([JSON.stringify(scriptMetadata)], {
292
- type: "application/json",
293
- }),
294
- );
301
+ // Add metadata as JSON
302
+ formData.append(
303
+ "metadata",
304
+ new Blob([JSON.stringify(scriptMetadata)], {
305
+ type: "application/json",
306
+ }),
307
+ );
295
308
 
296
- // Upload worker script with bindings
297
- const uploadResponse = await api.put(
298
- `/accounts/${api.accountId}/workers/scripts/${workerName}`,
299
- formData,
300
- {
301
- headers: {
302
- "Content-Type": "multipart/form-data",
303
- },
309
+ await new Promise((resolve) => setTimeout(resolve, 1000));
310
+
311
+ // Upload worker script with bindings
312
+ const uploadResponse = await api.put(
313
+ `/accounts/${api.accountId}/workers/scripts/${workerName}`,
314
+ formData,
315
+ {
316
+ headers: {
317
+ "Content-Type": "multipart/form-data",
318
+ },
319
+ },
320
+ );
321
+
322
+ // Check if the upload was successful
323
+ if (!uploadResponse.ok) {
324
+ const errorData: any = await uploadResponse
325
+ .json()
326
+ .catch(() => ({ errors: [{ message: uploadResponse.statusText }] }));
327
+
328
+ const errorMessage = `Error (HTTP ${uploadResponse.status}) uploading worker script '${workerName}': ${errorData.errors?.[0]?.message || uploadResponse.statusText}`;
329
+
330
+ if (
331
+ uploadResponse.status === 400 &&
332
+ errorMessage.includes("not found")
333
+ ) {
334
+ throw new NotFoundError(errorMessage);
335
+ }
336
+ throw new Error(errorMessage);
337
+ }
338
+
339
+ return formData;
304
340
  },
341
+ (err) => err instanceof NotFoundError,
342
+ 10,
343
+ 100,
305
344
  );
345
+ }
306
346
 
307
- // Check if the upload was successful
308
- if (!uploadResponse.ok) {
309
- const errorData: any = await uploadResponse
310
- .json()
311
- .catch(() => ({ errors: [{ message: uploadResponse.statusText }] }));
312
- throw new Error(
313
- `Error uploading worker script '${workerName}': ${errorData.errors?.[0]?.message || uploadResponse.statusText}`,
314
- );
347
+ class NotFoundError extends Error {
348
+ constructor(message: string) {
349
+ super(message);
350
+ this.name = "NotFoundError";
315
351
  }
316
-
317
- return formData;
318
352
  }
319
353
 
320
354
  interface WorkerMetadata {
321
- bindings: WorkerBinding[];
355
+ bindings: WorkerBindingSpec[];
322
356
  observability: {
323
357
  enabled: boolean;
324
358
  };
@@ -330,12 +364,12 @@ interface WorkerMetadata {
330
364
 
331
365
  async function prepareWorkerMetadata(
332
366
  ctx: Context<WorkerOutput>,
333
- oldBindings: WorkerBinding[] | undefined,
367
+ oldBindings: Bindings | undefined,
334
368
  props: WorkerProps,
335
369
  ): Promise<WorkerMetadata> {
336
370
  // Prepare metadata with bindings
337
371
  const meta: WorkerMetadata = {
338
- bindings: [] as WorkerBinding[],
372
+ bindings: [],
339
373
  observability: {
340
374
  enabled: props.observability?.enabled !== false,
341
375
  },
@@ -349,30 +383,51 @@ async function prepareWorkerMetadata(
349
383
  },
350
384
  };
351
385
 
386
+ const bindings = (props.bindings ?? {}) as Resolved<Bindings>;
387
+
352
388
  // Convert bindings to the format expected by the API
353
- for (const binding of props.bindings ?? []) {
389
+ for (const [bindingName, binding] of Object.entries(bindings)) {
354
390
  // Create a copy of the binding to avoid modifying the original
355
- const bindingData = { ...binding };
356
391
 
357
- if (isDurableObjectNamespace(bindingData)) {
358
- const stableId = bindingData.id;
359
- const className = bindingData.className;
392
+ if (isKVNamespace(binding)) {
393
+ meta.bindings.push({
394
+ type: "kv_namespace",
395
+ name: bindingName,
396
+ namespace_id: binding.id,
397
+ });
398
+ } else if (typeof binding === "string") {
399
+ meta.bindings.push({
400
+ type: "plain_text",
401
+ name: bindingName,
402
+ text: binding,
403
+ });
404
+ } else if (binding.type === "service") {
405
+ meta.bindings.push({
406
+ type: "service",
407
+ name: bindingName,
408
+ service: binding.id,
409
+ });
410
+ } else if (isDurableObjectNamespace(binding)) {
411
+ const stableId = binding.id;
412
+ const className = binding.className;
360
413
 
361
414
  meta.bindings.push({
362
415
  type: "durable_object_namespace",
363
- name: bindingData.bindingName,
416
+ name: bindingName,
364
417
  class_name: className,
365
- script_name: bindingData.scriptName,
366
- environment: bindingData.environment,
367
- namespace_id: bindingData.namespaceId,
418
+ script_name: binding.scriptName,
419
+ environment: binding.environment,
420
+ namespace_id: binding.namespaceId,
368
421
  });
369
422
 
370
- const oldBinding: DurableObjectNamespace | undefined = oldBindings
423
+ const oldBinding: DurableObjectNamespace | undefined = Object.values(
424
+ oldBindings ?? {},
425
+ )
371
426
  ?.filter(isDurableObjectNamespace)
372
427
  ?.find((b) => b.id === stableId);
373
428
 
374
429
  if (!oldBinding) {
375
- if (bindingData.sqlite) {
430
+ if (binding.sqlite) {
376
431
  meta.migrations!.new_sqlite_classes!.push(className);
377
432
  } else {
378
433
  meta.migrations!.new_classes!.push(className);
@@ -384,8 +439,8 @@ async function prepareWorkerMetadata(
384
439
  });
385
440
  }
386
441
  } else {
387
- // For all other binding types, use as-is
388
- meta.bindings.push(bindingData as WorkerBindingSpec);
442
+ // @ts-expect-error - we should never reach here
443
+ throw new Error(`Unsupported binding type: ${binding.type}`);
389
444
  }
390
445
  }
391
446
 
@@ -0,0 +1,115 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import { exec } from "child_process";
3
+ import { promisify } from "util";
4
+
5
+ // Convert exec to promise-based
6
+ const execAsync = promisify(exec);
7
+
8
+ /**
9
+ * Try to get a GitHub token from the GitHub CLI
10
+ */
11
+ export async function getGitHubTokenFromCLI(): Promise<string | null> {
12
+ try {
13
+ // Check if GitHub CLI is installed
14
+ const { stdout: ghVersion } = await execAsync("gh --version");
15
+ if (!ghVersion) return null;
16
+
17
+ // Get the auth token
18
+ const { stdout: token } = await execAsync("gh auth token");
19
+ return token?.trim() || null;
20
+ } catch (error) {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Get GitHub authentication token with the following priority:
27
+ * 1. Explicit token provided in props
28
+ * 2. GITHUB_ACCESS_TOKEN environment variable (for actions with admin permissions)
29
+ * 3. GITHUB_TOKEN environment variable
30
+ * 4. GitHub CLI token (if gh is installed and authenticated)
31
+ *
32
+ * @param token Optional token to use
33
+ * @returns The resolved token or null if not available
34
+ */
35
+ export async function getGitHubToken(token?: string): Promise<string | null> {
36
+ return (
37
+ token ||
38
+ process.env.GITHUB_ACCESS_TOKEN ||
39
+ process.env.GITHUB_TOKEN ||
40
+ (await getGitHubTokenFromCLI())
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Create an authenticated Octokit client
46
+ *
47
+ * @param options Options for creating the client
48
+ * @returns An authenticated Octokit client
49
+ */
50
+ export async function createGitHubClient(
51
+ options: { token?: string } = {},
52
+ ): Promise<Octokit> {
53
+ const token = await getGitHubToken(options.token);
54
+
55
+ return new Octokit({
56
+ auth: token,
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Verifies GitHub authentication and provides helpful error messages
62
+ *
63
+ * @param octokit Octokit client
64
+ * @param owner Repository owner
65
+ * @param repo Repository name
66
+ */
67
+ export async function verifyGitHubAuth(
68
+ octokit: Octokit,
69
+ owner: string,
70
+ repo: string,
71
+ ): Promise<void> {
72
+ try {
73
+ // Make a test request to check authentication
74
+ await octokit.rest.repos.get({
75
+ owner,
76
+ repo,
77
+ });
78
+ } catch (error: any) {
79
+ if (error.status === 401) {
80
+ console.error(
81
+ "\n⚠️ GitHub authentication failed. Please try one of the following:",
82
+ );
83
+ console.error(
84
+ "1. Run 'gh auth login' to authenticate with the GitHub CLI",
85
+ );
86
+ console.error(
87
+ "2. Set the GITHUB_TOKEN environment variable with a personal access token",
88
+ );
89
+ console.error("3. Pass a token directly to the constructor");
90
+ console.error(
91
+ "\nTo create a token, visit: https://github.com/settings/tokens",
92
+ );
93
+ console.error(
94
+ "Required scopes: 'repo' for private repos or 'public_repo' for public repos\n",
95
+ );
96
+ throw new Error("GitHub authentication failed");
97
+ } else if (error.status === 403) {
98
+ console.error(
99
+ "\n⚠️ Insufficient permissions. You need admin access to the repository.",
100
+ );
101
+ console.error(
102
+ "Make sure your token has the 'repo' scope for private repos or 'public_repo' for public repos\n",
103
+ );
104
+ throw new Error("Insufficient GitHub permissions");
105
+ } else if (error.status === 404) {
106
+ console.error(`\n⚠️ Repository not found: ${owner}/${repo}`);
107
+ console.error(
108
+ "Make sure the repository exists and you have access to it\n",
109
+ );
110
+ throw new Error("GitHub repository not found");
111
+ } else {
112
+ throw error;
113
+ }
114
+ }
115
+ }
@@ -0,0 +1 @@
1
+ export * from "./secret";
@@ -0,0 +1,172 @@
1
+ import sodium from "libsodium-wrappers";
2
+ import { type Context, Resource } from "../resource";
3
+ import { createGitHubClient, verifyGitHubAuth } from "./client";
4
+
5
+ /**
6
+ * Properties for creating or updating a GitHub Secret
7
+ */
8
+ export interface GitHubSecretProps {
9
+ /**
10
+ * Repository owner (user or organization)
11
+ */
12
+ owner: string;
13
+
14
+ /**
15
+ * Repository name
16
+ */
17
+ repository: string;
18
+
19
+ /**
20
+ * Secret name
21
+ */
22
+ name: string;
23
+
24
+ /**
25
+ * Secret value (will be stored securely on GitHub)
26
+ */
27
+ value: string;
28
+
29
+ /**
30
+ * Optional GitHub API token (overrides environment variable)
31
+ */
32
+ token?: string;
33
+ }
34
+
35
+ /**
36
+ * Output returned after Secret creation/update
37
+ */
38
+ export interface GitHubSecretOutput extends Omit<GitHubSecretProps, "value"> {
39
+ /**
40
+ * The ID of the resource
41
+ */
42
+ id: string;
43
+
44
+ /**
45
+ * Time at which the object was created/updated
46
+ */
47
+ updatedAt: string;
48
+ }
49
+
50
+ /**
51
+ * Encrypt a value for GitHub using libsodium
52
+ * Based on GitHub's documentation for encrypting secrets
53
+ *
54
+ * @param value - The secret value to encrypt
55
+ * @param publicKey - The base64-encoded public key from GitHub
56
+ * @returns The base64-encoded encrypted value
57
+ */
58
+ async function encryptValue(value: string, publicKey: string): Promise<string> {
59
+ // Initialize libsodium
60
+ await sodium.ready;
61
+
62
+ // Convert the public key from base64 to binary
63
+ const binKey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
64
+
65
+ // Convert the message to a Uint8Array
66
+ const binMessage = sodium.from_string(value);
67
+
68
+ // Encrypt the message with the public key using libsodium's sealed box
69
+ const encryptedBin = sodium.crypto_box_seal(binMessage, binKey);
70
+
71
+ // Convert the encrypted message to base64
72
+ return sodium.to_base64(encryptedBin, sodium.base64_variants.ORIGINAL);
73
+ }
74
+
75
+ /**
76
+ * Resource for managing GitHub repository secrets
77
+ */
78
+ export class GitHubSecret extends Resource(
79
+ "github::Secret",
80
+ async (
81
+ ctx: Context<GitHubSecretOutput>,
82
+ props: GitHubSecretProps,
83
+ ): Promise<GitHubSecretOutput | void> => {
84
+ // Create authenticated Octokit client - will automatically handle token resolution
85
+ const octokit = await createGitHubClient({ token: props.token });
86
+
87
+ // Verify authentication and permissions
88
+ if (!ctx.quiet) {
89
+ await verifyGitHubAuth(octokit, props.owner, props.repository);
90
+ }
91
+
92
+ if (ctx.event === "delete") {
93
+ if (ctx.output?.id) {
94
+ try {
95
+ // Delete the secret
96
+ await octokit.rest.actions.deleteRepoSecret({
97
+ owner: props.owner,
98
+ repo: props.repository,
99
+ secret_name: props.name,
100
+ });
101
+ } catch (error: any) {
102
+ // Ignore 404 errors (secret already deleted)
103
+ if (error.status === 404) {
104
+ console.log("Secret doesn't exist, ignoring");
105
+ } else {
106
+ throw error;
107
+ }
108
+ }
109
+ }
110
+
111
+ // Return void (a deleted resource has no content)
112
+ return undefined;
113
+ } else {
114
+ try {
115
+ // Get the repository's public key for encrypting secrets
116
+ const { data: publicKey } = await octokit.rest.actions.getRepoPublicKey(
117
+ {
118
+ owner: props.owner,
119
+ repo: props.repository,
120
+ },
121
+ );
122
+
123
+ // Encrypt the secret value using libsodium
124
+ const encryptedValue = await encryptValue(props.value, publicKey.key);
125
+
126
+ // Create or update the secret with the encrypted value and key_id
127
+ const response = await octokit.rest.actions.createOrUpdateRepoSecret({
128
+ owner: props.owner,
129
+ repo: props.repository,
130
+ secret_name: props.name,
131
+ encrypted_value: encryptedValue,
132
+ key_id: publicKey.key_id,
133
+ });
134
+
135
+ // Log success
136
+ if (!ctx.quiet) {
137
+ console.log(`Successfully created/updated secret: ${props.name}`);
138
+ }
139
+
140
+ // GitHub doesn't return the secret details on create/update, so we need to construct it
141
+ const output: GitHubSecretOutput = {
142
+ id: `${props.owner}/${props.repository}/${props.name}`,
143
+ owner: props.owner,
144
+ repository: props.repository,
145
+ name: props.name,
146
+ token: props.token,
147
+ updatedAt: new Date().toISOString(),
148
+ };
149
+
150
+ return output;
151
+ } catch (error: any) {
152
+ if (
153
+ error.status === 403 &&
154
+ error.message?.includes("Must have admin rights")
155
+ ) {
156
+ console.error(
157
+ "\n⚠️ Error creating/updating GitHub secret: You must have admin rights to the repository.",
158
+ );
159
+ console.error(
160
+ "Make sure your GitHub token has the required permissions (repo scope for private repos).\n",
161
+ );
162
+ } else {
163
+ console.error(
164
+ "Error creating/updating GitHub secret:",
165
+ error.message,
166
+ );
167
+ }
168
+ throw error;
169
+ }
170
+ }
171
+ },
172
+ ) {}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Utility function for exponential backoff retry
3
+ * Retries an operation with exponential backoff when a retryable error occurs
4
+ *
5
+ * @param operation The async operation to execute and potentially retry
6
+ * @param isRetryable Function to determine if an error should trigger a retry
7
+ * @param maxAttempts Maximum number of attempts before giving up
8
+ * @param initialDelayMs Initial delay in milliseconds before first retry
9
+ * @returns Result of the operation
10
+ * @throws The last error encountered if all retries fail
11
+ */
12
+ export async function withExponentialBackoff<T>(
13
+ operation: () => Promise<T>,
14
+ isRetryable: (error: any) => boolean,
15
+ maxAttempts = 5,
16
+ initialDelayMs = 100,
17
+ ): Promise<T> {
18
+ let attempt = 0;
19
+ let delay = initialDelayMs;
20
+
21
+ while (true) {
22
+ try {
23
+ return await operation();
24
+ } catch (error) {
25
+ attempt++;
26
+ if (attempt >= maxAttempts || !isRetryable(error)) {
27
+ console.warn("Failed after ", attempt, " attempts");
28
+ throw error;
29
+ }
30
+
31
+ // Exponential backoff with jitter
32
+ const jitter = Math.random() * 0.1 * delay;
33
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
34
+ delay *= 2; // Double the delay for next attempt
35
+ }
36
+ }
37
+ }