@wraps.dev/cli 2.5.1 → 2.6.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.
package/dist/cli.js CHANGED
@@ -737,7 +737,9 @@ var init_events = __esm({
737
737
  import * as clack from "@clack/prompts";
738
738
  import pc2 from "picocolors";
739
739
  function isAWSError(error) {
740
- if (!(error instanceof Error)) return false;
740
+ if (!(error instanceof Error)) {
741
+ return false;
742
+ }
741
743
  const awsErrorNames = [
742
744
  "ExpiredTokenException",
743
745
  "InvalidClientTokenId",
@@ -754,7 +756,9 @@ function isAWSError(error) {
754
756
  return awsErrorNames.includes(error.name) || "$metadata" in error;
755
757
  }
756
758
  function isPulumiError(error) {
757
- if (!(error instanceof Error)) return false;
759
+ if (!(error instanceof Error)) {
760
+ return false;
761
+ }
758
762
  return error.message?.includes("pulumi") || error.message?.includes("Pulumi") || error.message?.includes("resource") || error.message?.includes("creating") || error.message?.includes("AccessDenied");
759
763
  }
760
764
  function parseAWSError(error) {
@@ -805,7 +809,9 @@ function parsePulumiError(error) {
805
809
  return { code: "PULUMI_ERROR" };
806
810
  }
807
811
  function sanitizeErrorMessage(error) {
808
- if (!error) return "Unknown error";
812
+ if (!error) {
813
+ return "Unknown error";
814
+ }
809
815
  let message = error instanceof Error ? error.message : String(error);
810
816
  message = message.replace(/\b\d{12}\b/g, "[ACCOUNT_ID]");
811
817
  message = message.replace(
@@ -1022,7 +1028,7 @@ To remove: wraps destroy --stack ${stackName}`,
1022
1028
  stackLocked: () => new WrapsError(
1023
1029
  "The Pulumi stack is locked from a previous run",
1024
1030
  "STACK_LOCKED",
1025
- "This happens when a previous deployment was interrupted.\n\nTo unlock, run:\n rm -rf ~/.wraps/pulumi/.pulumi/locks\n\nThen try your command again.",
1031
+ "This happens when a previous deployment was interrupted.\n\nFor local state, run:\n rm -rf ~/.wraps/pulumi/.pulumi/locks\n\nFor S3 state, delete the lock object in your wraps-state-* bucket under .pulumi/locks/\n\nThen try your command again.",
1026
1032
  "https://wraps.dev/docs/guides/aws-setup/permissions/troubleshooting"
1027
1033
  ),
1028
1034
  // SMS-specific errors
@@ -1162,6 +1168,24 @@ View required SES permissions:
1162
1168
  "ROUTE53_PERMISSION_DENIED",
1163
1169
  "Your IAM user/role needs Route53 permissions for automatic DNS management.\nRequired actions: ChangeResourceRecordSets, ListHostedZones\n\nThis is optional - you can add DNS records manually instead.",
1164
1170
  "https://wraps.dev/docs/guides/aws-setup/permissions"
1171
+ ),
1172
+ s3StateBucketCreationFailed: (bucketName) => new WrapsError(
1173
+ `Failed to create S3 state bucket: ${bucketName}`,
1174
+ "S3_STATE_BUCKET_CREATION_FAILED",
1175
+ "Ensure your IAM user/role has s3:CreateBucket, s3:PutBucketEncryption, s3:PutBucketVersioning permissions.\n\nTo use local-only state instead:\n export WRAPS_LOCAL_ONLY=1",
1176
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
1177
+ ),
1178
+ s3StateAccessDenied: () => new WrapsError(
1179
+ "Access denied to S3 state bucket",
1180
+ "S3_STATE_ACCESS_DENIED",
1181
+ "Ensure your IAM user/role has s3:GetObject, s3:PutObject, s3:ListBucket permissions on wraps-state-* buckets.\n\nTo use local-only state instead:\n export WRAPS_LOCAL_ONLY=1",
1182
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
1183
+ ),
1184
+ stateMigrationFailed: () => new WrapsError(
1185
+ "Failed to migrate Pulumi state to S3",
1186
+ "STATE_MIGRATION_FAILED",
1187
+ "The migration from local to S3 state storage failed.\nYour local state is still intact.\n\nTo skip migration and use local-only state:\n export WRAPS_LOCAL_ONLY=1",
1188
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
1165
1189
  )
1166
1190
  };
1167
1191
  }
@@ -3308,31 +3332,285 @@ var init_route53 = __esm({
3308
3332
  }
3309
3333
  });
3310
3334
 
3311
- // src/utils/shared/fs.ts
3335
+ // src/utils/shared/s3-state.ts
3336
+ var s3_state_exports = {};
3337
+ __export(s3_state_exports, {
3338
+ downloadMetadata: () => downloadMetadata,
3339
+ ensureStateBucket: () => ensureStateBucket,
3340
+ getS3BackendUrl: () => getS3BackendUrl,
3341
+ getStateBucketName: () => getStateBucketName,
3342
+ migrateLocalPulumiState: () => migrateLocalPulumiState,
3343
+ needsMigration: () => needsMigration,
3344
+ stateBucketExists: () => stateBucketExists,
3345
+ uploadMetadata: () => uploadMetadata
3346
+ });
3312
3347
  import { existsSync as existsSync2 } from "fs";
3348
+ import { readdir, writeFile } from "fs/promises";
3349
+ import { join as join2 } from "path";
3350
+ function getStateBucketName(accountId, region) {
3351
+ return `wraps-state-${accountId}-${region}`;
3352
+ }
3353
+ function getS3BackendUrl(accountId, region) {
3354
+ return `s3://${getStateBucketName(accountId, region)}`;
3355
+ }
3356
+ async function stateBucketExists(accountId, region) {
3357
+ const { S3Client, HeadBucketCommand } = await import("@aws-sdk/client-s3");
3358
+ const client = new S3Client({ region });
3359
+ const bucketName = getStateBucketName(accountId, region);
3360
+ try {
3361
+ await client.send(new HeadBucketCommand({ Bucket: bucketName }));
3362
+ return true;
3363
+ } catch (error) {
3364
+ if (error.name === "NotFound" || error.name === "NoSuchBucket" || error.$metadata?.httpStatusCode === 404) {
3365
+ return false;
3366
+ }
3367
+ throw error;
3368
+ }
3369
+ }
3370
+ async function ensureStateBucket(accountId, region) {
3371
+ const {
3372
+ S3Client,
3373
+ HeadBucketCommand,
3374
+ CreateBucketCommand,
3375
+ PutBucketEncryptionCommand,
3376
+ PutBucketVersioningCommand,
3377
+ PutPublicAccessBlockCommand,
3378
+ PutBucketTaggingCommand
3379
+ } = await import("@aws-sdk/client-s3");
3380
+ const client = new S3Client({ region });
3381
+ const bucketName = getStateBucketName(accountId, region);
3382
+ try {
3383
+ await client.send(new HeadBucketCommand({ Bucket: bucketName }));
3384
+ return bucketName;
3385
+ } catch (error) {
3386
+ if (error.name !== "NotFound" && error.name !== "NoSuchBucket" && error.$metadata?.httpStatusCode !== 404) {
3387
+ throw error;
3388
+ }
3389
+ }
3390
+ const createParams = { Bucket: bucketName };
3391
+ if (region !== "us-east-1") {
3392
+ createParams.CreateBucketConfiguration = {
3393
+ LocationConstraint: region
3394
+ };
3395
+ }
3396
+ await client.send(new CreateBucketCommand(createParams));
3397
+ await client.send(
3398
+ new PutBucketEncryptionCommand({
3399
+ Bucket: bucketName,
3400
+ ServerSideEncryptionConfiguration: {
3401
+ Rules: [
3402
+ {
3403
+ ApplyServerSideEncryptionByDefault: {
3404
+ SSEAlgorithm: "AES256"
3405
+ }
3406
+ }
3407
+ ]
3408
+ }
3409
+ })
3410
+ );
3411
+ await client.send(
3412
+ new PutBucketVersioningCommand({
3413
+ Bucket: bucketName,
3414
+ VersioningConfiguration: {
3415
+ Status: "Enabled"
3416
+ }
3417
+ })
3418
+ );
3419
+ await client.send(
3420
+ new PutPublicAccessBlockCommand({
3421
+ Bucket: bucketName,
3422
+ PublicAccessBlockConfiguration: {
3423
+ BlockPublicAcls: true,
3424
+ BlockPublicPolicy: true,
3425
+ IgnorePublicAcls: true,
3426
+ RestrictPublicBuckets: true
3427
+ }
3428
+ })
3429
+ );
3430
+ await client.send(
3431
+ new PutBucketTaggingCommand({
3432
+ Bucket: bucketName,
3433
+ Tagging: {
3434
+ TagSet: [
3435
+ { Key: "ManagedBy", Value: "wraps-cli" },
3436
+ { Key: "Purpose", Value: "state" }
3437
+ ]
3438
+ }
3439
+ })
3440
+ );
3441
+ return bucketName;
3442
+ }
3443
+ async function uploadMetadata(bucketName, metadata) {
3444
+ const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
3445
+ const client = new S3Client({ region: metadata.region });
3446
+ const key = `metadata/${metadata.accountId}-${metadata.region}.json`;
3447
+ await client.send(
3448
+ new PutObjectCommand({
3449
+ Bucket: bucketName,
3450
+ Key: key,
3451
+ Body: JSON.stringify(metadata, null, 2),
3452
+ ContentType: "application/json"
3453
+ })
3454
+ );
3455
+ }
3456
+ async function downloadMetadata(bucketName, accountId, region) {
3457
+ const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
3458
+ const client = new S3Client({ region });
3459
+ const key = `metadata/${accountId}-${region}.json`;
3460
+ try {
3461
+ const response = await client.send(
3462
+ new GetObjectCommand({
3463
+ Bucket: bucketName,
3464
+ Key: key
3465
+ })
3466
+ );
3467
+ const body = await response.Body?.transformToString();
3468
+ if (!body) {
3469
+ return null;
3470
+ }
3471
+ return JSON.parse(body);
3472
+ } catch (error) {
3473
+ if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
3474
+ return null;
3475
+ }
3476
+ throw error;
3477
+ }
3478
+ }
3479
+ async function needsMigration(localPulumiDir, accountId, region) {
3480
+ const markerPath = join2(localPulumiDir, `.migrated-${accountId}-${region}`);
3481
+ if (existsSync2(markerPath)) {
3482
+ return false;
3483
+ }
3484
+ const stacksDir = join2(localPulumiDir, ".pulumi", "stacks");
3485
+ if (!existsSync2(stacksDir)) {
3486
+ return false;
3487
+ }
3488
+ try {
3489
+ const files = await readdir(stacksDir);
3490
+ const matchingStacks = files.filter(
3491
+ (f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
3492
+ );
3493
+ return matchingStacks.length > 0;
3494
+ } catch {
3495
+ return false;
3496
+ }
3497
+ }
3498
+ async function migrateLocalPulumiState(localPulumiDir, bucketName, accountId, region) {
3499
+ const pulumi28 = await import("@pulumi/pulumi/automation/index.js");
3500
+ const stacksDir = join2(localPulumiDir, ".pulumi", "stacks");
3501
+ const files = await readdir(stacksDir);
3502
+ const stackFiles = files.filter(
3503
+ (f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
3504
+ );
3505
+ for (const stackFile of stackFiles) {
3506
+ const stackName = stackFile.replace(".json", "");
3507
+ let projectName = "wraps-email";
3508
+ if (stackName.startsWith("wraps-sms-")) {
3509
+ projectName = "wraps-sms";
3510
+ } else if (stackName.startsWith("wraps-cdn-")) {
3511
+ projectName = "wraps-cdn";
3512
+ }
3513
+ try {
3514
+ const localStack = await pulumi28.LocalWorkspace.selectStack({
3515
+ stackName,
3516
+ workDir: localPulumiDir
3517
+ });
3518
+ const state = await localStack.exportStack();
3519
+ const s3Stack = await pulumi28.LocalWorkspace.createOrSelectStack(
3520
+ {
3521
+ stackName,
3522
+ projectName,
3523
+ program: async () => ({})
3524
+ },
3525
+ {
3526
+ workDir: localPulumiDir,
3527
+ envVars: {
3528
+ PULUMI_BACKEND_URL: `s3://${bucketName}`,
3529
+ PULUMI_CONFIG_PASSPHRASE: ""
3530
+ }
3531
+ }
3532
+ );
3533
+ await s3Stack.importStack(state);
3534
+ } catch (error) {
3535
+ console.error(
3536
+ `Warning: Failed to migrate stack ${stackName}: ${error.message}`
3537
+ );
3538
+ }
3539
+ }
3540
+ const markerPath = join2(localPulumiDir, `.migrated-${accountId}-${region}`);
3541
+ await writeFile(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
3542
+ }
3543
+ var init_s3_state = __esm({
3544
+ "src/utils/shared/s3-state.ts"() {
3545
+ "use strict";
3546
+ init_esm_shims();
3547
+ }
3548
+ });
3549
+
3550
+ // src/utils/shared/fs.ts
3551
+ import { existsSync as existsSync3 } from "fs";
3313
3552
  import { mkdir } from "fs/promises";
3314
3553
  import { homedir as homedir2 } from "os";
3315
- import { join as join2 } from "path";
3554
+ import { join as join3 } from "path";
3316
3555
  function getWrapsDir() {
3317
- return join2(homedir2(), ".wraps");
3556
+ return join3(homedir2(), ".wraps");
3318
3557
  }
3319
3558
  function getPulumiWorkDir() {
3320
- return join2(getWrapsDir(), "pulumi");
3559
+ return join3(getWrapsDir(), "pulumi");
3321
3560
  }
3322
3561
  async function ensureWrapsDir() {
3323
3562
  const wrapsDir = getWrapsDir();
3324
- if (!existsSync2(wrapsDir)) {
3563
+ if (!existsSync3(wrapsDir)) {
3325
3564
  await mkdir(wrapsDir, { recursive: true });
3326
3565
  }
3327
3566
  }
3328
- async function ensurePulumiWorkDir() {
3567
+ async function ensurePulumiWorkDir(options) {
3329
3568
  await ensureWrapsDir();
3330
3569
  const pulumiDir = getPulumiWorkDir();
3331
- if (!existsSync2(pulumiDir)) {
3570
+ if (!existsSync3(pulumiDir)) {
3332
3571
  await mkdir(pulumiDir, { recursive: true });
3333
3572
  }
3334
- process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
3335
3573
  process.env.PULUMI_CONFIG_PASSPHRASE = "";
3574
+ const useS3 = options?.accountId && options?.region && process.env.WRAPS_LOCAL_ONLY !== "1";
3575
+ if (useS3) {
3576
+ try {
3577
+ const {
3578
+ ensureStateBucket: ensureStateBucket2,
3579
+ getS3BackendUrl: getS3BackendUrl2,
3580
+ needsMigration: needsMigration2,
3581
+ migrateLocalPulumiState: migrateLocalPulumiState2
3582
+ } = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
3583
+ const bucketName = await ensureStateBucket2(
3584
+ options.accountId,
3585
+ options.region
3586
+ );
3587
+ const shouldMigrate = await needsMigration2(
3588
+ pulumiDir,
3589
+ options.accountId,
3590
+ options.region
3591
+ );
3592
+ if (shouldMigrate) {
3593
+ process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
3594
+ await migrateLocalPulumiState2(
3595
+ pulumiDir,
3596
+ bucketName,
3597
+ options.accountId,
3598
+ options.region
3599
+ );
3600
+ }
3601
+ process.env.PULUMI_BACKEND_URL = getS3BackendUrl2(
3602
+ options.accountId,
3603
+ options.region
3604
+ );
3605
+ return;
3606
+ } catch (error) {
3607
+ const clack38 = await import("@clack/prompts");
3608
+ clack38.log.warn(
3609
+ `S3 state backend unavailable (${error.message}). Using local state.`
3610
+ );
3611
+ }
3612
+ }
3613
+ process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
3336
3614
  }
3337
3615
  var init_fs = __esm({
3338
3616
  "src/utils/shared/fs.ts"() {
@@ -3346,6 +3624,7 @@ var metadata_exports = {};
3346
3624
  __export(metadata_exports, {
3347
3625
  addServiceToConnection: () => addServiceToConnection,
3348
3626
  applyConfigUpdates: () => applyConfigUpdates,
3627
+ buildEmailStackConfig: () => buildEmailStackConfig,
3349
3628
  connectionExists: () => connectionExists,
3350
3629
  createConnectionMetadata: () => createConnectionMetadata,
3351
3630
  deleteConnectionMetadata: () => deleteConnectionMetadata,
@@ -3362,19 +3641,19 @@ __export(metadata_exports, {
3362
3641
  updateServiceConfig: () => updateServiceConfig
3363
3642
  });
3364
3643
  import { randomBytes } from "crypto";
3365
- import { existsSync as existsSync3 } from "fs";
3366
- import { readFile, writeFile } from "fs/promises";
3367
- import { join as join3 } from "path";
3644
+ import { existsSync as existsSync4 } from "fs";
3645
+ import { readFile, writeFile as writeFile2 } from "fs/promises";
3646
+ import { join as join4 } from "path";
3368
3647
  function getConnectionsDir() {
3369
- return join3(getWrapsDir(), "connections");
3648
+ return join4(getWrapsDir(), "connections");
3370
3649
  }
3371
3650
  function getMetadataPath(accountId, region) {
3372
- return join3(getConnectionsDir(), `${accountId}-${region}.json`);
3651
+ return join4(getConnectionsDir(), `${accountId}-${region}.json`);
3373
3652
  }
3374
3653
  async function ensureConnectionsDir() {
3375
3654
  await ensureWrapsDir();
3376
3655
  const connectionsDir = getConnectionsDir();
3377
- if (!existsSync3(connectionsDir)) {
3656
+ if (!existsSync4(connectionsDir)) {
3378
3657
  const { mkdir: mkdir2 } = await import("fs/promises");
3379
3658
  await mkdir2(connectionsDir, { recursive: true });
3380
3659
  }
@@ -3402,57 +3681,121 @@ function isLegacyMetadata(data) {
3402
3681
  }
3403
3682
  async function loadConnectionMetadata(accountId, region) {
3404
3683
  const metadataPath = getMetadataPath(accountId, region);
3405
- if (!existsSync3(metadataPath)) {
3406
- return null;
3684
+ let localData = null;
3685
+ if (existsSync4(metadataPath)) {
3686
+ try {
3687
+ const content = await readFile(metadataPath, "utf-8");
3688
+ const data = JSON.parse(content);
3689
+ if (isLegacyMetadata(data)) {
3690
+ const migrated = migrateLegacyMetadata(data);
3691
+ await saveConnectionMetadataLocal(migrated);
3692
+ localData = migrated;
3693
+ } else {
3694
+ if (!data.version) {
3695
+ data.version = "1.0.0";
3696
+ await saveConnectionMetadataLocal(data);
3697
+ }
3698
+ localData = data;
3699
+ }
3700
+ } catch (error) {
3701
+ console.error("Error loading connection metadata:", error.message);
3702
+ }
3407
3703
  }
3408
- try {
3409
- const content = await readFile(metadataPath, "utf-8");
3410
- const data = JSON.parse(content);
3411
- if (isLegacyMetadata(data)) {
3412
- const migrated = migrateLegacyMetadata(data);
3413
- await saveConnectionMetadata(migrated);
3414
- return migrated;
3415
- }
3416
- if (!data.version) {
3417
- data.version = "1.0.0";
3418
- await saveConnectionMetadata(data);
3419
- }
3420
- return data;
3421
- } catch (error) {
3422
- console.error("Error loading connection metadata:", error.message);
3423
- return null;
3704
+ if (process.env.WRAPS_LOCAL_ONLY !== "1") {
3705
+ try {
3706
+ const {
3707
+ stateBucketExists: stateBucketExists2,
3708
+ downloadMetadata: downloadMetadata2,
3709
+ uploadMetadata: uploadMetadata2,
3710
+ getStateBucketName: getStateBucketName2
3711
+ } = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
3712
+ const bucketExists = await stateBucketExists2(accountId, region);
3713
+ if (bucketExists) {
3714
+ const bucketName = getStateBucketName2(accountId, region);
3715
+ const remoteData = await downloadMetadata2(
3716
+ bucketName,
3717
+ accountId,
3718
+ region
3719
+ );
3720
+ if (remoteData && localData) {
3721
+ if (remoteData.timestamp > localData.timestamp) {
3722
+ await saveConnectionMetadataLocal(remoteData);
3723
+ return remoteData;
3724
+ }
3725
+ if (localData.timestamp > remoteData.timestamp) {
3726
+ await uploadMetadata2(bucketName, localData).catch(() => {
3727
+ });
3728
+ return localData;
3729
+ }
3730
+ return localData;
3731
+ }
3732
+ if (remoteData && !localData) {
3733
+ await saveConnectionMetadataLocal(remoteData);
3734
+ return remoteData;
3735
+ }
3736
+ if (localData && !remoteData) {
3737
+ await uploadMetadata2(bucketName, localData).catch(() => {
3738
+ });
3739
+ }
3740
+ }
3741
+ } catch {
3742
+ }
3424
3743
  }
3744
+ return localData;
3745
+ }
3746
+ async function saveConnectionMetadataLocal(metadata) {
3747
+ await ensureConnectionsDir();
3748
+ const metadataPath = getMetadataPath(metadata.accountId, metadata.region);
3749
+ const content = JSON.stringify(metadata, null, 2);
3750
+ await writeFile2(metadataPath, content, "utf-8");
3425
3751
  }
3426
3752
  async function saveConnectionMetadata(metadata) {
3427
3753
  await ensureConnectionsDir();
3428
3754
  const metadataPath = getMetadataPath(metadata.accountId, metadata.region);
3429
3755
  try {
3430
3756
  const content = JSON.stringify(metadata, null, 2);
3431
- await writeFile(metadataPath, content, "utf-8");
3757
+ await writeFile2(metadataPath, content, "utf-8");
3432
3758
  } catch (error) {
3433
3759
  console.error("Error saving connection metadata:", error.message);
3434
3760
  throw error;
3435
3761
  }
3762
+ if (process.env.WRAPS_LOCAL_ONLY !== "1") {
3763
+ try {
3764
+ const { stateBucketExists: stateBucketExists2, uploadMetadata: uploadMetadata2, getStateBucketName: getStateBucketName2 } = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
3765
+ const bucketExists = await stateBucketExists2(
3766
+ metadata.accountId,
3767
+ metadata.region
3768
+ );
3769
+ if (bucketExists) {
3770
+ const bucketName = getStateBucketName2(
3771
+ metadata.accountId,
3772
+ metadata.region
3773
+ );
3774
+ await uploadMetadata2(bucketName, metadata);
3775
+ }
3776
+ } catch {
3777
+ }
3778
+ }
3436
3779
  }
3437
3780
  async function deleteConnectionMetadata(accountId, region) {
3438
3781
  const metadataPath = getMetadataPath(accountId, region);
3439
- if (existsSync3(metadataPath)) {
3782
+ if (existsSync4(metadataPath)) {
3440
3783
  const { unlink } = await import("fs/promises");
3441
3784
  await unlink(metadataPath);
3442
3785
  }
3443
3786
  }
3444
3787
  async function listConnections() {
3445
3788
  const connectionsDir = getConnectionsDir();
3446
- if (!existsSync3(connectionsDir)) {
3789
+ if (!existsSync4(connectionsDir)) {
3447
3790
  return [];
3448
3791
  }
3449
3792
  try {
3450
- const { readdir } = await import("fs/promises");
3451
- const files = await readdir(connectionsDir);
3793
+ const { readdir: readdir2 } = await import("fs/promises");
3794
+ const files = await readdir2(connectionsDir);
3452
3795
  const connections = [];
3453
3796
  for (const file of files) {
3454
3797
  if (file.endsWith(".json")) {
3455
- const content = await readFile(join3(connectionsDir, file), "utf-8");
3798
+ const content = await readFile(join4(connectionsDir, file), "utf-8");
3456
3799
  try {
3457
3800
  const metadata = JSON.parse(content);
3458
3801
  connections.push(metadata);
@@ -3469,7 +3812,7 @@ async function listConnections() {
3469
3812
  }
3470
3813
  async function connectionExists(accountId, region) {
3471
3814
  const metadataPath = getMetadataPath(accountId, region);
3472
- return existsSync3(metadataPath);
3815
+ return existsSync4(metadataPath);
3473
3816
  }
3474
3817
  function createConnectionMetadata(accountId, region, provider, emailConfig, preset) {
3475
3818
  return {
@@ -3659,6 +4002,25 @@ async function findConnectionsWithService(accountId, service) {
3659
4002
  const accountConnections = await findConnectionsForAccount(accountId);
3660
4003
  return accountConnections.filter((conn) => hasService(conn, service));
3661
4004
  }
4005
+ function buildEmailStackConfig(metadata, region, overrides) {
4006
+ const emailService = metadata.services.email;
4007
+ let webhook;
4008
+ if (overrides && "webhook" in overrides) {
4009
+ webhook = overrides.webhook;
4010
+ } else if (emailService?.webhookSecret) {
4011
+ webhook = {
4012
+ awsAccountNumber: metadata.accountId,
4013
+ webhookSecret: emailService.webhookSecret
4014
+ };
4015
+ }
4016
+ return {
4017
+ provider: metadata.provider,
4018
+ region,
4019
+ vercel: metadata.vercel,
4020
+ emailConfig: overrides?.emailConfig ?? emailService.config,
4021
+ webhook
4022
+ };
4023
+ }
3662
4024
  function generateWebhookSecret() {
3663
4025
  return randomBytes(32).toString("hex");
3664
4026
  }
@@ -3868,9 +4230,9 @@ __export(lambda_exports, {
3868
4230
  getLambdaCode: () => getLambdaCode
3869
4231
  });
3870
4232
  import { randomBytes as randomBytes2 } from "crypto";
3871
- import { existsSync as existsSync4, mkdirSync } from "fs";
4233
+ import { existsSync as existsSync5, mkdirSync } from "fs";
3872
4234
  import { tmpdir } from "os";
3873
- import { dirname, join as join4 } from "path";
4235
+ import { dirname, join as join5 } from "path";
3874
4236
  import { fileURLToPath as fileURLToPath2 } from "url";
3875
4237
  import * as aws8 from "@pulumi/aws";
3876
4238
  import * as pulumi11 from "@pulumi/pulumi";
@@ -3879,7 +4241,7 @@ function getPackageRoot() {
3879
4241
  const currentFile = fileURLToPath2(import.meta.url);
3880
4242
  let dir = dirname(currentFile);
3881
4243
  while (dir !== dirname(dir)) {
3882
- if (existsSync4(join4(dir, "package.json"))) {
4244
+ if (existsSync5(join5(dir, "package.json"))) {
3883
4245
  return dir;
3884
4246
  }
3885
4247
  dir = dirname(dir);
@@ -3922,18 +4284,18 @@ async function findEventSourceMapping(functionName, queueArn) {
3922
4284
  }
3923
4285
  async function getLambdaCode(functionName) {
3924
4286
  const packageRoot = getPackageRoot();
3925
- const distLambdaPath = join4(packageRoot, "dist", "lambda", functionName);
3926
- const distBundleMarker = join4(distLambdaPath, ".bundled");
3927
- if (existsSync4(distBundleMarker)) {
4287
+ const distLambdaPath = join5(packageRoot, "dist", "lambda", functionName);
4288
+ const distBundleMarker = join5(distLambdaPath, ".bundled");
4289
+ if (existsSync5(distBundleMarker)) {
3928
4290
  return distLambdaPath;
3929
4291
  }
3930
- const lambdaPath = join4(packageRoot, "lambda", functionName);
3931
- const lambdaBundleMarker = join4(lambdaPath, ".bundled");
3932
- if (existsSync4(lambdaBundleMarker)) {
4292
+ const lambdaPath = join5(packageRoot, "lambda", functionName);
4293
+ const lambdaBundleMarker = join5(lambdaPath, ".bundled");
4294
+ if (existsSync5(lambdaBundleMarker)) {
3933
4295
  return lambdaPath;
3934
4296
  }
3935
- const sourcePath = join4(lambdaPath, "index.ts");
3936
- if (!existsSync4(sourcePath)) {
4297
+ const sourcePath = join5(lambdaPath, "index.ts");
4298
+ if (!existsSync5(sourcePath)) {
3937
4299
  throw new Error(
3938
4300
  `Lambda source not found: ${sourcePath}
3939
4301
  This usually means the build process didn't complete successfully.
@@ -3941,8 +4303,8 @@ Try running: pnpm build`
3941
4303
  );
3942
4304
  }
3943
4305
  const buildId = randomBytes2(8).toString("hex");
3944
- const outdir = join4(tmpdir(), `wraps-lambda-${buildId}`);
3945
- if (!existsSync4(outdir)) {
4306
+ const outdir = join5(tmpdir(), `wraps-lambda-${buildId}`);
4307
+ if (!existsSync5(outdir)) {
3946
4308
  mkdirSync(outdir, { recursive: true });
3947
4309
  }
3948
4310
  await build({
@@ -3951,7 +4313,7 @@ Try running: pnpm build`
3951
4313
  platform: "node",
3952
4314
  target: "node24",
3953
4315
  format: "esm",
3954
- outfile: join4(outdir, "index.mjs"),
4316
+ outfile: join5(outdir, "index.mjs"),
3955
4317
  external: ["@aws-sdk/*"],
3956
4318
  // AWS SDK v3 is included in Lambda runtime
3957
4319
  minify: true,
@@ -5059,12 +5421,22 @@ async function createDNSRecordsForProvider(credentials, data, selectedCategories
5059
5421
  data.mailFromDomain
5060
5422
  );
5061
5423
  let recordsCreated = 0;
5062
- if (categories.has("dkim")) recordsCreated += data.dkimTokens.length;
5063
- if (categories.has("spf")) recordsCreated += 1;
5064
- if (categories.has("dmarc")) recordsCreated += 1;
5424
+ if (categories.has("dkim")) {
5425
+ recordsCreated += data.dkimTokens.length;
5426
+ }
5427
+ if (categories.has("spf")) {
5428
+ recordsCreated += 1;
5429
+ }
5430
+ if (categories.has("dmarc")) {
5431
+ recordsCreated += 1;
5432
+ }
5065
5433
  if (data.mailFromDomain) {
5066
- if (categories.has("mailfrom_mx")) recordsCreated += 1;
5067
- if (categories.has("mailfrom_spf")) recordsCreated += 1;
5434
+ if (categories.has("mailfrom_mx")) {
5435
+ recordsCreated += 1;
5436
+ }
5437
+ if (categories.has("mailfrom_spf")) {
5438
+ recordsCreated += 1;
5439
+ }
5068
5440
  }
5069
5441
  return {
5070
5442
  success: true,
@@ -5366,10 +5738,18 @@ async function detectAvailableDNSProviders(domain, region) {
5366
5738
  hint: "I'll add DNS records myself"
5367
5739
  });
5368
5740
  return providers.sort((a, b) => {
5369
- if (a.provider === "manual") return 1;
5370
- if (b.provider === "manual") return -1;
5371
- if (a.detected && !b.detected) return -1;
5372
- if (!a.detected && b.detected) return 1;
5741
+ if (a.provider === "manual") {
5742
+ return 1;
5743
+ }
5744
+ if (b.provider === "manual") {
5745
+ return -1;
5746
+ }
5747
+ if (a.detected && !b.detected) {
5748
+ return -1;
5749
+ }
5750
+ if (!a.detected && b.detected) {
5751
+ return 1;
5752
+ }
5373
5753
  return 0;
5374
5754
  });
5375
5755
  }
@@ -5731,7 +6111,7 @@ var init_dynamodb_metrics = __esm({
5731
6111
  // src/cli.ts
5732
6112
  init_esm_shims();
5733
6113
  import { readFileSync as readFileSync2 } from "fs";
5734
- import { dirname as dirname2, join as join5 } from "path";
6114
+ import { dirname as dirname2, join as join6 } from "path";
5735
6115
  import { fileURLToPath as fileURLToPath4 } from "url";
5736
6116
  import * as clack37 from "@clack/prompts";
5737
6117
  import args from "args";
@@ -7316,7 +7696,7 @@ async function cdnDestroy(options) {
7316
7696
  const previewResult = await progress.execute(
7317
7697
  "Generating destruction preview",
7318
7698
  async () => {
7319
- await ensurePulumiWorkDir();
7699
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
7320
7700
  const stackName = storedStackName || `wraps-cdn-${identity.accountId}-${region}`;
7321
7701
  let stack;
7322
7702
  try {
@@ -7394,7 +7774,7 @@ async function cdnDestroy(options) {
7394
7774
  await progress.execute(
7395
7775
  "Destroying CDN infrastructure (this may take 2-3 minutes)",
7396
7776
  async () => {
7397
- await ensurePulumiWorkDir();
7777
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
7398
7778
  const stackName = storedStackName || `wraps-cdn-${identity.accountId}-${region}`;
7399
7779
  let stack;
7400
7780
  try {
@@ -7591,6 +7971,8 @@ async function createCdnBucket(config2) {
7591
7971
  "https://*.wraps.dev",
7592
7972
  "http://localhost:3000",
7593
7973
  "http://localhost:3001",
7974
+ "http://localhost:3002",
7975
+ "http://localhost:3003",
7594
7976
  "http://localhost:4000",
7595
7977
  "http://localhost:5173",
7596
7978
  "http://localhost:5174",
@@ -8649,7 +9031,7 @@ ${pc8.yellow(pc8.bold("Configuration Notes:"))}`);
8649
9031
  const previewResult = await progress.execute(
8650
9032
  "Generating infrastructure preview",
8651
9033
  async () => {
8652
- await ensurePulumiWorkDir();
9034
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
8653
9035
  const stack = await pulumi4.automation.LocalWorkspace.createOrSelectStack(
8654
9036
  {
8655
9037
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
@@ -8713,7 +9095,7 @@ ${pc8.yellow(pc8.bold("Configuration Notes:"))}`);
8713
9095
  outputs = await progress.execute(
8714
9096
  "Deploying CDN infrastructure (this may take 2-3 minutes)",
8715
9097
  async () => {
8716
- await ensurePulumiWorkDir();
9098
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
8717
9099
  const stack = await pulumi4.automation.LocalWorkspace.createOrSelectStack(
8718
9100
  {
8719
9101
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
@@ -9082,7 +9464,7 @@ async function cdnStatus(options) {
9082
9464
  }
9083
9465
  let stackOutputs = {};
9084
9466
  try {
9085
- await ensurePulumiWorkDir();
9467
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
9086
9468
  const stack = await pulumi5.automation.LocalWorkspace.selectStack({
9087
9469
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
9088
9470
  workDir: getPulumiWorkDir()
@@ -9260,7 +9642,7 @@ async function cdnSync(options) {
9260
9642
  let existingCertArn;
9261
9643
  if (cdnConfig.cdn.customDomain) {
9262
9644
  try {
9263
- await ensurePulumiWorkDir();
9645
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
9264
9646
  const checkStack = await pulumi6.automation.LocalWorkspace.selectStack({
9265
9647
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
9266
9648
  workDir: getPulumiWorkDir()
@@ -9297,7 +9679,7 @@ async function cdnSync(options) {
9297
9679
  };
9298
9680
  try {
9299
9681
  await progress.execute("Syncing CDN infrastructure", async () => {
9300
- await ensurePulumiWorkDir();
9682
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
9301
9683
  const stack = await pulumi6.automation.LocalWorkspace.createOrSelectStack(
9302
9684
  {
9303
9685
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
@@ -9414,7 +9796,7 @@ async function cdnUpgrade(options) {
9414
9796
  const cdnConfig = cdnService.config;
9415
9797
  let stackOutputs = {};
9416
9798
  try {
9417
- await ensurePulumiWorkDir();
9799
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
9418
9800
  const stack = await pulumi7.automation.LocalWorkspace.selectStack({
9419
9801
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
9420
9802
  workDir: getPulumiWorkDir()
@@ -9551,7 +9933,7 @@ Ready to add custom domain: ${pc11.cyan(pendingDomain)}`);
9551
9933
  await progress.execute(
9552
9934
  "Updating CloudFront distribution (this may take 2-3 minutes)",
9553
9935
  async () => {
9554
- await ensurePulumiWorkDir();
9936
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
9555
9937
  const stack = await pulumi7.automation.LocalWorkspace.createOrSelectStack(
9556
9938
  {
9557
9939
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
@@ -9721,7 +10103,7 @@ async function cdnVerify(options) {
9721
10103
  }
9722
10104
  let stackOutputs = {};
9723
10105
  try {
9724
- await ensurePulumiWorkDir();
10106
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
9725
10107
  const stack = await pulumi8.automation.LocalWorkspace.selectStack({
9726
10108
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
9727
10109
  workDir: getPulumiWorkDir()
@@ -14026,22 +14408,13 @@ ${pc14.bold("Current Configuration:")}
14026
14408
  process.exit(0);
14027
14409
  }
14028
14410
  }
14029
- let vercelConfig;
14030
- if (metadata.provider === "vercel" && metadata.vercel) {
14031
- vercelConfig = metadata.vercel;
14032
- }
14033
- const stackConfig = {
14034
- provider: metadata.provider,
14035
- region,
14036
- vercel: vercelConfig,
14037
- emailConfig: config2
14038
- };
14411
+ const stackConfig = buildEmailStackConfig(metadata, region);
14039
14412
  if (options.preview) {
14040
14413
  try {
14041
14414
  const previewResult = await progress.execute(
14042
14415
  "Generating update preview",
14043
14416
  async () => {
14044
- await ensurePulumiWorkDir();
14417
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
14045
14418
  const stack = await pulumi12.automation.LocalWorkspace.createOrSelectStack(
14046
14419
  {
14047
14420
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
@@ -14105,7 +14478,7 @@ ${pc14.bold("Current Configuration:")}
14105
14478
  outputs = await progress.execute(
14106
14479
  "Updating Wraps infrastructure (this may take 2-3 minutes)",
14107
14480
  async () => {
14108
- await ensurePulumiWorkDir();
14481
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
14109
14482
  const stack = await pulumi12.automation.LocalWorkspace.createOrSelectStack(
14110
14483
  {
14111
14484
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
@@ -14542,7 +14915,7 @@ async function connect2(options) {
14542
14915
  const previewResult = await progress.execute(
14543
14916
  "Generating infrastructure preview",
14544
14917
  async () => {
14545
- await ensurePulumiWorkDir();
14918
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
14546
14919
  const stack = await pulumi13.automation.LocalWorkspace.createOrSelectStack(
14547
14920
  {
14548
14921
  stackName: `wraps-${identity.accountId}-${region}`,
@@ -14606,7 +14979,7 @@ async function connect2(options) {
14606
14979
  outputs = await progress.execute(
14607
14980
  "Deploying Wraps infrastructure (this may take 2-3 minutes)",
14608
14981
  async () => {
14609
- await ensurePulumiWorkDir();
14982
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
14610
14983
  const stack = await pulumi13.automation.LocalWorkspace.createOrSelectStack(
14611
14984
  {
14612
14985
  stackName: `wraps-${identity.accountId}-${region}`,
@@ -14902,7 +15275,7 @@ async function emailDestroy(options) {
14902
15275
  const previewResult = await progress.execute(
14903
15276
  "Generating destruction preview",
14904
15277
  async () => {
14905
- await ensurePulumiWorkDir();
15278
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
14906
15279
  const stackName = storedStackName || `wraps-${identity.accountId}-${region}`;
14907
15280
  let stack;
14908
15281
  try {
@@ -14973,7 +15346,7 @@ async function emailDestroy(options) {
14973
15346
  await progress.execute(
14974
15347
  "Destroying email infrastructure (this may take 2-3 minutes)",
14975
15348
  async () => {
14976
- await ensurePulumiWorkDir();
15349
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
14977
15350
  const stackName = storedStackName || `wraps-${identity.accountId}-${region}`;
14978
15351
  let stack;
14979
15352
  try {
@@ -15517,7 +15890,11 @@ var CORE_IAM_ACTIONS = [
15517
15890
  "ses:CreateEmailIdentity",
15518
15891
  "ses:DeleteEmailIdentity",
15519
15892
  "ses:GetEmailIdentity",
15520
- "ses:PutEmailIdentityDkimAttributes"
15893
+ "ses:PutEmailIdentityDkimAttributes",
15894
+ "s3:HeadBucket",
15895
+ "s3:CreateBucket",
15896
+ "s3:GetObject",
15897
+ "s3:PutObject"
15521
15898
  ];
15522
15899
  var EVENT_TRACKING_ACTIONS = [
15523
15900
  "events:CreateEventBus",
@@ -15584,8 +15961,12 @@ async function checkIAMPermissions(userArn, actions, region) {
15584
15961
  const actionName = result.EvalActionName;
15585
15962
  const decision = result.EvalDecision;
15586
15963
  if (decision === "allowed") {
15587
- if (actionName) allowedActions.push(actionName);
15588
- } else if (actionName) deniedActions.push(actionName);
15964
+ if (actionName) {
15965
+ allowedActions.push(actionName);
15966
+ }
15967
+ } else if (actionName) {
15968
+ deniedActions.push(actionName);
15969
+ }
15589
15970
  }
15590
15971
  }
15591
15972
  return {
@@ -15615,7 +15996,9 @@ async function checkIAMPermissions(userArn, actions, region) {
15615
15996
  }
15616
15997
  }
15617
15998
  function formatDeniedActions(actions) {
15618
- if (actions.length === 0) return "";
15999
+ if (actions.length === 0) {
16000
+ return "";
16001
+ }
15619
16002
  const byService = {};
15620
16003
  for (const action of actions) {
15621
16004
  const [service, actionName] = action.split(":");
@@ -15772,7 +16155,7 @@ ${pc18.yellow(pc18.bold("Configuration Warnings:"))}`);
15772
16155
  const previewResult = await progress.execute(
15773
16156
  "Generating infrastructure preview",
15774
16157
  async () => {
15775
- await ensurePulumiWorkDir();
16158
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
15776
16159
  const stack = await pulumi15.automation.LocalWorkspace.createOrSelectStack(
15777
16160
  {
15778
16161
  stackName: `wraps-${identity.accountId}-${region}`,
@@ -15841,7 +16224,7 @@ ${pc18.yellow(pc18.bold("Configuration Warnings:"))}`);
15841
16224
  outputs = await progress.execute(
15842
16225
  "Deploying infrastructure (this may take 2-3 minutes)",
15843
16226
  async () => {
15844
- await ensurePulumiWorkDir();
16227
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
15845
16228
  const stack = await pulumi15.automation.LocalWorkspace.createOrSelectStack(
15846
16229
  {
15847
16230
  stackName: `wraps-${identity.accountId}-${region}`,
@@ -16374,7 +16757,7 @@ async function emailStatus(options) {
16374
16757
  }
16375
16758
  let stackOutputs = {};
16376
16759
  try {
16377
- await ensurePulumiWorkDir();
16760
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
16378
16761
  const stack = await pulumi17.automation.LocalWorkspace.selectStack({
16379
16762
  stackName: `wraps-${identity.accountId}-${region}`,
16380
16763
  workDir: getPulumiWorkDir()
@@ -17570,30 +17953,18 @@ ${pc21.bold("Cost Impact:")}`);
17570
17953
  process.exit(0);
17571
17954
  }
17572
17955
  }
17573
- let vercelConfig;
17574
17956
  if (metadata.provider === "vercel" && !metadata.vercel) {
17575
- vercelConfig = await promptVercelConfig();
17576
- } else if (metadata.provider === "vercel") {
17577
- vercelConfig = metadata.vercel;
17957
+ metadata.vercel = await promptVercelConfig();
17578
17958
  }
17579
- const stackConfig = {
17580
- provider: metadata.provider,
17581
- region,
17582
- vercel: vercelConfig,
17583
- emailConfig: updatedConfig,
17584
- // Include webhook config if Wraps Dashboard is connected
17585
- webhook: metadata.services.email?.webhookSecret ? {
17586
- awsAccountNumber: metadata.accountId,
17587
- // User's 12-digit AWS account ID
17588
- webhookSecret: metadata.services.email.webhookSecret
17589
- } : void 0
17590
- };
17959
+ const stackConfig = buildEmailStackConfig(metadata, region, {
17960
+ emailConfig: updatedConfig
17961
+ });
17591
17962
  if (options.preview) {
17592
17963
  try {
17593
17964
  const previewResult = await progress.execute(
17594
17965
  "Generating upgrade preview",
17595
17966
  async () => {
17596
- await ensurePulumiWorkDir();
17967
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
17597
17968
  const stack = await pulumi18.automation.LocalWorkspace.createOrSelectStack(
17598
17969
  {
17599
17970
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
@@ -17671,7 +18042,7 @@ ${pc21.bold("Cost Impact:")}`);
17671
18042
  outputs = await progress.execute(
17672
18043
  "Updating Wraps infrastructure (this may take 2-3 minutes)",
17673
18044
  async () => {
17674
- await ensurePulumiWorkDir();
18045
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
17675
18046
  const stack = await pulumi18.automation.LocalWorkspace.createOrSelectStack(
17676
18047
  {
17677
18048
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
@@ -18290,6 +18661,26 @@ function getS3Statements() {
18290
18661
  }
18291
18662
  ];
18292
18663
  }
18664
+ function getS3StateStatements() {
18665
+ return [
18666
+ {
18667
+ Sid: "S3StateManagement",
18668
+ Effect: "Allow",
18669
+ Action: [
18670
+ "s3:CreateBucket",
18671
+ "s3:HeadBucket",
18672
+ "s3:PutBucketEncryption",
18673
+ "s3:PutBucketVersioning",
18674
+ "s3:PutPublicAccessBlock",
18675
+ "s3:PutBucketTagging",
18676
+ "s3:GetObject",
18677
+ "s3:PutObject",
18678
+ "s3:ListBucket"
18679
+ ],
18680
+ Resource: ["arn:aws:s3:::wraps-state-*", "arn:aws:s3:::wraps-state-*/*"]
18681
+ }
18682
+ ];
18683
+ }
18293
18684
  function getCloudFrontStatements() {
18294
18685
  return [
18295
18686
  {
@@ -18364,7 +18755,10 @@ function getSMSStatements() {
18364
18755
  ];
18365
18756
  }
18366
18757
  function buildPolicy(service, preset) {
18367
- const statements = [...getBaseStatements()];
18758
+ const statements = [
18759
+ ...getBaseStatements(),
18760
+ ...getS3StateStatements()
18761
+ ];
18368
18762
  if (!service || service === "email") {
18369
18763
  statements.push(...getSESStatements());
18370
18764
  if (!preset || preset === "production" || preset === "enterprise") {
@@ -18413,6 +18807,9 @@ ${pc23.dim("Service:")} ${pc23.cyan(serviceLabel)}`);
18413
18807
  console.log(` ${pc23.green("+")} ${pc23.bold("IAM")} - Role management`);
18414
18808
  console.log(` ${pc23.green("+")} ${pc23.bold("STS")} - Credential validation`);
18415
18809
  console.log(` ${pc23.green("+")} ${pc23.bold("CloudWatch")} - Metrics access`);
18810
+ console.log(
18811
+ ` ${pc23.green("+")} ${pc23.bold("S3")} - State management ${pc23.dim("(wraps-state-* buckets)")}`
18812
+ );
18416
18813
  if (!service || service === "email") {
18417
18814
  console.log(
18418
18815
  ` ${pc23.green("+")} ${pc23.bold("SES")} - Email sending & configuration`
@@ -18725,9 +19122,9 @@ Run ${pc24.cyan("wraps email init")} or ${pc24.cyan("wraps sms init")} first.
18725
19122
  let webhookSecret;
18726
19123
  let needsDeployment = false;
18727
19124
  if (hasEmail) {
18728
- const emailConfig = metadata.services.email.config;
18729
- const existingSecret = metadata.services.email.webhookSecret;
18730
- if (!emailConfig.eventTracking?.enabled) {
19125
+ const emailConfig = metadata.services.email?.config;
19126
+ const existingSecret = metadata.services.email?.webhookSecret;
19127
+ if (!emailConfig?.eventTracking?.enabled) {
18731
19128
  progress.stop();
18732
19129
  log21.warn(
18733
19130
  "Event tracking must be enabled to connect to the Wraps Platform."
@@ -18756,8 +19153,8 @@ Run ${pc24.cyan("wraps email init")} or ${pc24.cyan("wraps sms init")} first.
18756
19153
  "BOUNCE",
18757
19154
  "COMPLAINT"
18758
19155
  ],
18759
- dynamoDBHistory: emailConfig.eventTracking?.dynamoDBHistory ?? false,
18760
- archiveRetention: emailConfig.eventTracking?.archiveRetention ?? "90days"
19156
+ dynamoDBHistory: emailConfig?.eventTracking?.dynamoDBHistory ?? false,
19157
+ archiveRetention: emailConfig?.eventTracking?.archiveRetention ?? "90days"
18761
19158
  }
18762
19159
  };
18763
19160
  needsDeployment = true;
@@ -18817,25 +19214,15 @@ Run ${pc24.cyan("wraps email init")} or ${pc24.cyan("wraps sms init")} first.
18817
19214
  }
18818
19215
  }
18819
19216
  if (needsDeployment && hasEmail) {
18820
- let vercelConfig;
18821
19217
  if (metadata.provider === "vercel" && !metadata.vercel) {
18822
19218
  progress.stop();
18823
- vercelConfig = await promptVercelConfig();
18824
- } else if (metadata.provider === "vercel") {
18825
- vercelConfig = metadata.vercel;
19219
+ metadata.vercel = await promptVercelConfig();
18826
19220
  }
18827
- const stackConfig = {
18828
- provider: metadata.provider,
18829
- region,
18830
- vercel: vercelConfig,
18831
- emailConfig: metadata.services.email.config,
18832
- webhook: webhookSecret ? {
18833
- awsAccountNumber: metadata.accountId,
18834
- webhookSecret
18835
- } : void 0
18836
- };
19221
+ const stackConfig = buildEmailStackConfig(metadata, region, {
19222
+ webhook: webhookSecret ? { awsAccountNumber: metadata.accountId, webhookSecret } : void 0
19223
+ });
18837
19224
  await progress.execute("Configuring event streaming", async () => {
18838
- await ensurePulumiWorkDir();
19225
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
18839
19226
  const stack = await pulumi19.automation.LocalWorkspace.createOrSelectStack(
18840
19227
  {
18841
19228
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
@@ -21827,7 +22214,7 @@ async function dashboard(options) {
21827
22214
  let smsStackOutputs = {};
21828
22215
  let storageStackOutputs = {};
21829
22216
  try {
21830
- await ensurePulumiWorkDir();
22217
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
21831
22218
  try {
21832
22219
  const emailStack = await pulumi20.automation.LocalWorkspace.selectStack({
21833
22220
  stackName: `wraps-${identity.accountId}-${region}`,
@@ -22037,7 +22424,7 @@ async function status(_options) {
22037
22424
  progress.info(`Region: ${pc29.cyan(region)}`);
22038
22425
  const services = [];
22039
22426
  try {
22040
- await ensurePulumiWorkDir();
22427
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
22041
22428
  const emailStack = await pulumi21.automation.LocalWorkspace.selectStack({
22042
22429
  stackName: `wraps-${identity.accountId}-${region}`,
22043
22430
  workDir: getPulumiWorkDir()
@@ -23083,7 +23470,7 @@ async function smsDestroy(options) {
23083
23470
  const previewResult = await progress.execute(
23084
23471
  "Generating destruction preview",
23085
23472
  async () => {
23086
- await ensurePulumiWorkDir();
23473
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
23087
23474
  const stackName = storedStackName || `wraps-sms-${identity.accountId}-${region}`;
23088
23475
  let stack;
23089
23476
  try {
@@ -23140,7 +23527,7 @@ async function smsDestroy(options) {
23140
23527
  await progress.execute(
23141
23528
  "Destroying SMS infrastructure (this may take 2-3 minutes)",
23142
23529
  async () => {
23143
- await ensurePulumiWorkDir();
23530
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
23144
23531
  const stackName = storedStackName || `wraps-sms-${identity.accountId}-${region}`;
23145
23532
  let stack;
23146
23533
  try {
@@ -23847,7 +24234,7 @@ ${pc31.yellow(pc31.bold("Important Notes:"))}`);
23847
24234
  outputs = await progress.execute(
23848
24235
  "Deploying SMS infrastructure (this may take 2-3 minutes)",
23849
24236
  async () => {
23850
- await ensurePulumiWorkDir();
24237
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
23851
24238
  const stack = await pulumi24.automation.LocalWorkspace.createOrSelectStack(
23852
24239
  {
23853
24240
  stackName: `wraps-sms-${identity.accountId}-${region}`,
@@ -24257,7 +24644,7 @@ Run ${pc33.cyan("wraps sms init")} to deploy SMS infrastructure.
24257
24644
  }
24258
24645
  let stackOutputs = {};
24259
24646
  try {
24260
- await ensurePulumiWorkDir();
24647
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
24261
24648
  const stackName = metadata.services.sms.pulumiStackName || `wraps-sms-${identity.accountId}-${region}`;
24262
24649
  const stack = await pulumi25.automation.LocalWorkspace.selectStack({
24263
24650
  stackName,
@@ -24354,7 +24741,7 @@ Run ${pc34.cyan("wraps sms init")} to deploy SMS infrastructure first.
24354
24741
  let outputs;
24355
24742
  try {
24356
24743
  outputs = await progress.execute("Syncing SMS infrastructure", async () => {
24357
- await ensurePulumiWorkDir();
24744
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
24358
24745
  const stackName = storedStackName || `wraps-sms-${identity.accountId}-${region}`;
24359
24746
  const stack = await pulumi26.automation.LocalWorkspace.createOrSelectStack(
24360
24747
  {
@@ -25419,7 +25806,7 @@ ${pc36.bold("Cost Impact:")}`);
25419
25806
  outputs = await progress.execute(
25420
25807
  "Updating SMS infrastructure (this may take 2-3 minutes)",
25421
25808
  async () => {
25422
- await ensurePulumiWorkDir();
25809
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
25423
25810
  const stack = await pulumi27.automation.LocalWorkspace.createOrSelectStack(
25424
25811
  {
25425
25812
  stackName: metadata.services.sms?.pulumiStackName || `wraps-sms-${identity.accountId}-${region}`,
@@ -26102,7 +26489,7 @@ if (nodeMajorVersion < 20) {
26102
26489
  var __filename2 = fileURLToPath4(import.meta.url);
26103
26490
  var __dirname3 = dirname2(__filename2);
26104
26491
  var packageJson = JSON.parse(
26105
- readFileSync2(join5(__dirname3, "../package.json"), "utf-8")
26492
+ readFileSync2(join6(__dirname3, "../package.json"), "utf-8")
26106
26493
  );
26107
26494
  var VERSION = packageJson.version;
26108
26495
  setupTabCompletion();