alchemy 0.9.1 → 0.10.0

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.
@@ -1,14 +1,19 @@
1
1
  import {
2
+ AddPermissionCommand,
2
3
  Architecture,
3
4
  CreateFunctionCommand,
5
+ CreateFunctionUrlConfigCommand,
4
6
  DeleteFunctionCommand,
7
+ DeleteFunctionUrlConfigCommand,
5
8
  GetFunctionCommand,
6
9
  GetFunctionConfigurationCommand,
10
+ GetFunctionUrlConfigCommand,
7
11
  LambdaClient,
8
12
  ResourceNotFoundException,
9
13
  Runtime,
10
14
  UpdateFunctionCodeCommand,
11
15
  UpdateFunctionConfigurationCommand,
16
+ UpdateFunctionUrlConfigCommand,
12
17
  } from "@aws-sdk/client-lambda";
13
18
  import fs from "node:fs";
14
19
  import path from "node:path";
@@ -229,6 +234,11 @@ export interface Function extends Resource<"lambda::Function">, FunctionProps {
229
234
  * ARN of the signing job
230
235
  */
231
236
  signingJobArn?: string;
237
+
238
+ /**
239
+ * Function URL if configured
240
+ */
241
+ functionUrl?: string;
232
242
  }
233
243
 
234
244
  /**
@@ -294,22 +304,38 @@ export const Function = Resource(
294
304
  const code = await zipCode(props.zipPath);
295
305
 
296
306
  if (this.phase === "delete") {
307
+ // Delete function URL if it exists
308
+ if (this.output?.url) {
309
+ try {
310
+ await client.send(
311
+ new DeleteFunctionUrlConfigCommand({
312
+ FunctionName: props.functionName,
313
+ })
314
+ );
315
+ } catch (error: any) {
316
+ if (error.name !== "ResourceNotFoundException") {
317
+ console.warn("Failed to delete function URL:", error);
318
+ }
319
+ }
320
+ }
321
+
297
322
  await ignore(ResourceNotFoundException.name, () =>
298
323
  client.send(
299
324
  new DeleteFunctionCommand({
300
325
  FunctionName: props.functionName,
301
- }),
302
- ),
326
+ })
327
+ )
303
328
  );
304
329
 
305
330
  return this.destroy();
306
331
  } else {
332
+ let functionUrl: string | undefined;
307
333
  try {
308
334
  // Check if function exists
309
335
  await client.send(
310
336
  new GetFunctionCommand({
311
337
  FunctionName: props.functionName,
312
- }),
338
+ })
313
339
  );
314
340
 
315
341
  if (this.phase === "update") {
@@ -321,7 +347,7 @@ export const Function = Resource(
321
347
  new UpdateFunctionCodeCommand({
322
348
  FunctionName: props.functionName,
323
349
  ZipFile: code,
324
- }),
350
+ })
325
351
  );
326
352
 
327
353
  // Wait for code update to stabilize
@@ -340,11 +366,157 @@ export const Function = Resource(
340
366
  Environment: props.environment
341
367
  ? { Variables: props.environment }
342
368
  : undefined,
343
- }),
369
+ })
344
370
  );
345
371
 
346
372
  // Wait for configuration update to stabilize
347
373
  await waitForFunctionStabilization(client, props.functionName);
374
+
375
+ // Handle URL configuration
376
+ if (props.url) {
377
+ try {
378
+ // Check if URL config exists already
379
+ const urlConfig = await client.send(
380
+ new GetFunctionUrlConfigCommand({
381
+ FunctionName: props.functionName,
382
+ })
383
+ );
384
+
385
+ // Update URL configuration if it exists
386
+ if (urlConfig) {
387
+ const updateResult = await client.send(
388
+ new UpdateFunctionUrlConfigCommand({
389
+ FunctionName: props.functionName,
390
+ AuthType: props.url.authType || "NONE",
391
+ Cors: props.url.cors
392
+ ? {
393
+ AllowCredentials: props.url.cors.allowCredentials,
394
+ AllowHeaders: props.url.cors.allowHeaders,
395
+ AllowMethods: props.url.cors.allowMethods,
396
+ AllowOrigins: props.url.cors.allowOrigins,
397
+ ExposeHeaders: props.url.cors.exposeHeaders,
398
+ MaxAge: props.url.cors.maxAge,
399
+ }
400
+ : undefined,
401
+ })
402
+ );
403
+ functionUrl = updateResult.FunctionUrl;
404
+
405
+ // Add public access permission for function URL
406
+ if (props.url.authType === "NONE") {
407
+ try {
408
+ await client.send(
409
+ new AddPermissionCommand({
410
+ FunctionName: props.functionName,
411
+ StatementId: "FunctionURLAllowPublicAccess",
412
+ Action: "lambda:InvokeFunctionUrl",
413
+ Principal: "*",
414
+ FunctionUrlAuthType: "NONE",
415
+ })
416
+ );
417
+ } catch (permError: any) {
418
+ if (!permError.message?.includes("already exists")) {
419
+ console.warn("Error adding URL permission:", permError);
420
+ }
421
+ }
422
+ }
423
+ } else {
424
+ // Create URL configuration if it doesn't exist
425
+ const createResult = await client.send(
426
+ new CreateFunctionUrlConfigCommand({
427
+ FunctionName: props.functionName,
428
+ AuthType: props.url.authType || "NONE",
429
+ Cors: props.url.cors
430
+ ? {
431
+ AllowCredentials: props.url.cors.allowCredentials,
432
+ AllowHeaders: props.url.cors.allowHeaders,
433
+ AllowMethods: props.url.cors.allowMethods,
434
+ AllowOrigins: props.url.cors.allowOrigins,
435
+ ExposeHeaders: props.url.cors.exposeHeaders,
436
+ MaxAge: props.url.cors.maxAge,
437
+ }
438
+ : undefined,
439
+ })
440
+ );
441
+ functionUrl = createResult.FunctionUrl;
442
+
443
+ // Add public access permission for function URL
444
+ if (props.url.authType === "NONE") {
445
+ try {
446
+ await client.send(
447
+ new AddPermissionCommand({
448
+ FunctionName: props.functionName,
449
+ StatementId: "FunctionURLAllowPublicAccess",
450
+ Action: "lambda:InvokeFunctionUrl",
451
+ Principal: "*",
452
+ FunctionUrlAuthType: "NONE",
453
+ })
454
+ );
455
+ } catch (permError: any) {
456
+ if (!permError.message?.includes("already exists")) {
457
+ console.warn("Error adding URL permission:", permError);
458
+ }
459
+ }
460
+ }
461
+ }
462
+ } catch (error: any) {
463
+ if (error.name === "ResourceNotFoundException") {
464
+ // Create URL configuration if it doesn't exist
465
+ const createResult = await client.send(
466
+ new CreateFunctionUrlConfigCommand({
467
+ FunctionName: props.functionName,
468
+ AuthType: props.url.authType || "NONE",
469
+ Cors: props.url.cors
470
+ ? {
471
+ AllowCredentials: props.url.cors.allowCredentials,
472
+ AllowHeaders: props.url.cors.allowHeaders,
473
+ AllowMethods: props.url.cors.allowMethods,
474
+ AllowOrigins: props.url.cors.allowOrigins,
475
+ ExposeHeaders: props.url.cors.exposeHeaders,
476
+ MaxAge: props.url.cors.maxAge,
477
+ }
478
+ : undefined,
479
+ })
480
+ );
481
+ functionUrl = createResult.FunctionUrl;
482
+
483
+ // Add public access permission for function URL
484
+ if (props.url.authType === "NONE") {
485
+ try {
486
+ await client.send(
487
+ new AddPermissionCommand({
488
+ FunctionName: props.functionName,
489
+ StatementId: "FunctionURLAllowPublicAccess",
490
+ Action: "lambda:InvokeFunctionUrl",
491
+ Principal: "*",
492
+ FunctionUrlAuthType: "NONE",
493
+ })
494
+ );
495
+ } catch (permError: any) {
496
+ if (!permError.message?.includes("already exists")) {
497
+ console.warn("Error adding URL permission:", permError);
498
+ }
499
+ }
500
+ }
501
+ } else {
502
+ throw error;
503
+ }
504
+ }
505
+ } else if (this.output?.url) {
506
+ // Remove URL config if it was previously set but not in current props
507
+ try {
508
+ await client.send(
509
+ new DeleteFunctionUrlConfigCommand({
510
+ FunctionName: props.functionName,
511
+ })
512
+ );
513
+ functionUrl = undefined;
514
+ } catch (error: any) {
515
+ if (error.name !== "ResourceNotFoundException") {
516
+ console.warn("Failed to delete function URL:", error);
517
+ }
518
+ }
519
+ }
348
520
  }
349
521
  } catch (error: any) {
350
522
  if (error.name === "ResourceNotFoundException") {
@@ -371,7 +543,7 @@ export const Function = Resource(
371
543
  ? [props.architecture]
372
544
  : [Architecture.x86_64],
373
545
  Tags: props.tags,
374
- }),
546
+ })
375
547
  );
376
548
  break; // Success - exit retry loop
377
549
  } catch (createError: any) {
@@ -384,7 +556,7 @@ export const Function = Resource(
384
556
 
385
557
  if (Date.now() - startTime > 10000) {
386
558
  throw new Error(
387
- "Timeout waiting for IAM role to be assumable by Lambda after 10s",
559
+ "Timeout waiting for IAM role to be assumable by Lambda after 10s"
388
560
  );
389
561
  }
390
562
 
@@ -399,13 +571,57 @@ export const Function = Resource(
399
571
  const config = await client.send(
400
572
  new GetFunctionConfigurationCommand({
401
573
  FunctionName: props.functionName,
402
- }),
574
+ })
403
575
  );
404
576
  isCreating = config.State === "Pending";
405
577
  if (isCreating) {
406
578
  await new Promise((resolve) => setTimeout(resolve, 1000));
407
579
  }
408
580
  }
581
+
582
+ // Create URL configuration if needed
583
+ if (props.url) {
584
+ try {
585
+ const createResult = await client.send(
586
+ new CreateFunctionUrlConfigCommand({
587
+ FunctionName: props.functionName,
588
+ AuthType: props.url.authType || "NONE",
589
+ Cors: props.url.cors
590
+ ? {
591
+ AllowCredentials: props.url.cors.allowCredentials,
592
+ AllowHeaders: props.url.cors.allowHeaders,
593
+ AllowMethods: props.url.cors.allowMethods,
594
+ AllowOrigins: props.url.cors.allowOrigins,
595
+ ExposeHeaders: props.url.cors.exposeHeaders,
596
+ MaxAge: props.url.cors.maxAge,
597
+ }
598
+ : undefined,
599
+ })
600
+ );
601
+ functionUrl = createResult.FunctionUrl;
602
+
603
+ // Add public access permission for function URL
604
+ if (props.url.authType === "NONE") {
605
+ try {
606
+ await client.send(
607
+ new AddPermissionCommand({
608
+ FunctionName: props.functionName,
609
+ StatementId: "FunctionURLAllowPublicAccess",
610
+ Action: "lambda:InvokeFunctionUrl",
611
+ Principal: "*",
612
+ FunctionUrlAuthType: "NONE",
613
+ })
614
+ );
615
+ } catch (permError: any) {
616
+ if (!permError.message?.includes("already exists")) {
617
+ console.warn("Error adding URL permission:", permError);
618
+ }
619
+ }
620
+ }
621
+ } catch (error) {
622
+ console.warn("Failed to create function URL:", error);
623
+ }
624
+ }
409
625
  } else {
410
626
  throw error;
411
627
  }
@@ -416,15 +632,31 @@ export const Function = Resource(
416
632
  client.send(
417
633
  new GetFunctionCommand({
418
634
  FunctionName: props.functionName,
419
- }),
635
+ })
420
636
  ),
421
637
  client.send(
422
638
  new GetFunctionConfigurationCommand({
423
639
  FunctionName: props.functionName,
424
- }),
640
+ })
425
641
  ),
426
642
  ]);
427
643
 
644
+ // Try to get function URL if it wasn't already retrieved and URL is configured
645
+ if (!functionUrl && (props.url || this.output?.url)) {
646
+ try {
647
+ const urlConfig = await client.send(
648
+ new GetFunctionUrlConfigCommand({
649
+ FunctionName: props.functionName,
650
+ })
651
+ );
652
+ functionUrl = urlConfig.FunctionUrl;
653
+ } catch (error: any) {
654
+ if (error.name !== "ResourceNotFoundException") {
655
+ console.warn("Failed to get function URL:", error);
656
+ }
657
+ }
658
+ }
659
+
428
660
  return this({
429
661
  ...props,
430
662
  arn: config.FunctionArn!,
@@ -447,21 +679,22 @@ export const Function = Resource(
447
679
  packageType: config.PackageType!,
448
680
  signingProfileVersionArn: config.SigningProfileVersionArn,
449
681
  signingJobArn: config.SigningJobArn,
682
+ functionUrl: functionUrl,
450
683
  });
451
684
  }
452
- },
685
+ }
453
686
  );
454
687
 
455
688
  // Helper to wait for function to stabilize
456
689
  async function waitForFunctionStabilization(
457
690
  client: LambdaClient,
458
- functionName: string,
691
+ functionName: string
459
692
  ) {
460
693
  while (true) {
461
694
  const config = await client.send(
462
695
  new GetFunctionConfigurationCommand({
463
696
  FunctionName: functionName,
464
- }),
697
+ })
465
698
  );
466
699
 
467
700
  // Check if function is in a stable state
@@ -472,7 +705,7 @@ async function waitForFunctionStabilization(
472
705
  // If there's a failure, throw an error
473
706
  if (config.State === "Failed" || config.LastUpdateStatus === "Failed") {
474
707
  throw new Error(
475
- `Function failed to stabilize: ${config.StateReason || config.LastUpdateStatusReason}`,
708
+ `Function failed to stabilize: ${config.StateReason || config.LastUpdateStatusReason}`
476
709
  );
477
710
  }
478
711
 
@@ -48,11 +48,16 @@ export async function handleApiError(
48
48
  resourceType: string,
49
49
  resourceName: string
50
50
  ): Promise<never> {
51
- const json: any = await response.json();
51
+ const text = await response.text();
52
+ let json: any;
53
+ try {
54
+ json = JSON.parse(text);
55
+ } catch (error) {
56
+ json = { errors: [{ message: text }] };
57
+ }
52
58
  const errors: { message: string }[] = json?.errors || [
53
59
  { message: response.statusText },
54
60
  ];
55
61
  const errorMessage = `Error ${action} ${resourceType} '${resourceName}': ${errors[0]?.message || response.statusText}`;
56
-
57
62
  throw new CloudflareApiError(errorMessage, response, errors);
58
63
  }
@@ -1 +1,2 @@
1
+ export * from "./repository-environment";
1
2
  export * from "./secret";