@wraps.dev/cli 2.1.0 → 2.2.1
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/README.md +36 -0
- package/dist/cli.js +700 -437
- package/dist/cli.js.map +1 -1
- package/dist/console/assets/{index-C2_7RIq3.js → index-JCrXm_D5.js} +2 -2
- package/dist/console/index.html +1 -1
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -147,7 +147,7 @@ var require_package = __commonJS({
|
|
|
147
147
|
"package.json"(exports, module) {
|
|
148
148
|
module.exports = {
|
|
149
149
|
name: "@wraps.dev/cli",
|
|
150
|
-
version: "2.1
|
|
150
|
+
version: "2.2.1",
|
|
151
151
|
description: "CLI for deploying Wraps email infrastructure to your AWS account",
|
|
152
152
|
type: "module",
|
|
153
153
|
main: "./dist/cli.js",
|
|
@@ -647,6 +647,19 @@ To remove: wraps destroy --stack ${stackName}`,
|
|
|
647
647
|
"SMS_SIMULATOR_LIMIT",
|
|
648
648
|
"Upgrade to a toll-free number for production use:\n wraps sms upgrade --phone-type toll-free",
|
|
649
649
|
"https://wraps.dev/docs/cli-reference"
|
|
650
|
+
),
|
|
651
|
+
// SMTP-specific errors
|
|
652
|
+
smtpRequiresSending: () => new WrapsError(
|
|
653
|
+
"SMTP credentials require email sending to be enabled",
|
|
654
|
+
"SMTP_REQUIRES_SENDING",
|
|
655
|
+
"Enable sending first:\n wraps email upgrade\nAnd select 'Custom configuration' to enable sending.",
|
|
656
|
+
"https://wraps.dev/docs/cli-reference"
|
|
657
|
+
),
|
|
658
|
+
smtpCredentialsNotFound: () => new WrapsError(
|
|
659
|
+
"SMTP credentials not found",
|
|
660
|
+
"SMTP_CREDENTIALS_NOT_FOUND",
|
|
661
|
+
"Enable SMTP credentials:\n wraps email upgrade\nAnd select 'Enable SMTP credentials'",
|
|
662
|
+
"https://wraps.dev/docs/cli-reference"
|
|
650
663
|
)
|
|
651
664
|
};
|
|
652
665
|
}
|
|
@@ -953,6 +966,15 @@ function calculateEmailArchivingCost(config2, emailsPerMonth) {
|
|
|
953
966
|
description: `Email archiving (${retention}, ~${storageGB.toFixed(2)} GB at steady-state)`
|
|
954
967
|
};
|
|
955
968
|
}
|
|
969
|
+
function calculateSMTPCredentialsCost(config2) {
|
|
970
|
+
if (!config2.smtpCredentials?.enabled) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
monthly: 0,
|
|
975
|
+
description: "SMTP credentials (no additional cost)"
|
|
976
|
+
};
|
|
977
|
+
}
|
|
956
978
|
function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
957
979
|
const tracking = calculateTrackingCost(config2);
|
|
958
980
|
const reputationMetrics = calculateReputationMetricsCost(config2);
|
|
@@ -961,8 +983,9 @@ function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
|
961
983
|
const emailArchiving = calculateEmailArchivingCost(config2, emailsPerMonth);
|
|
962
984
|
const dedicatedIp = calculateDedicatedIpCost(config2);
|
|
963
985
|
const waf = calculateWafCost(config2, emailsPerMonth);
|
|
986
|
+
const smtpCredentials = calculateSMTPCredentialsCost(config2);
|
|
964
987
|
const sesEmailCost = Math.max(0, emailsPerMonth - FREE_TIER.SES_EMAILS) * AWS_PRICING.SES_PER_EMAIL;
|
|
965
|
-
const totalMonthlyCost = sesEmailCost + (tracking?.monthly || 0) + (reputationMetrics?.monthly || 0) + (eventTracking?.monthly || 0) + (dynamoDBHistory?.monthly || 0) + (emailArchiving?.monthly || 0) + (dedicatedIp?.monthly || 0) + (waf?.monthly || 0);
|
|
988
|
+
const totalMonthlyCost = sesEmailCost + (tracking?.monthly || 0) + (reputationMetrics?.monthly || 0) + (eventTracking?.monthly || 0) + (dynamoDBHistory?.monthly || 0) + (emailArchiving?.monthly || 0) + (dedicatedIp?.monthly || 0) + (waf?.monthly || 0) + (smtpCredentials?.monthly || 0);
|
|
966
989
|
return {
|
|
967
990
|
tracking,
|
|
968
991
|
reputationMetrics,
|
|
@@ -971,6 +994,7 @@ function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
|
971
994
|
emailArchiving,
|
|
972
995
|
dedicatedIp,
|
|
973
996
|
waf,
|
|
997
|
+
smtpCredentials,
|
|
974
998
|
total: {
|
|
975
999
|
monthly: totalMonthlyCost,
|
|
976
1000
|
perEmail: AWS_PRICING.SES_PER_EMAIL,
|
|
@@ -1031,6 +1055,11 @@ function getCostSummary(config2, emailsPerMonth = 1e4) {
|
|
|
1031
1055
|
` - ${costs.waf.description}: ${formatCost(costs.waf.monthly)}`
|
|
1032
1056
|
);
|
|
1033
1057
|
}
|
|
1058
|
+
if (costs.smtpCredentials) {
|
|
1059
|
+
lines.push(
|
|
1060
|
+
` - ${costs.smtpCredentials.description}: ${formatCost(costs.smtpCredentials.monthly)}`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1034
1063
|
return lines.join("\n");
|
|
1035
1064
|
}
|
|
1036
1065
|
var AWS_PRICING, FREE_TIER;
|
|
@@ -2317,6 +2346,11 @@ function applyConfigUpdates(existingConfig, updates) {
|
|
|
2317
2346
|
...result.emailArchiving,
|
|
2318
2347
|
...value
|
|
2319
2348
|
};
|
|
2349
|
+
} else if (key === "smtpCredentials" && typeof value === "object") {
|
|
2350
|
+
result.smtpCredentials = {
|
|
2351
|
+
...result.smtpCredentials,
|
|
2352
|
+
...value
|
|
2353
|
+
};
|
|
2320
2354
|
} else {
|
|
2321
2355
|
result[key] = value;
|
|
2322
2356
|
}
|
|
@@ -2348,8 +2382,8 @@ function addServiceToConnection(accountId, region, provider, service, config2, p
|
|
|
2348
2382
|
config: config2,
|
|
2349
2383
|
deployedAt: timestamp
|
|
2350
2384
|
};
|
|
2351
|
-
} else if (service === "
|
|
2352
|
-
existingMetadata.services.
|
|
2385
|
+
} else if (service === "cdn") {
|
|
2386
|
+
existingMetadata.services.cdn = {
|
|
2353
2387
|
preset,
|
|
2354
2388
|
config: config2,
|
|
2355
2389
|
deployedAt: timestamp
|
|
@@ -2378,8 +2412,8 @@ function addServiceToConnection(accountId, region, provider, service, config2, p
|
|
|
2378
2412
|
config: config2,
|
|
2379
2413
|
deployedAt: timestamp
|
|
2380
2414
|
};
|
|
2381
|
-
} else if (service === "
|
|
2382
|
-
metadata.services.
|
|
2415
|
+
} else if (service === "cdn") {
|
|
2416
|
+
metadata.services.cdn = {
|
|
2383
2417
|
preset,
|
|
2384
2418
|
config: config2,
|
|
2385
2419
|
deployedAt: timestamp
|
|
@@ -2398,9 +2432,9 @@ function updateServiceConfig(metadata, service, config2) {
|
|
|
2398
2432
|
...metadata.services.sms.config,
|
|
2399
2433
|
...config2
|
|
2400
2434
|
};
|
|
2401
|
-
} else if (service === "
|
|
2402
|
-
metadata.services.
|
|
2403
|
-
...metadata.services.
|
|
2435
|
+
} else if (service === "cdn" && metadata.services.cdn) {
|
|
2436
|
+
metadata.services.cdn.config = {
|
|
2437
|
+
...metadata.services.cdn.config,
|
|
2404
2438
|
...config2
|
|
2405
2439
|
};
|
|
2406
2440
|
} else {
|
|
@@ -2415,8 +2449,8 @@ function removeServiceFromConnection(metadata, service) {
|
|
|
2415
2449
|
} else if (service === "sms") {
|
|
2416
2450
|
const { sms, ...rest } = metadata.services;
|
|
2417
2451
|
metadata.services = rest;
|
|
2418
|
-
} else if (service === "
|
|
2419
|
-
const {
|
|
2452
|
+
} else if (service === "cdn") {
|
|
2453
|
+
const { cdn, ...rest } = metadata.services;
|
|
2420
2454
|
metadata.services = rest;
|
|
2421
2455
|
}
|
|
2422
2456
|
metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2428,8 +2462,8 @@ function hasService(metadata, service) {
|
|
|
2428
2462
|
if (service === "sms") {
|
|
2429
2463
|
return metadata.services.sms !== void 0;
|
|
2430
2464
|
}
|
|
2431
|
-
if (service === "
|
|
2432
|
-
return metadata.services.
|
|
2465
|
+
if (service === "cdn") {
|
|
2466
|
+
return metadata.services.cdn !== void 0;
|
|
2433
2467
|
}
|
|
2434
2468
|
return false;
|
|
2435
2469
|
}
|
|
@@ -2441,8 +2475,8 @@ function getConfiguredServices(metadata) {
|
|
|
2441
2475
|
if (metadata.services.sms) {
|
|
2442
2476
|
services.push("sms");
|
|
2443
2477
|
}
|
|
2444
|
-
if (metadata.services.
|
|
2445
|
-
services.push("
|
|
2478
|
+
if (metadata.services.cdn) {
|
|
2479
|
+
services.push("cdn");
|
|
2446
2480
|
}
|
|
2447
2481
|
return services;
|
|
2448
2482
|
}
|
|
@@ -3230,12 +3264,12 @@ var acm_exports = {};
|
|
|
3230
3264
|
__export(acm_exports, {
|
|
3231
3265
|
createACMCertificate: () => createACMCertificate
|
|
3232
3266
|
});
|
|
3233
|
-
import * as
|
|
3267
|
+
import * as aws9 from "@pulumi/aws";
|
|
3234
3268
|
async function createACMCertificate(config2) {
|
|
3235
|
-
const usEast1Provider = new
|
|
3269
|
+
const usEast1Provider = new aws9.Provider("acm-us-east-1", {
|
|
3236
3270
|
region: "us-east-1"
|
|
3237
3271
|
});
|
|
3238
|
-
const certificate = new
|
|
3272
|
+
const certificate = new aws9.acm.Certificate(
|
|
3239
3273
|
"wraps-email-tracking-cert",
|
|
3240
3274
|
{
|
|
3241
3275
|
domainName: config2.domain,
|
|
@@ -3258,7 +3292,7 @@ async function createACMCertificate(config2) {
|
|
|
3258
3292
|
);
|
|
3259
3293
|
let certificateValidation;
|
|
3260
3294
|
if (config2.hostedZoneId) {
|
|
3261
|
-
const validationRecord = new
|
|
3295
|
+
const validationRecord = new aws9.route53.Record(
|
|
3262
3296
|
"wraps-email-tracking-cert-validation",
|
|
3263
3297
|
{
|
|
3264
3298
|
zoneId: config2.hostedZoneId,
|
|
@@ -3268,7 +3302,7 @@ async function createACMCertificate(config2) {
|
|
|
3268
3302
|
ttl: 60
|
|
3269
3303
|
}
|
|
3270
3304
|
);
|
|
3271
|
-
certificateValidation = new
|
|
3305
|
+
certificateValidation = new aws9.acm.CertificateValidation(
|
|
3272
3306
|
"wraps-email-tracking-cert-validation-waiter",
|
|
3273
3307
|
{
|
|
3274
3308
|
certificateArn: certificate.arn,
|
|
@@ -3297,7 +3331,7 @@ var cloudfront_exports = {};
|
|
|
3297
3331
|
__export(cloudfront_exports, {
|
|
3298
3332
|
createCloudFrontTracking: () => createCloudFrontTracking
|
|
3299
3333
|
});
|
|
3300
|
-
import * as
|
|
3334
|
+
import * as aws10 from "@pulumi/aws";
|
|
3301
3335
|
async function findDistributionByAlias(alias) {
|
|
3302
3336
|
try {
|
|
3303
3337
|
const { CloudFrontClient, ListDistributionsCommand } = await import("@aws-sdk/client-cloudfront");
|
|
@@ -3313,10 +3347,10 @@ async function findDistributionByAlias(alias) {
|
|
|
3313
3347
|
}
|
|
3314
3348
|
}
|
|
3315
3349
|
async function createWAFWebACL() {
|
|
3316
|
-
const usEast1Provider = new
|
|
3350
|
+
const usEast1Provider = new aws10.Provider("waf-us-east-1", {
|
|
3317
3351
|
region: "us-east-1"
|
|
3318
3352
|
});
|
|
3319
|
-
const webAcl = new
|
|
3353
|
+
const webAcl = new aws10.wafv2.WebAcl(
|
|
3320
3354
|
"wraps-email-tracking-waf",
|
|
3321
3355
|
{
|
|
3322
3356
|
scope: "CLOUDFRONT",
|
|
@@ -3431,14 +3465,14 @@ async function createCloudFrontTracking(config2) {
|
|
|
3431
3465
|
Description: "Wraps email tracking CloudFront distribution"
|
|
3432
3466
|
}
|
|
3433
3467
|
};
|
|
3434
|
-
const distribution = existingDistributionId ? new
|
|
3468
|
+
const distribution = existingDistributionId ? new aws10.cloudfront.Distribution(
|
|
3435
3469
|
"wraps-email-tracking-cdn",
|
|
3436
3470
|
distributionConfig,
|
|
3437
3471
|
{
|
|
3438
3472
|
import: existingDistributionId
|
|
3439
3473
|
// Import existing distribution
|
|
3440
3474
|
}
|
|
3441
|
-
) : new
|
|
3475
|
+
) : new aws10.cloudfront.Distribution(
|
|
3442
3476
|
"wraps-email-tracking-cdn",
|
|
3443
3477
|
distributionConfig
|
|
3444
3478
|
);
|
|
@@ -8143,7 +8177,7 @@ import pc8 from "picocolors";
|
|
|
8143
8177
|
|
|
8144
8178
|
// src/infrastructure/email-stack.ts
|
|
8145
8179
|
init_esm_shims();
|
|
8146
|
-
import * as
|
|
8180
|
+
import * as aws11 from "@pulumi/aws";
|
|
8147
8181
|
import * as pulumi4 from "@pulumi/pulumi";
|
|
8148
8182
|
|
|
8149
8183
|
// src/infrastructure/resources/dynamodb.ts
|
|
@@ -8366,10 +8400,10 @@ import * as pulumi2 from "@pulumi/pulumi";
|
|
|
8366
8400
|
async function roleExists(roleName) {
|
|
8367
8401
|
try {
|
|
8368
8402
|
const { IAMClient: IAMClient3, GetRoleCommand: GetRoleCommand2 } = await import("@aws-sdk/client-iam");
|
|
8369
|
-
const
|
|
8403
|
+
const iam8 = new IAMClient3({
|
|
8370
8404
|
region: process.env.AWS_REGION || "us-east-1"
|
|
8371
8405
|
});
|
|
8372
|
-
await
|
|
8406
|
+
await iam8.send(new GetRoleCommand2({ RoleName: roleName }));
|
|
8373
8407
|
return true;
|
|
8374
8408
|
} catch (error) {
|
|
8375
8409
|
if (error.name === "NoSuchEntityException" || error.Code === "NoSuchEntity" || error.Error?.Code === "NoSuchEntity") {
|
|
@@ -8728,11 +8762,93 @@ async function createSESResources(config2) {
|
|
|
8728
8762
|
};
|
|
8729
8763
|
}
|
|
8730
8764
|
|
|
8731
|
-
// src/infrastructure/resources/
|
|
8765
|
+
// src/infrastructure/resources/smtp-credentials.ts
|
|
8732
8766
|
init_esm_shims();
|
|
8733
8767
|
import * as aws6 from "@pulumi/aws";
|
|
8768
|
+
import { createHmac } from "crypto";
|
|
8769
|
+
function convertToSMTPPassword(secretAccessKey, region) {
|
|
8770
|
+
const DATE = "11111111";
|
|
8771
|
+
const SERVICE = "ses";
|
|
8772
|
+
const MESSAGE = "SendRawEmail";
|
|
8773
|
+
const TERMINAL = "aws4_request";
|
|
8774
|
+
const VERSION2 = 4;
|
|
8775
|
+
const kDate = createHmac("sha256", `AWS4${secretAccessKey}`).update(DATE).digest();
|
|
8776
|
+
const kRegion = createHmac("sha256", kDate).update(region).digest();
|
|
8777
|
+
const kService = createHmac("sha256", kRegion).update(SERVICE).digest();
|
|
8778
|
+
const kTerminal = createHmac("sha256", kService).update(TERMINAL).digest();
|
|
8779
|
+
const kMessage = createHmac("sha256", kTerminal).update(MESSAGE).digest();
|
|
8780
|
+
const signatureWithVersion = Buffer.concat([Buffer.from([VERSION2]), kMessage]);
|
|
8781
|
+
return signatureWithVersion.toString("base64");
|
|
8782
|
+
}
|
|
8783
|
+
async function userExists(userName) {
|
|
8784
|
+
try {
|
|
8785
|
+
const { IAMClient: IAMClient3, GetUserCommand } = await import("@aws-sdk/client-iam");
|
|
8786
|
+
const iam8 = new IAMClient3({ region: process.env.AWS_REGION || "us-east-1" });
|
|
8787
|
+
await iam8.send(new GetUserCommand({ UserName: userName }));
|
|
8788
|
+
return true;
|
|
8789
|
+
} catch (error) {
|
|
8790
|
+
if (error.name === "NoSuchEntityException" || error.Code === "NoSuchEntity") {
|
|
8791
|
+
return false;
|
|
8792
|
+
}
|
|
8793
|
+
return false;
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
async function createSMTPCredentials(config2) {
|
|
8797
|
+
const userName = "wraps-email-smtp-user";
|
|
8798
|
+
const userAlreadyExists = await userExists(userName);
|
|
8799
|
+
const iamUser = userAlreadyExists ? new aws6.iam.User(
|
|
8800
|
+
userName,
|
|
8801
|
+
{
|
|
8802
|
+
name: userName,
|
|
8803
|
+
tags: {
|
|
8804
|
+
ManagedBy: "wraps-cli",
|
|
8805
|
+
Purpose: "SES SMTP Authentication"
|
|
8806
|
+
}
|
|
8807
|
+
},
|
|
8808
|
+
{ import: userName }
|
|
8809
|
+
) : new aws6.iam.User(userName, {
|
|
8810
|
+
name: userName,
|
|
8811
|
+
tags: {
|
|
8812
|
+
ManagedBy: "wraps-cli",
|
|
8813
|
+
Purpose: "SES SMTP Authentication"
|
|
8814
|
+
}
|
|
8815
|
+
});
|
|
8816
|
+
new aws6.iam.UserPolicy("wraps-email-smtp-policy", {
|
|
8817
|
+
user: iamUser.name,
|
|
8818
|
+
policy: JSON.stringify({
|
|
8819
|
+
Version: "2012-10-17",
|
|
8820
|
+
Statement: [
|
|
8821
|
+
{
|
|
8822
|
+
Effect: "Allow",
|
|
8823
|
+
Action: "ses:SendRawEmail",
|
|
8824
|
+
Resource: "*",
|
|
8825
|
+
Condition: {
|
|
8826
|
+
StringEquals: {
|
|
8827
|
+
"ses:ConfigurationSetName": config2.configSetName
|
|
8828
|
+
}
|
|
8829
|
+
}
|
|
8830
|
+
}
|
|
8831
|
+
]
|
|
8832
|
+
})
|
|
8833
|
+
});
|
|
8834
|
+
const accessKey = new aws6.iam.AccessKey("wraps-email-smtp-key", {
|
|
8835
|
+
user: iamUser.name
|
|
8836
|
+
});
|
|
8837
|
+
const smtpPassword = accessKey.secret.apply(
|
|
8838
|
+
(secret) => convertToSMTPPassword(secret, config2.region)
|
|
8839
|
+
);
|
|
8840
|
+
return {
|
|
8841
|
+
iamUser,
|
|
8842
|
+
accessKey,
|
|
8843
|
+
smtpPassword
|
|
8844
|
+
};
|
|
8845
|
+
}
|
|
8846
|
+
|
|
8847
|
+
// src/infrastructure/resources/sqs.ts
|
|
8848
|
+
init_esm_shims();
|
|
8849
|
+
import * as aws7 from "@pulumi/aws";
|
|
8734
8850
|
async function createSQSResources() {
|
|
8735
|
-
const dlq = new
|
|
8851
|
+
const dlq = new aws7.sqs.Queue("wraps-email-events-dlq", {
|
|
8736
8852
|
name: "wraps-email-events-dlq",
|
|
8737
8853
|
messageRetentionSeconds: 1209600,
|
|
8738
8854
|
// 14 days
|
|
@@ -8741,7 +8857,7 @@ async function createSQSResources() {
|
|
|
8741
8857
|
Description: "Dead letter queue for failed SES event processing"
|
|
8742
8858
|
}
|
|
8743
8859
|
});
|
|
8744
|
-
const queue = new
|
|
8860
|
+
const queue = new aws7.sqs.Queue("wraps-email-events", {
|
|
8745
8861
|
name: "wraps-email-events",
|
|
8746
8862
|
visibilityTimeoutSeconds: 300,
|
|
8747
8863
|
// 5 minutes (Lambda timeout)
|
|
@@ -8769,14 +8885,14 @@ async function createSQSResources() {
|
|
|
8769
8885
|
|
|
8770
8886
|
// src/infrastructure/vercel-oidc.ts
|
|
8771
8887
|
init_esm_shims();
|
|
8772
|
-
import * as
|
|
8888
|
+
import * as aws8 from "@pulumi/aws";
|
|
8773
8889
|
async function getExistingOIDCProviderArn(url) {
|
|
8774
8890
|
try {
|
|
8775
8891
|
const { IAMClient: IAMClient3, ListOpenIDConnectProvidersCommand } = await import("@aws-sdk/client-iam");
|
|
8776
|
-
const
|
|
8892
|
+
const iam8 = new IAMClient3({
|
|
8777
8893
|
region: process.env.AWS_REGION || "us-east-1"
|
|
8778
8894
|
});
|
|
8779
|
-
const response = await
|
|
8895
|
+
const response = await iam8.send(new ListOpenIDConnectProvidersCommand({}));
|
|
8780
8896
|
const expectedArnSuffix = url.replace("https://", "");
|
|
8781
8897
|
const provider = response.OpenIDConnectProviderList?.find(
|
|
8782
8898
|
(p) => p.Arn?.endsWith(expectedArnSuffix)
|
|
@@ -8791,7 +8907,7 @@ async function createVercelOIDC(config2) {
|
|
|
8791
8907
|
const url = `https://oidc.vercel.com/${config2.teamSlug}`;
|
|
8792
8908
|
const existingArn = await getExistingOIDCProviderArn(url);
|
|
8793
8909
|
if (existingArn) {
|
|
8794
|
-
return new
|
|
8910
|
+
return new aws8.iam.OpenIdConnectProvider(
|
|
8795
8911
|
"wraps-vercel-oidc",
|
|
8796
8912
|
{
|
|
8797
8913
|
url,
|
|
@@ -8812,7 +8928,7 @@ async function createVercelOIDC(config2) {
|
|
|
8812
8928
|
}
|
|
8813
8929
|
);
|
|
8814
8930
|
}
|
|
8815
|
-
return new
|
|
8931
|
+
return new aws8.iam.OpenIdConnectProvider("wraps-vercel-oidc", {
|
|
8816
8932
|
url,
|
|
8817
8933
|
clientIdLists: [`https://vercel.com/${config2.teamSlug}`],
|
|
8818
8934
|
thumbprintLists: [
|
|
@@ -8829,7 +8945,7 @@ async function createVercelOIDC(config2) {
|
|
|
8829
8945
|
|
|
8830
8946
|
// src/infrastructure/email-stack.ts
|
|
8831
8947
|
async function deployEmailStack(config2) {
|
|
8832
|
-
const identity = await
|
|
8948
|
+
const identity = await aws11.getCallerIdentity();
|
|
8833
8949
|
const accountId = identity.accountId;
|
|
8834
8950
|
let oidcProvider;
|
|
8835
8951
|
if (config2.provider === "vercel" && config2.vercel) {
|
|
@@ -8934,6 +9050,13 @@ async function deployEmailStack(config2) {
|
|
|
8934
9050
|
region: config2.region
|
|
8935
9051
|
});
|
|
8936
9052
|
}
|
|
9053
|
+
let smtpResources;
|
|
9054
|
+
if (emailConfig.smtpCredentials?.enabled && sesResources) {
|
|
9055
|
+
smtpResources = await createSMTPCredentials({
|
|
9056
|
+
configSetName: "wraps-email-tracking",
|
|
9057
|
+
region: config2.region
|
|
9058
|
+
});
|
|
9059
|
+
}
|
|
8937
9060
|
return {
|
|
8938
9061
|
roleArn: role.arn,
|
|
8939
9062
|
configSetName: sesResources?.configSet.configurationSetName,
|
|
@@ -8953,7 +9076,12 @@ async function deployEmailStack(config2) {
|
|
|
8953
9076
|
mailFromDomain: sesResources?.mailFromDomain,
|
|
8954
9077
|
archiveArn: archiveResources?.archiveArn,
|
|
8955
9078
|
archivingEnabled: emailConfig.emailArchiving?.enabled,
|
|
8956
|
-
archiveRetention: emailConfig.emailArchiving?.enabled ? emailConfig.emailArchiving.retention : void 0
|
|
9079
|
+
archiveRetention: emailConfig.emailArchiving?.enabled ? emailConfig.emailArchiving.retention : void 0,
|
|
9080
|
+
// SMTP credentials (shown once, not stored)
|
|
9081
|
+
smtpUserArn: smtpResources?.iamUser.arn,
|
|
9082
|
+
smtpUsername: smtpResources?.accessKey.id,
|
|
9083
|
+
smtpPassword: smtpResources?.smtpPassword,
|
|
9084
|
+
smtpEndpoint: smtpResources ? `email-smtp.${config2.region}.amazonaws.com` : void 0
|
|
8957
9085
|
};
|
|
8958
9086
|
}
|
|
8959
9087
|
|
|
@@ -10000,13 +10128,13 @@ async function scanLambdaFunctions(region) {
|
|
|
10000
10128
|
}
|
|
10001
10129
|
}
|
|
10002
10130
|
async function scanIAMRoles(region) {
|
|
10003
|
-
const
|
|
10131
|
+
const iam8 = new IAMClient({ region });
|
|
10004
10132
|
const roles = [];
|
|
10005
10133
|
try {
|
|
10006
10134
|
let marker;
|
|
10007
10135
|
let hasMore = true;
|
|
10008
10136
|
while (hasMore) {
|
|
10009
|
-
const listResponse = await
|
|
10137
|
+
const listResponse = await iam8.send(
|
|
10010
10138
|
new ListRolesCommand({
|
|
10011
10139
|
Marker: marker,
|
|
10012
10140
|
MaxItems: 100
|
|
@@ -11901,6 +12029,11 @@ ${pc15.bold("Current Configuration:")}
|
|
|
11901
12029
|
value: "wraps-dashboard",
|
|
11902
12030
|
label: metadata.services.email?.webhookSecret ? "Manage Wraps Dashboard connection" : "Connect to Wraps Dashboard",
|
|
11903
12031
|
hint: metadata.services.email?.webhookSecret ? "Regenerate secret or disconnect" : "Send events to dashboard for analytics"
|
|
12032
|
+
},
|
|
12033
|
+
{
|
|
12034
|
+
value: "smtp-credentials",
|
|
12035
|
+
label: metadata.services.email?.smtpCredentials?.enabled ? "Manage SMTP credentials" : "Enable SMTP credentials",
|
|
12036
|
+
hint: metadata.services.email?.smtpCredentials?.enabled ? "Rotate or disable credentials" : "Generate credentials for PHP, WordPress, etc."
|
|
11904
12037
|
}
|
|
11905
12038
|
]
|
|
11906
12039
|
});
|
|
@@ -12492,6 +12625,98 @@ ${pc15.bold("Webhook Configuration:")}`);
|
|
|
12492
12625
|
newPreset = void 0;
|
|
12493
12626
|
break;
|
|
12494
12627
|
}
|
|
12628
|
+
case "smtp-credentials": {
|
|
12629
|
+
if (metadata.services.email?.smtpCredentials?.enabled) {
|
|
12630
|
+
clack14.log.info(
|
|
12631
|
+
`SMTP credentials are currently ${pc15.green("enabled")} (created ${metadata.services.email.smtpCredentials.createdAt})`
|
|
12632
|
+
);
|
|
12633
|
+
const smtpAction = await clack14.select({
|
|
12634
|
+
message: "What would you like to do?",
|
|
12635
|
+
options: [
|
|
12636
|
+
{
|
|
12637
|
+
value: "rotate",
|
|
12638
|
+
label: "Rotate credentials",
|
|
12639
|
+
hint: "Generate new credentials (invalidates old ones)"
|
|
12640
|
+
},
|
|
12641
|
+
{
|
|
12642
|
+
value: "disable",
|
|
12643
|
+
label: "Disable SMTP credentials",
|
|
12644
|
+
hint: "Delete IAM user and credentials"
|
|
12645
|
+
},
|
|
12646
|
+
{
|
|
12647
|
+
value: "cancel",
|
|
12648
|
+
label: "Cancel",
|
|
12649
|
+
hint: "Keep current credentials"
|
|
12650
|
+
}
|
|
12651
|
+
]
|
|
12652
|
+
});
|
|
12653
|
+
if (clack14.isCancel(smtpAction) || smtpAction === "cancel") {
|
|
12654
|
+
clack14.log.info("No changes made.");
|
|
12655
|
+
process.exit(0);
|
|
12656
|
+
}
|
|
12657
|
+
if (smtpAction === "disable") {
|
|
12658
|
+
const confirmDisable = await clack14.confirm({
|
|
12659
|
+
message: "Are you sure? Any systems using these credentials will stop working immediately.",
|
|
12660
|
+
initialValue: false
|
|
12661
|
+
});
|
|
12662
|
+
if (clack14.isCancel(confirmDisable) || !confirmDisable) {
|
|
12663
|
+
clack14.log.info("SMTP credentials not disabled.");
|
|
12664
|
+
process.exit(0);
|
|
12665
|
+
}
|
|
12666
|
+
updatedConfig = {
|
|
12667
|
+
...config2,
|
|
12668
|
+
smtpCredentials: { enabled: false }
|
|
12669
|
+
};
|
|
12670
|
+
if (metadata.services.email) {
|
|
12671
|
+
metadata.services.email.smtpCredentials = void 0;
|
|
12672
|
+
}
|
|
12673
|
+
newPreset = void 0;
|
|
12674
|
+
break;
|
|
12675
|
+
}
|
|
12676
|
+
clack14.log.info(
|
|
12677
|
+
"\nRotating credentials will invalidate your current SMTP password."
|
|
12678
|
+
);
|
|
12679
|
+
clack14.log.warn("You will need to update all systems using the old credentials.");
|
|
12680
|
+
const confirmRotate = await clack14.confirm({
|
|
12681
|
+
message: "Generate new SMTP credentials?",
|
|
12682
|
+
initialValue: false
|
|
12683
|
+
});
|
|
12684
|
+
if (clack14.isCancel(confirmRotate) || !confirmRotate) {
|
|
12685
|
+
clack14.log.info("Credential rotation cancelled.");
|
|
12686
|
+
process.exit(0);
|
|
12687
|
+
}
|
|
12688
|
+
}
|
|
12689
|
+
clack14.log.info(`
|
|
12690
|
+
${pc15.bold("SMTP Credentials for Legacy Systems")}
|
|
12691
|
+
`);
|
|
12692
|
+
clack14.log.info(pc15.dim("Generate SMTP username/password that works with:"));
|
|
12693
|
+
clack14.log.info(pc15.dim(" - PHP mail() and PHPMailer"));
|
|
12694
|
+
clack14.log.info(pc15.dim(" - WordPress (WP Mail SMTP plugin)"));
|
|
12695
|
+
clack14.log.info(pc15.dim(" - Nodemailer and other SMTP libraries"));
|
|
12696
|
+
clack14.log.info(pc15.dim(" - Any SMTP-compatible email client"));
|
|
12697
|
+
console.log("");
|
|
12698
|
+
clack14.log.warn(
|
|
12699
|
+
"Credentials will be shown ONCE after deployment - save them immediately!"
|
|
12700
|
+
);
|
|
12701
|
+
console.log("");
|
|
12702
|
+
const confirmCreate = await clack14.confirm({
|
|
12703
|
+
message: "Create SMTP credentials?",
|
|
12704
|
+
initialValue: true
|
|
12705
|
+
});
|
|
12706
|
+
if (clack14.isCancel(confirmCreate) || !confirmCreate) {
|
|
12707
|
+
clack14.log.info("SMTP credentials not created.");
|
|
12708
|
+
process.exit(0);
|
|
12709
|
+
}
|
|
12710
|
+
updatedConfig = {
|
|
12711
|
+
...config2,
|
|
12712
|
+
smtpCredentials: {
|
|
12713
|
+
enabled: true,
|
|
12714
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12715
|
+
}
|
|
12716
|
+
};
|
|
12717
|
+
newPreset = void 0;
|
|
12718
|
+
break;
|
|
12719
|
+
}
|
|
12495
12720
|
}
|
|
12496
12721
|
const newCostData = calculateCosts(updatedConfig, 5e4);
|
|
12497
12722
|
const costDiff = newCostData.total.monthly - currentCostData.total.monthly;
|
|
@@ -12643,7 +12868,12 @@ ${pc15.bold("Cost Impact:")}`);
|
|
|
12643
12868
|
acmCertificateValidationRecords: result.acmCertificateValidationRecords,
|
|
12644
12869
|
archiveArn: result.archiveArn,
|
|
12645
12870
|
archivingEnabled: result.archivingEnabled,
|
|
12646
|
-
archiveRetention: result.archiveRetention
|
|
12871
|
+
archiveRetention: result.archiveRetention,
|
|
12872
|
+
// SMTP credentials (shown once)
|
|
12873
|
+
smtpUserArn: result.smtpUserArn,
|
|
12874
|
+
smtpUsername: result.smtpUsername,
|
|
12875
|
+
smtpPassword: result.smtpPassword,
|
|
12876
|
+
smtpEndpoint: result.smtpEndpoint
|
|
12647
12877
|
};
|
|
12648
12878
|
}
|
|
12649
12879
|
},
|
|
@@ -12679,7 +12909,12 @@ ${pc15.bold("Cost Impact:")}`);
|
|
|
12679
12909
|
acmCertificateValidationRecords: pulumiOutputs.acmCertificateValidationRecords?.value,
|
|
12680
12910
|
archiveArn: pulumiOutputs.archiveArn?.value,
|
|
12681
12911
|
archivingEnabled: pulumiOutputs.archivingEnabled?.value,
|
|
12682
|
-
archiveRetention: pulumiOutputs.archiveRetention?.value
|
|
12912
|
+
archiveRetention: pulumiOutputs.archiveRetention?.value,
|
|
12913
|
+
// SMTP credentials (shown once)
|
|
12914
|
+
smtpUserArn: pulumiOutputs.smtpUserArn?.value,
|
|
12915
|
+
smtpUsername: pulumiOutputs.smtpUsername?.value,
|
|
12916
|
+
smtpPassword: pulumiOutputs.smtpPassword?.value,
|
|
12917
|
+
smtpEndpoint: pulumiOutputs.smtpEndpoint?.value
|
|
12683
12918
|
};
|
|
12684
12919
|
}
|
|
12685
12920
|
);
|
|
@@ -12823,6 +13058,31 @@ ${pc15.green("\u2713")} ${pc15.bold("Upgrade complete!")}
|
|
|
12823
13058
|
)
|
|
12824
13059
|
);
|
|
12825
13060
|
}
|
|
13061
|
+
if (upgradeAction === "smtp-credentials" && outputs.smtpUsername && outputs.smtpPassword) {
|
|
13062
|
+
console.log(pc15.bold("\n\u{1F4E7} SMTP Connection Details\n"));
|
|
13063
|
+
console.log(` ${pc15.cyan("Server:")} ${outputs.smtpEndpoint}`);
|
|
13064
|
+
console.log(` ${pc15.cyan("Port:")} 587 (STARTTLS) or 465 (TLS)`);
|
|
13065
|
+
console.log(` ${pc15.cyan("Username:")} ${outputs.smtpUsername}`);
|
|
13066
|
+
console.log(` ${pc15.cyan("Password:")} ${outputs.smtpPassword}`);
|
|
13067
|
+
console.log(` ${pc15.cyan("Encryption:")} TLS/STARTTLS required
|
|
13068
|
+
`);
|
|
13069
|
+
console.log(pc15.yellow("\u26A0\uFE0F IMPORTANT: Save these credentials NOW!"));
|
|
13070
|
+
console.log(pc15.yellow(" They cannot be retrieved later.\n"));
|
|
13071
|
+
console.log(pc15.bold(" Environment Variables:\n"));
|
|
13072
|
+
console.log(pc15.dim(` SMTP_HOST=${outputs.smtpEndpoint}`));
|
|
13073
|
+
console.log(pc15.dim(` SMTP_PORT=587`));
|
|
13074
|
+
console.log(pc15.dim(` SMTP_USER=${outputs.smtpUsername}`));
|
|
13075
|
+
console.log(pc15.dim(` SMTP_PASS=${outputs.smtpPassword}
|
|
13076
|
+
`));
|
|
13077
|
+
if (metadata.services.email && outputs.smtpUserArn) {
|
|
13078
|
+
metadata.services.email.smtpCredentials = {
|
|
13079
|
+
enabled: true,
|
|
13080
|
+
iamUserArn: outputs.smtpUserArn,
|
|
13081
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13082
|
+
};
|
|
13083
|
+
await saveConnectionMetadata(metadata);
|
|
13084
|
+
}
|
|
13085
|
+
}
|
|
12826
13086
|
const enabledFeatures = [];
|
|
12827
13087
|
if (updatedConfig.tracking?.enabled) {
|
|
12828
13088
|
enabledFeatures.push("tracking");
|
|
@@ -12842,6 +13102,9 @@ ${pc15.green("\u2713")} ${pc15.bold("Upgrade complete!")}
|
|
|
12842
13102
|
if (updatedConfig.emailArchiving?.enabled) {
|
|
12843
13103
|
enabledFeatures.push("email_archiving");
|
|
12844
13104
|
}
|
|
13105
|
+
if (updatedConfig.smtpCredentials?.enabled) {
|
|
13106
|
+
enabledFeatures.push("smtp_credentials");
|
|
13107
|
+
}
|
|
12845
13108
|
trackServiceUpgrade("email", {
|
|
12846
13109
|
from_preset: metadata.services.email?.preset,
|
|
12847
13110
|
to_preset: newPreset,
|
|
@@ -12945,10 +13208,10 @@ Run ${pc18.cyan("wraps email init")} to deploy infrastructure first.
|
|
|
12945
13208
|
process.exit(1);
|
|
12946
13209
|
}
|
|
12947
13210
|
const roleName = "wraps-console-access-role";
|
|
12948
|
-
const
|
|
13211
|
+
const iam8 = new IAMClient2({ region: "us-east-1" });
|
|
12949
13212
|
let roleExists4 = false;
|
|
12950
13213
|
try {
|
|
12951
|
-
await
|
|
13214
|
+
await iam8.send(new GetRoleCommand({ RoleName: roleName }));
|
|
12952
13215
|
roleExists4 = true;
|
|
12953
13216
|
} catch (error) {
|
|
12954
13217
|
if (error && typeof error === "object" && "name" in error && error.name !== "NoSuchEntity") {
|
|
@@ -12989,7 +13252,7 @@ Run ${pc18.cyan("wraps email init")} to deploy infrastructure first.
|
|
|
12989
13252
|
const smsEventTracking = smsConfig?.eventTracking;
|
|
12990
13253
|
await progress.execute("Updating IAM role permissions", async () => {
|
|
12991
13254
|
const { PutRolePolicyCommand } = await import("@aws-sdk/client-iam");
|
|
12992
|
-
await
|
|
13255
|
+
await iam8.send(
|
|
12993
13256
|
new PutRolePolicyCommand({
|
|
12994
13257
|
RoleName: roleName,
|
|
12995
13258
|
PolicyName: "wraps-console-access-policy",
|
|
@@ -15113,11 +15376,11 @@ function createSMSRouter(config2) {
|
|
|
15113
15376
|
return router;
|
|
15114
15377
|
}
|
|
15115
15378
|
|
|
15116
|
-
// src/console/routes/
|
|
15379
|
+
// src/console/routes/cdn.ts
|
|
15117
15380
|
init_esm_shims();
|
|
15118
15381
|
init_metadata();
|
|
15119
15382
|
import { Router as createRouter6 } from "express";
|
|
15120
|
-
function
|
|
15383
|
+
function createCdnRouter(config2) {
|
|
15121
15384
|
const router = createRouter6();
|
|
15122
15385
|
router.get("/settings", async (_req, res) => {
|
|
15123
15386
|
try {
|
|
@@ -15125,36 +15388,36 @@ function createStorageRouter(config2) {
|
|
|
15125
15388
|
config2.accountId || "",
|
|
15126
15389
|
config2.region
|
|
15127
15390
|
);
|
|
15128
|
-
if (!metadata?.services.
|
|
15391
|
+
if (!metadata?.services.cdn) {
|
|
15129
15392
|
return res.status(404).json({
|
|
15130
|
-
error: "No
|
|
15393
|
+
error: "No CDN infrastructure found for this account and region"
|
|
15131
15394
|
});
|
|
15132
15395
|
}
|
|
15133
|
-
const
|
|
15134
|
-
const
|
|
15396
|
+
const cdnService = metadata.services.cdn;
|
|
15397
|
+
const cdnConfig = cdnService.config;
|
|
15135
15398
|
const settings = {
|
|
15136
|
-
bucketName: config2.
|
|
15137
|
-
bucketArn: `arn:aws:s3:::${config2.
|
|
15399
|
+
bucketName: config2.cdnBucketName || `wraps-cdn-${config2.accountId}`,
|
|
15400
|
+
bucketArn: `arn:aws:s3:::${config2.cdnBucketName || `wraps-cdn-${config2.accountId}`}`,
|
|
15138
15401
|
region: config2.region,
|
|
15139
|
-
roleArn: config2.
|
|
15402
|
+
roleArn: config2.cdnRoleArn || config2.roleArn,
|
|
15140
15403
|
cdn: {
|
|
15141
|
-
enabled:
|
|
15142
|
-
distributionId: config2.
|
|
15143
|
-
distributionDomain: config2.
|
|
15144
|
-
customDomain:
|
|
15145
|
-
status: config2.
|
|
15404
|
+
enabled: cdnConfig.cdn?.enabled ?? false,
|
|
15405
|
+
distributionId: config2.cdnDistributionId,
|
|
15406
|
+
distributionDomain: config2.cdnDistributionDomain,
|
|
15407
|
+
customDomain: cdnConfig.cdn?.customDomain,
|
|
15408
|
+
status: config2.cdnDistributionId ? "Deployed" : void 0
|
|
15146
15409
|
},
|
|
15147
|
-
certificate:
|
|
15148
|
-
arn: config2.
|
|
15149
|
-
status: config2.
|
|
15410
|
+
certificate: cdnConfig.cdn?.customDomain ? {
|
|
15411
|
+
arn: config2.cdnCertificateArn,
|
|
15412
|
+
status: config2.cdnCertificateArn ? "ISSUED" : "PENDING_VALIDATION"
|
|
15150
15413
|
} : void 0,
|
|
15151
|
-
versioning:
|
|
15152
|
-
retention:
|
|
15414
|
+
versioning: cdnConfig.versioning ?? false,
|
|
15415
|
+
retention: cdnConfig.retention
|
|
15153
15416
|
};
|
|
15154
15417
|
res.json(settings);
|
|
15155
15418
|
} catch (error) {
|
|
15156
15419
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
15157
|
-
console.error("[
|
|
15420
|
+
console.error("[CDN] Error fetching settings:", error);
|
|
15158
15421
|
res.status(500).json({ error: errorMessage });
|
|
15159
15422
|
}
|
|
15160
15423
|
});
|
|
@@ -15164,16 +15427,16 @@ function createStorageRouter(config2) {
|
|
|
15164
15427
|
config2.accountId || "",
|
|
15165
15428
|
config2.region
|
|
15166
15429
|
);
|
|
15167
|
-
if (!metadata?.services.
|
|
15430
|
+
if (!metadata?.services.cdn) {
|
|
15168
15431
|
return res.status(404).json({
|
|
15169
|
-
error: "No
|
|
15432
|
+
error: "No CDN infrastructure found for this account and region"
|
|
15170
15433
|
});
|
|
15171
15434
|
}
|
|
15172
|
-
const bucketName = config2.
|
|
15435
|
+
const bucketName = config2.cdnBucketName || `wraps-cdn-${config2.accountId}`;
|
|
15173
15436
|
const { S3Client, ListObjectsV2Command, GetObjectTaggingCommand } = await import("@aws-sdk/client-s3");
|
|
15174
15437
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
15175
|
-
const credentials = config2.
|
|
15176
|
-
config2.
|
|
15438
|
+
const credentials = config2.cdnRoleArn || config2.roleArn ? await assumeRole2(
|
|
15439
|
+
config2.cdnRoleArn || config2.roleArn,
|
|
15177
15440
|
config2.region
|
|
15178
15441
|
) : void 0;
|
|
15179
15442
|
const s3Client = new S3Client({ region: config2.region, credentials });
|
|
@@ -15183,7 +15446,7 @@ function createStorageRouter(config2) {
|
|
|
15183
15446
|
MaxKeys: 100
|
|
15184
15447
|
})
|
|
15185
15448
|
);
|
|
15186
|
-
const cdnUrl = config2.
|
|
15449
|
+
const cdnUrl = config2.cdnDistributionDomain ? `https://${metadata.services.cdn.config.cdn?.customDomain || config2.cdnDistributionDomain}` : null;
|
|
15187
15450
|
const getContentType = (key) => {
|
|
15188
15451
|
const ext = key.split(".").pop()?.toLowerCase();
|
|
15189
15452
|
const mimeTypes = {
|
|
@@ -15246,15 +15509,15 @@ function createStorageRouter(config2) {
|
|
|
15246
15509
|
res.json({
|
|
15247
15510
|
bucketName,
|
|
15248
15511
|
region: config2.region,
|
|
15249
|
-
cdnDomain: config2.
|
|
15250
|
-
customDomain: metadata.services.
|
|
15512
|
+
cdnDomain: config2.cdnDistributionDomain,
|
|
15513
|
+
customDomain: metadata.services.cdn.config.cdn?.customDomain,
|
|
15251
15514
|
files,
|
|
15252
15515
|
totalSize,
|
|
15253
15516
|
fileCount: files.length
|
|
15254
15517
|
});
|
|
15255
15518
|
} catch (error) {
|
|
15256
15519
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
15257
|
-
console.error("[
|
|
15520
|
+
console.error("[CDN] Error fetching files:", error);
|
|
15258
15521
|
res.status(500).json({ error: errorMessage });
|
|
15259
15522
|
}
|
|
15260
15523
|
});
|
|
@@ -15266,16 +15529,16 @@ function createStorageRouter(config2) {
|
|
|
15266
15529
|
config2.accountId || "",
|
|
15267
15530
|
config2.region
|
|
15268
15531
|
);
|
|
15269
|
-
if (!metadata?.services.
|
|
15532
|
+
if (!metadata?.services.cdn) {
|
|
15270
15533
|
return res.status(404).json({
|
|
15271
|
-
error: "No
|
|
15534
|
+
error: "No CDN infrastructure found for this account and region"
|
|
15272
15535
|
});
|
|
15273
15536
|
}
|
|
15274
|
-
const bucketName = config2.
|
|
15537
|
+
const bucketName = config2.cdnBucketName || `wraps-cdn-${config2.accountId}`;
|
|
15275
15538
|
const { CloudWatchClient: CloudWatchClient3, GetMetricStatisticsCommand } = await import("@aws-sdk/client-cloudwatch");
|
|
15276
15539
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
15277
|
-
const credentials = config2.
|
|
15278
|
-
config2.
|
|
15540
|
+
const credentials = config2.cdnRoleArn || config2.roleArn ? await assumeRole2(
|
|
15541
|
+
config2.cdnRoleArn || config2.roleArn,
|
|
15279
15542
|
config2.region
|
|
15280
15543
|
) : void 0;
|
|
15281
15544
|
const cloudWatchClient = new CloudWatchClient3({
|
|
@@ -15317,7 +15580,7 @@ function createStorageRouter(config2) {
|
|
|
15317
15580
|
);
|
|
15318
15581
|
numberOfObjects = objectsResponse.Datapoints?.[0]?.Average || 0;
|
|
15319
15582
|
} catch (err) {
|
|
15320
|
-
console.log("[
|
|
15583
|
+
console.log("[CDN] CloudWatch metrics not available yet");
|
|
15321
15584
|
}
|
|
15322
15585
|
const usage = [];
|
|
15323
15586
|
const bandwidth = [];
|
|
@@ -15353,7 +15616,7 @@ function createStorageRouter(config2) {
|
|
|
15353
15616
|
});
|
|
15354
15617
|
} catch (error) {
|
|
15355
15618
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
15356
|
-
console.error("[
|
|
15619
|
+
console.error("[CDN] Error fetching metrics:", error);
|
|
15357
15620
|
res.status(500).json({ error: errorMessage });
|
|
15358
15621
|
}
|
|
15359
15622
|
});
|
|
@@ -15367,17 +15630,17 @@ function createStorageRouter(config2) {
|
|
|
15367
15630
|
config2.accountId || "",
|
|
15368
15631
|
config2.region
|
|
15369
15632
|
);
|
|
15370
|
-
if (!metadata?.services.
|
|
15633
|
+
if (!metadata?.services.cdn) {
|
|
15371
15634
|
return res.status(404).json({
|
|
15372
|
-
error: "No
|
|
15635
|
+
error: "No CDN infrastructure found for this account and region"
|
|
15373
15636
|
});
|
|
15374
15637
|
}
|
|
15375
|
-
const bucketName = config2.
|
|
15638
|
+
const bucketName = config2.cdnBucketName || `wraps-cdn-${config2.accountId}`;
|
|
15376
15639
|
const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
|
|
15377
15640
|
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
15378
15641
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
15379
|
-
const credentials = config2.
|
|
15380
|
-
config2.
|
|
15642
|
+
const credentials = config2.cdnRoleArn || config2.roleArn ? await assumeRole2(
|
|
15643
|
+
config2.cdnRoleArn || config2.roleArn,
|
|
15381
15644
|
config2.region
|
|
15382
15645
|
) : void 0;
|
|
15383
15646
|
const s3Client = new S3Client({ region: config2.region, credentials });
|
|
@@ -15390,7 +15653,7 @@ function createStorageRouter(config2) {
|
|
|
15390
15653
|
expiresIn: 3600
|
|
15391
15654
|
// 1 hour
|
|
15392
15655
|
});
|
|
15393
|
-
const cdnUrl = metadata.services.
|
|
15656
|
+
const cdnUrl = metadata.services.cdn.config.cdn?.customDomain ? `https://${metadata.services.cdn.config.cdn.customDomain}/${filename}` : config2.cdnDistributionDomain ? `https://${config2.cdnDistributionDomain}/${filename}` : `s3://${bucketName}/${filename}`;
|
|
15394
15657
|
res.json({
|
|
15395
15658
|
uploadUrl,
|
|
15396
15659
|
cdnUrl,
|
|
@@ -15399,7 +15662,7 @@ function createStorageRouter(config2) {
|
|
|
15399
15662
|
});
|
|
15400
15663
|
} catch (error) {
|
|
15401
15664
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
15402
|
-
console.error("[
|
|
15665
|
+
console.error("[CDN] Error generating upload URL:", error);
|
|
15403
15666
|
res.status(500).json({ error: errorMessage });
|
|
15404
15667
|
}
|
|
15405
15668
|
});
|
|
@@ -15417,16 +15680,16 @@ function createStorageRouter(config2) {
|
|
|
15417
15680
|
config2.accountId || "",
|
|
15418
15681
|
config2.region
|
|
15419
15682
|
);
|
|
15420
|
-
if (!metadata?.services.
|
|
15683
|
+
if (!metadata?.services.cdn) {
|
|
15421
15684
|
return res.status(404).json({
|
|
15422
|
-
error: "No
|
|
15685
|
+
error: "No CDN infrastructure found for this account and region"
|
|
15423
15686
|
});
|
|
15424
15687
|
}
|
|
15425
|
-
const bucketName = config2.
|
|
15688
|
+
const bucketName = config2.cdnBucketName || `wraps-cdn-${config2.accountId}`;
|
|
15426
15689
|
const { S3Client, GetObjectTaggingCommand, PutObjectTaggingCommand } = await import("@aws-sdk/client-s3");
|
|
15427
15690
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
15428
|
-
const credentials = config2.
|
|
15429
|
-
config2.
|
|
15691
|
+
const credentials = config2.cdnRoleArn || config2.roleArn ? await assumeRole2(
|
|
15692
|
+
config2.cdnRoleArn || config2.roleArn,
|
|
15430
15693
|
config2.region
|
|
15431
15694
|
) : void 0;
|
|
15432
15695
|
const s3Client = new S3Client({ region: config2.region, credentials });
|
|
@@ -15458,7 +15721,7 @@ function createStorageRouter(config2) {
|
|
|
15458
15721
|
res.json({ success: true, key, starred });
|
|
15459
15722
|
} catch (error) {
|
|
15460
15723
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
15461
|
-
console.error("[
|
|
15724
|
+
console.error("[CDN] Error toggling star:", error);
|
|
15462
15725
|
res.status(500).json({ error: errorMessage });
|
|
15463
15726
|
}
|
|
15464
15727
|
});
|
|
@@ -15472,16 +15735,16 @@ function createStorageRouter(config2) {
|
|
|
15472
15735
|
config2.accountId || "",
|
|
15473
15736
|
config2.region
|
|
15474
15737
|
);
|
|
15475
|
-
if (!metadata?.services.
|
|
15738
|
+
if (!metadata?.services.cdn) {
|
|
15476
15739
|
return res.status(404).json({
|
|
15477
|
-
error: "No
|
|
15740
|
+
error: "No CDN infrastructure found for this account and region"
|
|
15478
15741
|
});
|
|
15479
15742
|
}
|
|
15480
|
-
const bucketName = config2.
|
|
15743
|
+
const bucketName = config2.cdnBucketName || `wraps-cdn-${config2.accountId}`;
|
|
15481
15744
|
const { S3Client, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
|
15482
15745
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
15483
|
-
const credentials = config2.
|
|
15484
|
-
config2.
|
|
15746
|
+
const credentials = config2.cdnRoleArn || config2.roleArn ? await assumeRole2(
|
|
15747
|
+
config2.cdnRoleArn || config2.roleArn,
|
|
15485
15748
|
config2.region
|
|
15486
15749
|
) : void 0;
|
|
15487
15750
|
const s3Client = new S3Client({ region: config2.region, credentials });
|
|
@@ -15494,7 +15757,7 @@ function createStorageRouter(config2) {
|
|
|
15494
15757
|
res.json({ success: true, key });
|
|
15495
15758
|
} catch (error) {
|
|
15496
15759
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
15497
|
-
console.error("[
|
|
15760
|
+
console.error("[CDN] Error deleting file:", error);
|
|
15498
15761
|
res.status(500).json({ error: errorMessage });
|
|
15499
15762
|
}
|
|
15500
15763
|
});
|
|
@@ -15511,16 +15774,16 @@ function createStorageRouter(config2) {
|
|
|
15511
15774
|
config2.accountId || "",
|
|
15512
15775
|
config2.region
|
|
15513
15776
|
);
|
|
15514
|
-
if (!metadata?.services.
|
|
15777
|
+
if (!metadata?.services.cdn) {
|
|
15515
15778
|
return res.status(404).json({
|
|
15516
|
-
error: "No
|
|
15779
|
+
error: "No CDN infrastructure found for this account and region"
|
|
15517
15780
|
});
|
|
15518
15781
|
}
|
|
15519
|
-
const bucketName = config2.
|
|
15782
|
+
const bucketName = config2.cdnBucketName || `wraps-cdn-${config2.accountId}`;
|
|
15520
15783
|
const { S3Client, CopyObjectCommand, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
|
15521
15784
|
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
15522
|
-
const credentials = config2.
|
|
15523
|
-
config2.
|
|
15785
|
+
const credentials = config2.cdnRoleArn || config2.roleArn ? await assumeRole2(
|
|
15786
|
+
config2.cdnRoleArn || config2.roleArn,
|
|
15524
15787
|
config2.region
|
|
15525
15788
|
) : void 0;
|
|
15526
15789
|
const s3Client = new S3Client({ region: config2.region, credentials });
|
|
@@ -15537,11 +15800,11 @@ function createStorageRouter(config2) {
|
|
|
15537
15800
|
Key: oldKey
|
|
15538
15801
|
})
|
|
15539
15802
|
);
|
|
15540
|
-
const cdnUrl = metadata.services.
|
|
15803
|
+
const cdnUrl = metadata.services.cdn.config.cdn?.customDomain ? `https://${metadata.services.cdn.config.cdn.customDomain}/${newKey}` : config2.cdnDistributionDomain ? `https://${config2.cdnDistributionDomain}/${newKey}` : `s3://${bucketName}/${newKey}`;
|
|
15541
15804
|
res.json({ success: true, oldKey, newKey, cdnUrl });
|
|
15542
15805
|
} catch (error) {
|
|
15543
15806
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
15544
|
-
console.error("[
|
|
15807
|
+
console.error("[CDN] Error renaming file:", error);
|
|
15545
15808
|
res.status(500).json({ error: errorMessage });
|
|
15546
15809
|
}
|
|
15547
15810
|
});
|
|
@@ -15641,7 +15904,7 @@ async function startConsoleServer(config2) {
|
|
|
15641
15904
|
app.use((_req, res, next) => {
|
|
15642
15905
|
res.setHeader("X-Frame-Options", "DENY");
|
|
15643
15906
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
15644
|
-
const customDomainSrc = config2.
|
|
15907
|
+
const customDomainSrc = config2.cdnCustomDomain ? ` https://${config2.cdnCustomDomain}` : "";
|
|
15645
15908
|
res.setHeader(
|
|
15646
15909
|
"Content-Security-Policy",
|
|
15647
15910
|
`default-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://*.amazonaws.com https://*.cloudfront.net${customDomainSrc}; connect-src 'self' https://*.amazonaws.com https://*.cloudfront.net${customDomainSrc}`
|
|
@@ -15675,9 +15938,9 @@ async function startConsoleServer(config2) {
|
|
|
15675
15938
|
app.use("/api/user", authenticateToken(authToken), createUserRouter(config2));
|
|
15676
15939
|
app.use("/api/sms", authenticateToken(authToken), createSMSRouter(config2));
|
|
15677
15940
|
app.use(
|
|
15678
|
-
"/api/
|
|
15941
|
+
"/api/cdn",
|
|
15679
15942
|
authenticateToken(authToken),
|
|
15680
|
-
|
|
15943
|
+
createCdnRouter(config2)
|
|
15681
15944
|
);
|
|
15682
15945
|
const staticDir = path2.join(__dirname2, "console");
|
|
15683
15946
|
app.use(express.static(staticDir));
|
|
@@ -15763,15 +16026,15 @@ async function dashboard(options) {
|
|
|
15763
16026
|
const smsPhoneNumberArn = smsStackOutputs.phoneNumberArn?.value;
|
|
15764
16027
|
const smsPhoneNumberType = smsStackOutputs.phoneNumberType?.value;
|
|
15765
16028
|
const smsConfigSetName = smsStackOutputs.configSetName?.value;
|
|
15766
|
-
const
|
|
15767
|
-
const
|
|
15768
|
-
const
|
|
15769
|
-
const
|
|
16029
|
+
const cdnBucketName = storageStackOutputs.bucketName?.value;
|
|
16030
|
+
const cdnDistributionId = storageStackOutputs.distributionId?.value;
|
|
16031
|
+
const cdnDistributionDomain = storageStackOutputs.distributionDomain?.value;
|
|
16032
|
+
const cdnCertificateArn = storageStackOutputs.acmCertificateArn?.value;
|
|
15770
16033
|
let smsProtectEnabled = false;
|
|
15771
16034
|
let smsAllowedCountries;
|
|
15772
16035
|
let smsAitFiltering;
|
|
15773
16036
|
let smsArchiveRetention;
|
|
15774
|
-
let
|
|
16037
|
+
let cdnCustomDomain;
|
|
15775
16038
|
try {
|
|
15776
16039
|
const metadata = await loadConnectionMetadata(identity.accountId, region);
|
|
15777
16040
|
if (metadata?.services?.sms?.config) {
|
|
@@ -15785,8 +16048,8 @@ async function dashboard(options) {
|
|
|
15785
16048
|
smsArchiveRetention = smsConfig.eventTracking.archiveRetention;
|
|
15786
16049
|
}
|
|
15787
16050
|
}
|
|
15788
|
-
if (metadata?.services?.
|
|
15789
|
-
|
|
16051
|
+
if (metadata?.services?.cdn?.config?.cdn?.customDomain) {
|
|
16052
|
+
cdnCustomDomain = metadata.services.cdn.config.cdn.customDomain;
|
|
15790
16053
|
}
|
|
15791
16054
|
} catch {
|
|
15792
16055
|
}
|
|
@@ -15817,13 +16080,13 @@ async function dashboard(options) {
|
|
|
15817
16080
|
smsAitFiltering,
|
|
15818
16081
|
smsArchiveRetention,
|
|
15819
16082
|
// Storage config (don't pass roleArn - use current credentials like email)
|
|
15820
|
-
|
|
15821
|
-
|
|
16083
|
+
cdnBucketName,
|
|
16084
|
+
cdnRoleArn: void 0,
|
|
15822
16085
|
// Use current credentials instead of assuming role
|
|
15823
|
-
|
|
15824
|
-
|
|
15825
|
-
|
|
15826
|
-
|
|
16086
|
+
cdnDistributionId,
|
|
16087
|
+
cdnDistributionDomain,
|
|
16088
|
+
cdnCustomDomain,
|
|
16089
|
+
cdnCertificateArn
|
|
15827
16090
|
});
|
|
15828
16091
|
console.log(`\\n${pc19.bold("Dashboard:")} ${pc19.cyan(url)}`);
|
|
15829
16092
|
console.log(`${pc19.dim("Press Ctrl+C to stop")}\\n`);
|
|
@@ -16014,15 +16277,15 @@ import pc22 from "picocolors";
|
|
|
16014
16277
|
|
|
16015
16278
|
// src/infrastructure/sms-stack.ts
|
|
16016
16279
|
init_esm_shims();
|
|
16017
|
-
import * as
|
|
16280
|
+
import * as aws12 from "@pulumi/aws";
|
|
16018
16281
|
import * as pulumi14 from "@pulumi/pulumi";
|
|
16019
16282
|
async function roleExists2(roleName) {
|
|
16020
16283
|
try {
|
|
16021
16284
|
const { IAMClient: IAMClient3, GetRoleCommand: GetRoleCommand2 } = await import("@aws-sdk/client-iam");
|
|
16022
|
-
const
|
|
16285
|
+
const iam8 = new IAMClient3({
|
|
16023
16286
|
region: process.env.AWS_REGION || "us-east-1"
|
|
16024
16287
|
});
|
|
16025
|
-
await
|
|
16288
|
+
await iam8.send(new GetRoleCommand2({ RoleName: roleName }));
|
|
16026
16289
|
return true;
|
|
16027
16290
|
} catch (error) {
|
|
16028
16291
|
if (error.name === "NoSuchEntityException" || error.Code === "NoSuchEntity" || error.Error?.Code === "NoSuchEntity") {
|
|
@@ -16092,7 +16355,7 @@ async function createSMSIAMRole(config2) {
|
|
|
16092
16355
|
}
|
|
16093
16356
|
const roleName = "wraps-sms-role";
|
|
16094
16357
|
const exists = await roleExists2(roleName);
|
|
16095
|
-
const role = exists ? new
|
|
16358
|
+
const role = exists ? new aws12.iam.Role(
|
|
16096
16359
|
roleName,
|
|
16097
16360
|
{
|
|
16098
16361
|
name: roleName,
|
|
@@ -16107,7 +16370,7 @@ async function createSMSIAMRole(config2) {
|
|
|
16107
16370
|
import: roleName,
|
|
16108
16371
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
16109
16372
|
}
|
|
16110
|
-
) : new
|
|
16373
|
+
) : new aws12.iam.Role(
|
|
16111
16374
|
roleName,
|
|
16112
16375
|
{
|
|
16113
16376
|
name: roleName,
|
|
@@ -16202,7 +16465,7 @@ async function createSMSIAMRole(config2) {
|
|
|
16202
16465
|
Resource: "arn:aws:logs:*:*:log-group:/aws/lambda/wraps-sms-*"
|
|
16203
16466
|
});
|
|
16204
16467
|
}
|
|
16205
|
-
new
|
|
16468
|
+
new aws12.iam.RolePolicy("wraps-sms-policy", {
|
|
16206
16469
|
role: role.name,
|
|
16207
16470
|
policy: JSON.stringify({
|
|
16208
16471
|
Version: "2012-10-17",
|
|
@@ -16212,7 +16475,7 @@ async function createSMSIAMRole(config2) {
|
|
|
16212
16475
|
return role;
|
|
16213
16476
|
}
|
|
16214
16477
|
function createSMSConfigurationSet() {
|
|
16215
|
-
return new
|
|
16478
|
+
return new aws12.pinpoint.Smsvoicev2ConfigurationSet("wraps-sms-config", {
|
|
16216
16479
|
name: "wraps-sms-config",
|
|
16217
16480
|
defaultMessageType: "TRANSACTIONAL",
|
|
16218
16481
|
tags: {
|
|
@@ -16222,7 +16485,7 @@ function createSMSConfigurationSet() {
|
|
|
16222
16485
|
});
|
|
16223
16486
|
}
|
|
16224
16487
|
function createSMSOptOutList() {
|
|
16225
|
-
return new
|
|
16488
|
+
return new aws12.pinpoint.Smsvoicev2OptOutList("wraps-sms-optouts", {
|
|
16226
16489
|
name: "wraps-sms-optouts",
|
|
16227
16490
|
tags: {
|
|
16228
16491
|
ManagedBy: "wraps-cli",
|
|
@@ -16286,7 +16549,7 @@ async function createSMSPhoneNumber(phoneNumberType, optOutList) {
|
|
|
16286
16549
|
}
|
|
16287
16550
|
};
|
|
16288
16551
|
if (existingArn) {
|
|
16289
|
-
return new
|
|
16552
|
+
return new aws12.pinpoint.Smsvoicev2PhoneNumber(
|
|
16290
16553
|
"wraps-sms-number",
|
|
16291
16554
|
phoneConfig,
|
|
16292
16555
|
{
|
|
@@ -16295,7 +16558,7 @@ async function createSMSPhoneNumber(phoneNumberType, optOutList) {
|
|
|
16295
16558
|
}
|
|
16296
16559
|
);
|
|
16297
16560
|
}
|
|
16298
|
-
return new
|
|
16561
|
+
return new aws12.pinpoint.Smsvoicev2PhoneNumber(
|
|
16299
16562
|
"wraps-sms-number",
|
|
16300
16563
|
phoneConfig,
|
|
16301
16564
|
{
|
|
@@ -16338,10 +16601,10 @@ async function createSMSSQSResources() {
|
|
|
16338
16601
|
Description: "Dead letter queue for failed SMS event processing"
|
|
16339
16602
|
}
|
|
16340
16603
|
};
|
|
16341
|
-
const dlq = dlqUrl ? new
|
|
16604
|
+
const dlq = dlqUrl ? new aws12.sqs.Queue(dlqName, dlqConfig, {
|
|
16342
16605
|
import: dlqUrl,
|
|
16343
16606
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
16344
|
-
}) : new
|
|
16607
|
+
}) : new aws12.sqs.Queue(dlqName, dlqConfig, {
|
|
16345
16608
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
16346
16609
|
});
|
|
16347
16610
|
const queueConfig = {
|
|
@@ -16364,10 +16627,10 @@ async function createSMSSQSResources() {
|
|
|
16364
16627
|
Description: "Queue for SMS events from SNS"
|
|
16365
16628
|
}
|
|
16366
16629
|
};
|
|
16367
|
-
const queue = queueUrl ? new
|
|
16630
|
+
const queue = queueUrl ? new aws12.sqs.Queue(queueName, queueConfig, {
|
|
16368
16631
|
import: queueUrl,
|
|
16369
16632
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
16370
|
-
}) : new
|
|
16633
|
+
}) : new aws12.sqs.Queue(queueName, queueConfig, {
|
|
16371
16634
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
16372
16635
|
});
|
|
16373
16636
|
return { queue, dlq };
|
|
@@ -16407,13 +16670,13 @@ async function createSMSSNSResources(config2) {
|
|
|
16407
16670
|
Description: "SNS topic for SMS delivery events"
|
|
16408
16671
|
}
|
|
16409
16672
|
};
|
|
16410
|
-
const topic = topicArn ? new
|
|
16673
|
+
const topic = topicArn ? new aws12.sns.Topic("wraps-sms-events-topic", topicConfig, {
|
|
16411
16674
|
import: topicArn,
|
|
16412
16675
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
16413
|
-
}) : new
|
|
16676
|
+
}) : new aws12.sns.Topic("wraps-sms-events-topic", topicConfig, {
|
|
16414
16677
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
16415
16678
|
});
|
|
16416
|
-
new
|
|
16679
|
+
new aws12.sns.TopicPolicy("wraps-sms-events-topic-policy", {
|
|
16417
16680
|
arn: topic.arn,
|
|
16418
16681
|
policy: topic.arn.apply(
|
|
16419
16682
|
(topicArn2) => JSON.stringify({
|
|
@@ -16430,7 +16693,7 @@ async function createSMSSNSResources(config2) {
|
|
|
16430
16693
|
})
|
|
16431
16694
|
)
|
|
16432
16695
|
});
|
|
16433
|
-
new
|
|
16696
|
+
new aws12.sqs.QueuePolicy("wraps-sms-events-queue-policy", {
|
|
16434
16697
|
queueUrl: config2.queueUrl,
|
|
16435
16698
|
policy: pulumi14.all([config2.queueArn, topic.arn]).apply(
|
|
16436
16699
|
([queueArn, topicArn2]) => JSON.stringify({
|
|
@@ -16449,7 +16712,7 @@ async function createSMSSNSResources(config2) {
|
|
|
16449
16712
|
})
|
|
16450
16713
|
)
|
|
16451
16714
|
});
|
|
16452
|
-
const subscription = new
|
|
16715
|
+
const subscription = new aws12.sns.TopicSubscription(
|
|
16453
16716
|
"wraps-sms-events-subscription",
|
|
16454
16717
|
{
|
|
16455
16718
|
topic: topic.arn,
|
|
@@ -16498,17 +16761,17 @@ async function createSMSDynamoDBTable() {
|
|
|
16498
16761
|
Service: "sms"
|
|
16499
16762
|
}
|
|
16500
16763
|
};
|
|
16501
|
-
return exists ? new
|
|
16764
|
+
return exists ? new aws12.dynamodb.Table(tableName, tableConfig, {
|
|
16502
16765
|
import: tableName,
|
|
16503
16766
|
customTimeouts: { create: "5m", update: "5m", delete: "5m" }
|
|
16504
|
-
}) : new
|
|
16767
|
+
}) : new aws12.dynamodb.Table(tableName, tableConfig, {
|
|
16505
16768
|
customTimeouts: { create: "5m", update: "5m", delete: "5m" }
|
|
16506
16769
|
});
|
|
16507
16770
|
}
|
|
16508
16771
|
async function deploySMSLambdaFunction(config2) {
|
|
16509
16772
|
const { getLambdaCode: getLambdaCode2 } = await Promise.resolve().then(() => (init_lambda(), lambda_exports));
|
|
16510
16773
|
const codeDir = await getLambdaCode2("sms-event-processor");
|
|
16511
|
-
const lambdaRole = new
|
|
16774
|
+
const lambdaRole = new aws12.iam.Role("wraps-sms-lambda-role", {
|
|
16512
16775
|
name: "wraps-sms-lambda-role",
|
|
16513
16776
|
assumeRolePolicy: JSON.stringify({
|
|
16514
16777
|
Version: "2012-10-17",
|
|
@@ -16525,11 +16788,11 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
16525
16788
|
Service: "sms"
|
|
16526
16789
|
}
|
|
16527
16790
|
});
|
|
16528
|
-
new
|
|
16791
|
+
new aws12.iam.RolePolicyAttachment("wraps-sms-lambda-basic-execution", {
|
|
16529
16792
|
role: lambdaRole.name,
|
|
16530
16793
|
policyArn: "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
16531
16794
|
});
|
|
16532
|
-
new
|
|
16795
|
+
new aws12.iam.RolePolicy("wraps-sms-lambda-policy", {
|
|
16533
16796
|
role: lambdaRole.name,
|
|
16534
16797
|
policy: pulumi14.all([config2.tableName, config2.queueArn]).apply(
|
|
16535
16798
|
([tableName, queueArn]) => JSON.stringify({
|
|
@@ -16561,7 +16824,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
16561
16824
|
})
|
|
16562
16825
|
)
|
|
16563
16826
|
});
|
|
16564
|
-
const eventProcessor = new
|
|
16827
|
+
const eventProcessor = new aws12.lambda.Function(
|
|
16565
16828
|
"wraps-sms-event-processor",
|
|
16566
16829
|
{
|
|
16567
16830
|
name: "wraps-sms-event-processor",
|
|
@@ -16588,7 +16851,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
16588
16851
|
customTimeouts: { create: "5m", update: "5m", delete: "2m" }
|
|
16589
16852
|
}
|
|
16590
16853
|
);
|
|
16591
|
-
new
|
|
16854
|
+
new aws12.lambda.EventSourceMapping(
|
|
16592
16855
|
"wraps-sms-event-source-mapping",
|
|
16593
16856
|
{
|
|
16594
16857
|
eventSourceArn: config2.queueArn,
|
|
@@ -16604,7 +16867,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
16604
16867
|
return eventProcessor;
|
|
16605
16868
|
}
|
|
16606
16869
|
async function deploySMSStack(config2) {
|
|
16607
|
-
const identity = await
|
|
16870
|
+
const identity = await aws12.getCallerIdentity();
|
|
16608
16871
|
const accountId = identity.accountId;
|
|
16609
16872
|
let oidcProvider;
|
|
16610
16873
|
if (config2.provider === "vercel" && config2.vercel) {
|
|
@@ -19843,7 +20106,7 @@ Run ${pc29.cyan(`wraps sms verify-number --phone-number ${phoneNumber} --resend`
|
|
|
19843
20106
|
}
|
|
19844
20107
|
}
|
|
19845
20108
|
|
|
19846
|
-
// src/commands/
|
|
20109
|
+
// src/commands/cdn/destroy.ts
|
|
19847
20110
|
init_esm_shims();
|
|
19848
20111
|
init_events();
|
|
19849
20112
|
init_route53();
|
|
@@ -19854,11 +20117,11 @@ init_metadata();
|
|
|
19854
20117
|
import * as clack28 from "@clack/prompts";
|
|
19855
20118
|
import * as pulumi20 from "@pulumi/pulumi";
|
|
19856
20119
|
import pc30 from "picocolors";
|
|
19857
|
-
async function
|
|
20120
|
+
async function cdnDestroy(options) {
|
|
19858
20121
|
const startTime = Date.now();
|
|
19859
20122
|
clack28.intro(
|
|
19860
20123
|
pc30.bold(
|
|
19861
|
-
options.preview ? "
|
|
20124
|
+
options.preview ? "CDN Infrastructure Destruction Preview" : "CDN Infrastructure Teardown"
|
|
19862
20125
|
)
|
|
19863
20126
|
);
|
|
19864
20127
|
const progress = new DeploymentProgress();
|
|
@@ -19868,16 +20131,16 @@ async function storageDestroy(options) {
|
|
|
19868
20131
|
);
|
|
19869
20132
|
let region = options.region || await getAWSRegion();
|
|
19870
20133
|
if (!(options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)) {
|
|
19871
|
-
const
|
|
20134
|
+
const cdnConnections = await findConnectionsWithService(
|
|
19872
20135
|
identity.accountId,
|
|
19873
|
-
"
|
|
20136
|
+
"cdn"
|
|
19874
20137
|
);
|
|
19875
|
-
if (
|
|
19876
|
-
region =
|
|
19877
|
-
} else if (
|
|
20138
|
+
if (cdnConnections.length === 1) {
|
|
20139
|
+
region = cdnConnections[0].region;
|
|
20140
|
+
} else if (cdnConnections.length > 1) {
|
|
19878
20141
|
const selectedRegion = await clack28.select({
|
|
19879
|
-
message: "Multiple
|
|
19880
|
-
options:
|
|
20142
|
+
message: "Multiple CDN deployments found. Which region to destroy?",
|
|
20143
|
+
options: cdnConnections.map((conn) => ({
|
|
19881
20144
|
value: conn.region,
|
|
19882
20145
|
label: conn.region
|
|
19883
20146
|
}))
|
|
@@ -19890,17 +20153,17 @@ async function storageDestroy(options) {
|
|
|
19890
20153
|
}
|
|
19891
20154
|
}
|
|
19892
20155
|
const metadata = await loadConnectionMetadata(identity.accountId, region);
|
|
19893
|
-
const
|
|
19894
|
-
const
|
|
19895
|
-
const customDomain =
|
|
19896
|
-
const storedStackName =
|
|
20156
|
+
const cdnService = metadata?.services?.cdn;
|
|
20157
|
+
const cdnConfig = cdnService?.config;
|
|
20158
|
+
const customDomain = cdnConfig?.cdn?.customDomain;
|
|
20159
|
+
const storedStackName = cdnService?.pulumiStackName;
|
|
19897
20160
|
if (!(options.force || options.preview)) {
|
|
19898
20161
|
clack28.log.warn(
|
|
19899
20162
|
pc30.yellow("This will delete your S3 bucket and all files in it!")
|
|
19900
20163
|
);
|
|
19901
20164
|
const confirmed = await clack28.confirm({
|
|
19902
20165
|
message: pc30.red(
|
|
19903
|
-
"Are you sure you want to destroy all
|
|
20166
|
+
"Are you sure you want to destroy all CDN infrastructure?"
|
|
19904
20167
|
),
|
|
19905
20168
|
initialValue: false
|
|
19906
20169
|
});
|
|
@@ -19935,7 +20198,7 @@ async function storageDestroy(options) {
|
|
|
19935
20198
|
"Generating destruction preview",
|
|
19936
20199
|
async () => {
|
|
19937
20200
|
await ensurePulumiWorkDir();
|
|
19938
|
-
const stackName = storedStackName || `wraps-
|
|
20201
|
+
const stackName = storedStackName || `wraps-cdn-${identity.accountId}-${region}`;
|
|
19939
20202
|
let stack;
|
|
19940
20203
|
try {
|
|
19941
20204
|
stack = await pulumi20.automation.LocalWorkspace.selectStack({
|
|
@@ -19943,7 +20206,7 @@ async function storageDestroy(options) {
|
|
|
19943
20206
|
workDir: getPulumiWorkDir()
|
|
19944
20207
|
});
|
|
19945
20208
|
} catch (_error) {
|
|
19946
|
-
throw new Error("No
|
|
20209
|
+
throw new Error("No CDN infrastructure found to preview");
|
|
19947
20210
|
}
|
|
19948
20211
|
const result = await previewWithResourceChanges(stack, {
|
|
19949
20212
|
diff: true
|
|
@@ -19955,7 +20218,7 @@ async function storageDestroy(options) {
|
|
|
19955
20218
|
changeSummary: previewResult.changeSummary,
|
|
19956
20219
|
resourceChanges: previewResult.resourceChanges,
|
|
19957
20220
|
costEstimate: "Monthly cost after destruction: $0.00",
|
|
19958
|
-
commandName: "wraps
|
|
20221
|
+
commandName: "wraps cdn destroy"
|
|
19959
20222
|
});
|
|
19960
20223
|
if (customDomain) {
|
|
19961
20224
|
const previewHostedZone = await findHostedZone(customDomain, region);
|
|
@@ -19968,7 +20231,7 @@ async function storageDestroy(options) {
|
|
|
19968
20231
|
clack28.outro(
|
|
19969
20232
|
pc30.green("Preview complete. Run without --preview to destroy.")
|
|
19970
20233
|
);
|
|
19971
|
-
trackServiceRemoved("
|
|
20234
|
+
trackServiceRemoved("cdn", {
|
|
19972
20235
|
preview: true,
|
|
19973
20236
|
region,
|
|
19974
20237
|
duration_ms: Date.now() - startTime
|
|
@@ -19976,8 +20239,8 @@ async function storageDestroy(options) {
|
|
|
19976
20239
|
return;
|
|
19977
20240
|
} catch (error) {
|
|
19978
20241
|
progress.stop();
|
|
19979
|
-
if (error.message.includes("No
|
|
19980
|
-
clack28.log.warn("No
|
|
20242
|
+
if (error.message.includes("No CDN infrastructure found")) {
|
|
20243
|
+
clack28.log.warn("No CDN infrastructure found to preview");
|
|
19981
20244
|
process.exit(0);
|
|
19982
20245
|
}
|
|
19983
20246
|
trackError("PREVIEW_FAILED", "storage destroy", { step: "preview" });
|
|
@@ -19989,7 +20252,7 @@ async function storageDestroy(options) {
|
|
|
19989
20252
|
await progress.execute(
|
|
19990
20253
|
`Deleting DNS records for ${customDomain}`,
|
|
19991
20254
|
async () => {
|
|
19992
|
-
await
|
|
20255
|
+
await deleteCdnDNSRecords(hostedZone.id, customDomain);
|
|
19993
20256
|
}
|
|
19994
20257
|
);
|
|
19995
20258
|
} catch (error) {
|
|
@@ -19997,7 +20260,7 @@ async function storageDestroy(options) {
|
|
|
19997
20260
|
clack28.log.info("You may need to delete them manually from Route53");
|
|
19998
20261
|
}
|
|
19999
20262
|
}
|
|
20000
|
-
const bucketName = `wraps-
|
|
20263
|
+
const bucketName = `wraps-cdn-${identity.accountId}`;
|
|
20001
20264
|
try {
|
|
20002
20265
|
await progress.execute(
|
|
20003
20266
|
"Emptying S3 bucket (this may take a while for large buckets)",
|
|
@@ -20010,10 +20273,10 @@ async function storageDestroy(options) {
|
|
|
20010
20273
|
}
|
|
20011
20274
|
try {
|
|
20012
20275
|
await progress.execute(
|
|
20013
|
-
"Destroying
|
|
20276
|
+
"Destroying CDN infrastructure (this may take 2-3 minutes)",
|
|
20014
20277
|
async () => {
|
|
20015
20278
|
await ensurePulumiWorkDir();
|
|
20016
|
-
const stackName = storedStackName || `wraps-
|
|
20279
|
+
const stackName = storedStackName || `wraps-cdn-${identity.accountId}-${region}`;
|
|
20017
20280
|
let stack;
|
|
20018
20281
|
try {
|
|
20019
20282
|
stack = await pulumi20.automation.LocalWorkspace.selectStack({
|
|
@@ -20021,7 +20284,7 @@ async function storageDestroy(options) {
|
|
|
20021
20284
|
workDir: getPulumiWorkDir()
|
|
20022
20285
|
});
|
|
20023
20286
|
} catch (_error) {
|
|
20024
|
-
throw new Error("No
|
|
20287
|
+
throw new Error("No CDN infrastructure found to destroy");
|
|
20025
20288
|
}
|
|
20026
20289
|
await stack.destroy({ onOutput: () => {
|
|
20027
20290
|
} });
|
|
@@ -20030,10 +20293,10 @@ async function storageDestroy(options) {
|
|
|
20030
20293
|
);
|
|
20031
20294
|
} catch (error) {
|
|
20032
20295
|
progress.stop();
|
|
20033
|
-
if (error.message.includes("No
|
|
20034
|
-
clack28.log.warn("No
|
|
20296
|
+
if (error.message.includes("No CDN infrastructure found")) {
|
|
20297
|
+
clack28.log.warn("No CDN infrastructure found");
|
|
20035
20298
|
if (metadata) {
|
|
20036
|
-
removeServiceFromConnection(metadata, "
|
|
20299
|
+
removeServiceFromConnection(metadata, "cdn");
|
|
20037
20300
|
await saveConnectionMetadata(metadata);
|
|
20038
20301
|
}
|
|
20039
20302
|
process.exit(0);
|
|
@@ -20043,11 +20306,11 @@ async function storageDestroy(options) {
|
|
|
20043
20306
|
throw errors.stackLocked();
|
|
20044
20307
|
}
|
|
20045
20308
|
trackError("DESTROY_FAILED", "storage destroy", { step: "destroy" });
|
|
20046
|
-
clack28.log.error("
|
|
20309
|
+
clack28.log.error("CDN infrastructure destruction failed");
|
|
20047
20310
|
throw error;
|
|
20048
20311
|
}
|
|
20049
20312
|
if (metadata) {
|
|
20050
|
-
removeServiceFromConnection(metadata, "
|
|
20313
|
+
removeServiceFromConnection(metadata, "cdn");
|
|
20051
20314
|
const hasOtherServices = Object.keys(metadata.services).length > 0;
|
|
20052
20315
|
if (hasOtherServices) {
|
|
20053
20316
|
await saveConnectionMetadata(metadata);
|
|
@@ -20061,7 +20324,7 @@ async function storageDestroy(options) {
|
|
|
20061
20324
|
if (shouldCleanDNS && hostedZone) {
|
|
20062
20325
|
deletedItems.push("Route53 DNS records");
|
|
20063
20326
|
}
|
|
20064
|
-
clack28.outro(pc30.green("
|
|
20327
|
+
clack28.outro(pc30.green("CDN infrastructure has been removed"));
|
|
20065
20328
|
console.log(`
|
|
20066
20329
|
${pc30.bold("Cleaned up:")}`);
|
|
20067
20330
|
for (const item of deletedItems) {
|
|
@@ -20069,10 +20332,10 @@ ${pc30.bold("Cleaned up:")}`);
|
|
|
20069
20332
|
}
|
|
20070
20333
|
console.log(
|
|
20071
20334
|
`
|
|
20072
|
-
Run ${pc30.cyan("wraps
|
|
20335
|
+
Run ${pc30.cyan("wraps cdn init")} to deploy CDN infrastructure again.
|
|
20073
20336
|
`
|
|
20074
20337
|
);
|
|
20075
|
-
trackServiceRemoved("
|
|
20338
|
+
trackServiceRemoved("cdn", {
|
|
20076
20339
|
reason: "user_initiated",
|
|
20077
20340
|
region,
|
|
20078
20341
|
duration_ms: Date.now() - startTime,
|
|
@@ -20111,7 +20374,7 @@ async function emptyS3Bucket(bucketName, region) {
|
|
|
20111
20374
|
versionIdMarker = listResponse.NextVersionIdMarker;
|
|
20112
20375
|
} while (keyMarker);
|
|
20113
20376
|
}
|
|
20114
|
-
async function
|
|
20377
|
+
async function deleteCdnDNSRecords(hostedZoneId, customDomain) {
|
|
20115
20378
|
const {
|
|
20116
20379
|
Route53Client: Route53Client2,
|
|
20117
20380
|
ListResourceRecordSetsCommand: ListResourceRecordSetsCommand2,
|
|
@@ -20144,20 +20407,20 @@ async function deleteStorageDNSRecords(hostedZoneId, customDomain) {
|
|
|
20144
20407
|
);
|
|
20145
20408
|
}
|
|
20146
20409
|
|
|
20147
|
-
// src/commands/
|
|
20410
|
+
// src/commands/cdn/init.ts
|
|
20148
20411
|
init_esm_shims();
|
|
20149
20412
|
import * as clack29 from "@clack/prompts";
|
|
20150
20413
|
import * as pulumi23 from "@pulumi/pulumi";
|
|
20151
20414
|
import pc31 from "picocolors";
|
|
20152
20415
|
|
|
20153
|
-
// src/infrastructure/
|
|
20416
|
+
// src/infrastructure/cdn-stack.ts
|
|
20154
20417
|
init_esm_shims();
|
|
20155
|
-
import * as
|
|
20418
|
+
import * as aws14 from "@pulumi/aws";
|
|
20156
20419
|
import * as pulumi22 from "@pulumi/pulumi";
|
|
20157
20420
|
|
|
20158
|
-
// src/infrastructure/resources/s3-
|
|
20421
|
+
// src/infrastructure/resources/s3-cdn.ts
|
|
20159
20422
|
init_esm_shims();
|
|
20160
|
-
import * as
|
|
20423
|
+
import * as aws13 from "@pulumi/aws";
|
|
20161
20424
|
import * as pulumi21 from "@pulumi/pulumi";
|
|
20162
20425
|
function retentionToDays(retention) {
|
|
20163
20426
|
switch (retention) {
|
|
@@ -20177,25 +20440,25 @@ function retentionToDays(retention) {
|
|
|
20177
20440
|
return null;
|
|
20178
20441
|
}
|
|
20179
20442
|
}
|
|
20180
|
-
async function
|
|
20181
|
-
const bucketName = config2.
|
|
20182
|
-
const bucket = new
|
|
20443
|
+
async function createCdnBucket(config2) {
|
|
20444
|
+
const bucketName = config2.cdnConfig.bucketName || `wraps-cdn-${config2.accountId}`;
|
|
20445
|
+
const bucket = new aws13.s3.BucketV2("wraps-cdn-bucket", {
|
|
20183
20446
|
bucket: bucketName,
|
|
20184
20447
|
tags: {
|
|
20185
20448
|
ManagedBy: "wraps-cli",
|
|
20186
|
-
Service: "
|
|
20449
|
+
Service: "cdn"
|
|
20187
20450
|
}
|
|
20188
20451
|
});
|
|
20189
|
-
if (config2.
|
|
20190
|
-
new
|
|
20452
|
+
if (config2.cdnConfig.versioning) {
|
|
20453
|
+
new aws13.s3.BucketVersioningV2("wraps-cdn-versioning", {
|
|
20191
20454
|
bucket: bucket.id,
|
|
20192
20455
|
versioningConfiguration: {
|
|
20193
20456
|
status: "Enabled"
|
|
20194
20457
|
}
|
|
20195
20458
|
});
|
|
20196
20459
|
}
|
|
20197
|
-
new
|
|
20198
|
-
"wraps-
|
|
20460
|
+
new aws13.s3.BucketServerSideEncryptionConfigurationV2(
|
|
20461
|
+
"wraps-cdn-encryption",
|
|
20199
20462
|
{
|
|
20200
20463
|
bucket: bucket.id,
|
|
20201
20464
|
rules: [
|
|
@@ -20218,9 +20481,9 @@ async function createStorageBucket(config2) {
|
|
|
20218
20481
|
"http://localhost:5555",
|
|
20219
20482
|
"http://localhost:5556",
|
|
20220
20483
|
"http://localhost:8080",
|
|
20221
|
-
...config2.
|
|
20484
|
+
...config2.cdnConfig.additionalOrigins || []
|
|
20222
20485
|
];
|
|
20223
|
-
new
|
|
20486
|
+
new aws13.s3.BucketCorsConfigurationV2("wraps-cdn-cors", {
|
|
20224
20487
|
bucket: bucket.id,
|
|
20225
20488
|
corsRules: [
|
|
20226
20489
|
{
|
|
@@ -20232,9 +20495,9 @@ async function createStorageBucket(config2) {
|
|
|
20232
20495
|
}
|
|
20233
20496
|
]
|
|
20234
20497
|
});
|
|
20235
|
-
const retentionDays = config2.
|
|
20498
|
+
const retentionDays = config2.cdnConfig.retention ? retentionToDays(config2.cdnConfig.retention) : null;
|
|
20236
20499
|
if (retentionDays) {
|
|
20237
|
-
new
|
|
20500
|
+
new aws13.s3.BucketLifecycleConfigurationV2("wraps-cdn-lifecycle", {
|
|
20238
20501
|
bucket: bucket.id,
|
|
20239
20502
|
rules: [
|
|
20240
20503
|
{
|
|
@@ -20247,7 +20510,7 @@ async function createStorageBucket(config2) {
|
|
|
20247
20510
|
]
|
|
20248
20511
|
});
|
|
20249
20512
|
}
|
|
20250
|
-
new
|
|
20513
|
+
new aws13.s3.BucketPublicAccessBlock("wraps-cdn-public-access", {
|
|
20251
20514
|
bucket: bucket.id,
|
|
20252
20515
|
blockPublicAcls: true,
|
|
20253
20516
|
blockPublicPolicy: true,
|
|
@@ -20260,16 +20523,16 @@ async function createStorageBucket(config2) {
|
|
|
20260
20523
|
bucketArn: bucket.arn
|
|
20261
20524
|
};
|
|
20262
20525
|
}
|
|
20263
|
-
async function
|
|
20264
|
-
const usEast1Provider = new
|
|
20526
|
+
async function createCdnWAF() {
|
|
20527
|
+
const usEast1Provider = new aws13.Provider("storage-waf-us-east-1", {
|
|
20265
20528
|
region: "us-east-1"
|
|
20266
20529
|
});
|
|
20267
|
-
const webAcl = new
|
|
20268
|
-
"wraps-
|
|
20530
|
+
const webAcl = new aws13.wafv2.WebAcl(
|
|
20531
|
+
"wraps-cdn-waf",
|
|
20269
20532
|
{
|
|
20270
20533
|
scope: "CLOUDFRONT",
|
|
20271
20534
|
// WAF for CloudFront must use CLOUDFRONT scope
|
|
20272
|
-
description: "Rate limiting protection for Wraps
|
|
20535
|
+
description: "Rate limiting protection for Wraps CDN",
|
|
20273
20536
|
defaultAction: {
|
|
20274
20537
|
allow: {}
|
|
20275
20538
|
// Allow by default
|
|
@@ -20292,20 +20555,20 @@ async function createStorageWAF() {
|
|
|
20292
20555
|
visibilityConfig: {
|
|
20293
20556
|
sampledRequestsEnabled: true,
|
|
20294
20557
|
cloudwatchMetricsEnabled: true,
|
|
20295
|
-
metricName: "
|
|
20558
|
+
metricName: "CdnRateLimitRule"
|
|
20296
20559
|
}
|
|
20297
20560
|
}
|
|
20298
20561
|
],
|
|
20299
20562
|
visibilityConfig: {
|
|
20300
20563
|
sampledRequestsEnabled: true,
|
|
20301
20564
|
cloudwatchMetricsEnabled: true,
|
|
20302
|
-
metricName: "wraps-
|
|
20565
|
+
metricName: "wraps-cdn-waf"
|
|
20303
20566
|
},
|
|
20304
20567
|
tags: {
|
|
20305
|
-
Name: "wraps-
|
|
20568
|
+
Name: "wraps-cdn-waf",
|
|
20306
20569
|
ManagedBy: "wraps-cli",
|
|
20307
|
-
Service: "
|
|
20308
|
-
Description: "WAF for Wraps
|
|
20570
|
+
Service: "cdn",
|
|
20571
|
+
Description: "WAF for Wraps CDN with rate limiting"
|
|
20309
20572
|
}
|
|
20310
20573
|
},
|
|
20311
20574
|
{
|
|
@@ -20314,11 +20577,11 @@ async function createStorageWAF() {
|
|
|
20314
20577
|
);
|
|
20315
20578
|
return webAcl;
|
|
20316
20579
|
}
|
|
20317
|
-
async function
|
|
20318
|
-
const webAcl = config2.wafEnabled ? await
|
|
20319
|
-
const oac = new
|
|
20320
|
-
name: "wraps-
|
|
20321
|
-
description: "OAC for Wraps
|
|
20580
|
+
async function createCdnDistribution(config2) {
|
|
20581
|
+
const webAcl = config2.wafEnabled ? await createCdnWAF() : void 0;
|
|
20582
|
+
const oac = new aws13.cloudfront.OriginAccessControl("wraps-cdn-oac", {
|
|
20583
|
+
name: "wraps-cdn-oac",
|
|
20584
|
+
description: "OAC for Wraps CDN S3 bucket",
|
|
20322
20585
|
originAccessControlOriginType: "s3",
|
|
20323
20586
|
signingBehavior: "always",
|
|
20324
20587
|
signingProtocol: "sigv4"
|
|
@@ -20333,7 +20596,7 @@ async function createStorageCDN(config2) {
|
|
|
20333
20596
|
};
|
|
20334
20597
|
const originConfig = {
|
|
20335
20598
|
domainName: config2.bucket.bucketRegionalDomainName,
|
|
20336
|
-
originId: "s3-
|
|
20599
|
+
originId: "s3-cdn",
|
|
20337
20600
|
originAccessControlId: oac.id
|
|
20338
20601
|
};
|
|
20339
20602
|
if (config2.originShield) {
|
|
@@ -20351,9 +20614,9 @@ async function createStorageCDN(config2) {
|
|
|
20351
20614
|
} : {
|
|
20352
20615
|
restrictionType: "none"
|
|
20353
20616
|
};
|
|
20354
|
-
const distribution = new
|
|
20617
|
+
const distribution = new aws13.cloudfront.Distribution("wraps-cdn-cdn", {
|
|
20355
20618
|
enabled: true,
|
|
20356
|
-
comment: "Wraps
|
|
20619
|
+
comment: "Wraps CDN",
|
|
20357
20620
|
aliases,
|
|
20358
20621
|
// Attach WAF Web ACL for rate limiting protection (only if enabled)
|
|
20359
20622
|
webAclId: webAcl?.arn,
|
|
@@ -20361,7 +20624,7 @@ async function createStorageCDN(config2) {
|
|
|
20361
20624
|
origins: [originConfig],
|
|
20362
20625
|
// Default cache behavior for static assets
|
|
20363
20626
|
defaultCacheBehavior: {
|
|
20364
|
-
targetOriginId: "s3-
|
|
20627
|
+
targetOriginId: "s3-cdn",
|
|
20365
20628
|
viewerProtocolPolicy: "redirect-to-https",
|
|
20366
20629
|
// Allow GET, HEAD, OPTIONS only (uploads go direct to S3 via presigned URLs)
|
|
20367
20630
|
allowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
@@ -20389,12 +20652,12 @@ async function createStorageCDN(config2) {
|
|
|
20389
20652
|
// SSL certificate
|
|
20390
20653
|
viewerCertificate,
|
|
20391
20654
|
tags: {
|
|
20392
|
-
Name: "wraps-
|
|
20655
|
+
Name: "wraps-cdn",
|
|
20393
20656
|
ManagedBy: "wraps-cli",
|
|
20394
|
-
Service: "
|
|
20657
|
+
Service: "cdn"
|
|
20395
20658
|
}
|
|
20396
20659
|
});
|
|
20397
|
-
new
|
|
20660
|
+
new aws13.s3.BucketPolicy("wraps-cdn-bucket-policy", {
|
|
20398
20661
|
bucket: config2.bucket.id,
|
|
20399
20662
|
policy: pulumi21.interpolate`{
|
|
20400
20663
|
"Version": "2012-10-17",
|
|
@@ -20422,19 +20685,19 @@ async function createStorageCDN(config2) {
|
|
|
20422
20685
|
webAcl
|
|
20423
20686
|
};
|
|
20424
20687
|
}
|
|
20425
|
-
async function
|
|
20426
|
-
const usEast1Provider = new
|
|
20688
|
+
async function createCdnACMCertificate(config2) {
|
|
20689
|
+
const usEast1Provider = new aws13.Provider("storage-acm-us-east-1", {
|
|
20427
20690
|
region: "us-east-1"
|
|
20428
20691
|
});
|
|
20429
|
-
const certificate = new
|
|
20430
|
-
"wraps-
|
|
20692
|
+
const certificate = new aws13.acm.Certificate(
|
|
20693
|
+
"wraps-cdn-cert",
|
|
20431
20694
|
{
|
|
20432
20695
|
domainName: config2.domain,
|
|
20433
20696
|
validationMethod: "DNS",
|
|
20434
20697
|
tags: {
|
|
20435
20698
|
ManagedBy: "wraps-cli",
|
|
20436
|
-
Service: "
|
|
20437
|
-
Description: "SSL certificate for Wraps
|
|
20699
|
+
Service: "cdn",
|
|
20700
|
+
Description: "SSL certificate for Wraps CDN domain"
|
|
20438
20701
|
}
|
|
20439
20702
|
},
|
|
20440
20703
|
{
|
|
@@ -20450,8 +20713,8 @@ async function createStorageACMCertificate(config2) {
|
|
|
20450
20713
|
);
|
|
20451
20714
|
let certificateValidation;
|
|
20452
20715
|
if (config2.hostedZoneId) {
|
|
20453
|
-
const validationRecord = new
|
|
20454
|
-
"wraps-
|
|
20716
|
+
const validationRecord = new aws13.route53.Record(
|
|
20717
|
+
"wraps-cdn-cert-validation",
|
|
20455
20718
|
{
|
|
20456
20719
|
zoneId: config2.hostedZoneId,
|
|
20457
20720
|
name: certificate.domainValidationOptions[0].resourceRecordName,
|
|
@@ -20460,8 +20723,8 @@ async function createStorageACMCertificate(config2) {
|
|
|
20460
20723
|
ttl: 60
|
|
20461
20724
|
}
|
|
20462
20725
|
);
|
|
20463
|
-
certificateValidation = new
|
|
20464
|
-
"wraps-
|
|
20726
|
+
certificateValidation = new aws13.acm.CertificateValidation(
|
|
20727
|
+
"wraps-cdn-cert-validation-waiter",
|
|
20465
20728
|
{
|
|
20466
20729
|
certificateArn: certificate.arn,
|
|
20467
20730
|
validationRecordFqdns: [validationRecord.fqdn]
|
|
@@ -20478,14 +20741,14 @@ async function createStorageACMCertificate(config2) {
|
|
|
20478
20741
|
};
|
|
20479
20742
|
}
|
|
20480
20743
|
|
|
20481
|
-
// src/infrastructure/
|
|
20744
|
+
// src/infrastructure/cdn-stack.ts
|
|
20482
20745
|
async function roleExists3(roleName) {
|
|
20483
20746
|
try {
|
|
20484
20747
|
const { IAMClient: IAMClient3, GetRoleCommand: GetRoleCommand2 } = await import("@aws-sdk/client-iam");
|
|
20485
|
-
const
|
|
20748
|
+
const iam8 = new IAMClient3({
|
|
20486
20749
|
region: process.env.AWS_REGION || "us-east-1"
|
|
20487
20750
|
});
|
|
20488
|
-
await
|
|
20751
|
+
await iam8.send(new GetRoleCommand2({ RoleName: roleName }));
|
|
20489
20752
|
return true;
|
|
20490
20753
|
} catch (error) {
|
|
20491
20754
|
if (error.name === "NoSuchEntityException" || error.Code === "NoSuchEntity" || error.Error?.Code === "NoSuchEntity") {
|
|
@@ -20495,7 +20758,7 @@ async function roleExists3(roleName) {
|
|
|
20495
20758
|
return false;
|
|
20496
20759
|
}
|
|
20497
20760
|
}
|
|
20498
|
-
async function
|
|
20761
|
+
async function createCdnIAMRole(config2) {
|
|
20499
20762
|
let assumeRolePolicy;
|
|
20500
20763
|
if (config2.provider === "vercel" && config2.oidcProvider) {
|
|
20501
20764
|
assumeRolePolicy = pulumi22.interpolate`{
|
|
@@ -20530,28 +20793,28 @@ async function createStorageIAMRole(config2) {
|
|
|
20530
20793
|
} else {
|
|
20531
20794
|
throw new Error("Other providers not yet implemented");
|
|
20532
20795
|
}
|
|
20533
|
-
const roleName = "wraps-
|
|
20796
|
+
const roleName = "wraps-cdn-role";
|
|
20534
20797
|
const exists = await roleExists3(roleName);
|
|
20535
|
-
const role = exists ? new
|
|
20798
|
+
const role = exists ? new aws14.iam.Role(
|
|
20536
20799
|
roleName,
|
|
20537
20800
|
{
|
|
20538
20801
|
name: roleName,
|
|
20539
20802
|
assumeRolePolicy,
|
|
20540
20803
|
tags: {
|
|
20541
20804
|
ManagedBy: "wraps-cli",
|
|
20542
|
-
Service: "
|
|
20805
|
+
Service: "cdn",
|
|
20543
20806
|
Provider: config2.provider
|
|
20544
20807
|
}
|
|
20545
20808
|
},
|
|
20546
20809
|
{
|
|
20547
20810
|
import: roleName
|
|
20548
20811
|
}
|
|
20549
|
-
) : new
|
|
20812
|
+
) : new aws14.iam.Role(roleName, {
|
|
20550
20813
|
name: roleName,
|
|
20551
20814
|
assumeRolePolicy,
|
|
20552
20815
|
tags: {
|
|
20553
20816
|
ManagedBy: "wraps-cli",
|
|
20554
|
-
Service: "
|
|
20817
|
+
Service: "cdn",
|
|
20555
20818
|
Provider: config2.provider
|
|
20556
20819
|
}
|
|
20557
20820
|
});
|
|
@@ -20584,7 +20847,7 @@ async function createStorageIAMRole(config2) {
|
|
|
20584
20847
|
Resource: config2.distributionArn
|
|
20585
20848
|
});
|
|
20586
20849
|
}
|
|
20587
|
-
new
|
|
20850
|
+
new aws14.iam.RolePolicy("wraps-cdn-policy", {
|
|
20588
20851
|
role: role.name,
|
|
20589
20852
|
policy: pulumi22.all([statements]).apply(
|
|
20590
20853
|
([stmts]) => JSON.stringify({
|
|
@@ -20595,7 +20858,7 @@ async function createStorageIAMRole(config2) {
|
|
|
20595
20858
|
});
|
|
20596
20859
|
return role;
|
|
20597
20860
|
}
|
|
20598
|
-
async function
|
|
20861
|
+
async function deployCdnStack(config2) {
|
|
20599
20862
|
const accountId = config2.accountId;
|
|
20600
20863
|
let oidcProvider;
|
|
20601
20864
|
if (config2.provider === "vercel" && config2.vercel) {
|
|
@@ -20604,42 +20867,42 @@ async function deployStorageStack(config2) {
|
|
|
20604
20867
|
accountId
|
|
20605
20868
|
});
|
|
20606
20869
|
}
|
|
20607
|
-
const bucketResources = await
|
|
20870
|
+
const bucketResources = await createCdnBucket({
|
|
20608
20871
|
accountId,
|
|
20609
20872
|
region: config2.region,
|
|
20610
|
-
|
|
20873
|
+
cdnConfig: config2.cdnConfig
|
|
20611
20874
|
});
|
|
20612
20875
|
let acmResources;
|
|
20613
20876
|
let hostedZone = null;
|
|
20614
|
-
if (config2.
|
|
20615
|
-
const domainParts = config2.
|
|
20616
|
-
const rootDomain = domainParts.length > 2 ? domainParts.slice(-2).join(".") : config2.
|
|
20877
|
+
if (config2.cdnConfig.cdn.customDomain) {
|
|
20878
|
+
const domainParts = config2.cdnConfig.cdn.customDomain.split(".");
|
|
20879
|
+
const rootDomain = domainParts.length > 2 ? domainParts.slice(-2).join(".") : config2.cdnConfig.cdn.customDomain;
|
|
20617
20880
|
const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
|
|
20618
20881
|
hostedZone = await findHostedZone2(rootDomain, config2.region);
|
|
20619
|
-
acmResources = await
|
|
20620
|
-
domain: config2.
|
|
20882
|
+
acmResources = await createCdnACMCertificate({
|
|
20883
|
+
domain: config2.cdnConfig.cdn.customDomain,
|
|
20621
20884
|
hostedZoneId: hostedZone?.id
|
|
20622
20885
|
});
|
|
20623
20886
|
}
|
|
20624
20887
|
let cdnResources;
|
|
20625
|
-
if (config2.
|
|
20888
|
+
if (config2.cdnConfig.cdn.enabled) {
|
|
20626
20889
|
const hasAutoValidation2 = acmResources?.certificateValidation;
|
|
20627
20890
|
const useCertFromUpgrade = config2.certValidated && config2.existingCertArn;
|
|
20628
20891
|
const certificateArn = useCertFromUpgrade ? pulumi22.output(config2.existingCertArn) : hasAutoValidation2 ? acmResources?.certificateValidation?.certificateArn : void 0;
|
|
20629
|
-
const customDomainForCdn = useCertFromUpgrade || hasAutoValidation2 ? config2.
|
|
20630
|
-
cdnResources = await
|
|
20892
|
+
const customDomainForCdn = useCertFromUpgrade || hasAutoValidation2 ? config2.cdnConfig.cdn.customDomain : void 0;
|
|
20893
|
+
cdnResources = await createCdnDistribution({
|
|
20631
20894
|
bucket: bucketResources.bucket,
|
|
20632
20895
|
bucketRegion: config2.region,
|
|
20633
20896
|
// For Origin Shield
|
|
20634
20897
|
customDomain: customDomainForCdn,
|
|
20635
20898
|
certificateArn,
|
|
20636
|
-
priceClass: config2.
|
|
20637
|
-
originShield: config2.
|
|
20638
|
-
geoRestriction: config2.
|
|
20639
|
-
wafEnabled: config2.
|
|
20899
|
+
priceClass: config2.cdnConfig.cdn.priceClass,
|
|
20900
|
+
originShield: config2.cdnConfig.cdn.originShield,
|
|
20901
|
+
geoRestriction: config2.cdnConfig.cdn.geoRestriction,
|
|
20902
|
+
wafEnabled: config2.cdnConfig.cdn.wafEnabled
|
|
20640
20903
|
});
|
|
20641
20904
|
}
|
|
20642
|
-
const role = await
|
|
20905
|
+
const role = await createCdnIAMRole({
|
|
20643
20906
|
provider: config2.provider,
|
|
20644
20907
|
oidcProvider,
|
|
20645
20908
|
vercelTeamSlug: config2.vercel?.teamSlug,
|
|
@@ -20649,8 +20912,8 @@ async function deployStorageStack(config2) {
|
|
|
20649
20912
|
});
|
|
20650
20913
|
const hasAutoValidation = acmResources?.certificateValidation;
|
|
20651
20914
|
const hasCertFromUpgrade = config2.certValidated && config2.existingCertArn;
|
|
20652
|
-
const customDomainActive = config2.
|
|
20653
|
-
const customDomainPending = config2.
|
|
20915
|
+
const customDomainActive = config2.cdnConfig.cdn.customDomain && (hasAutoValidation || hasCertFromUpgrade);
|
|
20916
|
+
const customDomainPending = config2.cdnConfig.cdn.customDomain && !hasAutoValidation && !hasCertFromUpgrade;
|
|
20654
20917
|
return {
|
|
20655
20918
|
roleArn: role.arn,
|
|
20656
20919
|
bucketName: bucketResources.bucketName,
|
|
@@ -20659,17 +20922,17 @@ async function deployStorageStack(config2) {
|
|
|
20659
20922
|
distributionId: cdnResources?.distributionId,
|
|
20660
20923
|
distributionDomain: cdnResources?.domainName,
|
|
20661
20924
|
// Report custom domain if it's actually configured on CloudFront (auto or manual validation)
|
|
20662
|
-
customDomain: customDomainActive ? config2.
|
|
20925
|
+
customDomain: customDomainActive ? config2.cdnConfig.cdn.customDomain : void 0,
|
|
20663
20926
|
// Report pending custom domain that needs manual cert validation
|
|
20664
|
-
customDomainPending: customDomainPending ? config2.
|
|
20927
|
+
customDomainPending: customDomainPending ? config2.cdnConfig.cdn.customDomain : void 0,
|
|
20665
20928
|
acmCertificateArn: acmResources?.certificate.arn,
|
|
20666
20929
|
acmCertificateValidationRecords: acmResources?.validationRecords,
|
|
20667
|
-
versioning: config2.
|
|
20668
|
-
retention: config2.
|
|
20930
|
+
versioning: config2.cdnConfig.versioning ?? false,
|
|
20931
|
+
retention: config2.cdnConfig.retention
|
|
20669
20932
|
};
|
|
20670
20933
|
}
|
|
20671
20934
|
|
|
20672
|
-
// src/commands/
|
|
20935
|
+
// src/commands/cdn/init.ts
|
|
20673
20936
|
init_events();
|
|
20674
20937
|
init_aws();
|
|
20675
20938
|
init_errors();
|
|
@@ -20677,7 +20940,7 @@ init_fs();
|
|
|
20677
20940
|
init_metadata();
|
|
20678
20941
|
init_prompts();
|
|
20679
20942
|
|
|
20680
|
-
// src/utils/
|
|
20943
|
+
// src/utils/cdn/costs.ts
|
|
20681
20944
|
init_esm_shims();
|
|
20682
20945
|
var PRICING = {
|
|
20683
20946
|
// S3 Storage (per GB/month)
|
|
@@ -20763,7 +21026,7 @@ function getCostSummary2(config2, estimatedStorageGB = 10, estimatedBandwidthGB
|
|
|
20763
21026
|
return lines.join("\n");
|
|
20764
21027
|
}
|
|
20765
21028
|
|
|
20766
|
-
// src/utils/
|
|
21029
|
+
// src/utils/cdn/presets.ts
|
|
20767
21030
|
init_esm_shims();
|
|
20768
21031
|
var STARTER_PRESET2 = {
|
|
20769
21032
|
cdn: {
|
|
@@ -20878,13 +21141,13 @@ function validateConfig2(config2) {
|
|
|
20878
21141
|
return warnings;
|
|
20879
21142
|
}
|
|
20880
21143
|
|
|
20881
|
-
// src/commands/
|
|
20882
|
-
async function
|
|
21144
|
+
// src/commands/cdn/init.ts
|
|
21145
|
+
async function promptCdnPreset() {
|
|
20883
21146
|
const starterInfo = getPresetInfo2("starter");
|
|
20884
21147
|
const productionInfo = getPresetInfo2("production");
|
|
20885
21148
|
const customInfo = getPresetInfo2("custom");
|
|
20886
21149
|
const result = await clack29.select({
|
|
20887
|
-
message: "Select a
|
|
21150
|
+
message: "Select a CDN configuration preset:",
|
|
20888
21151
|
options: [
|
|
20889
21152
|
{
|
|
20890
21153
|
value: "production",
|
|
@@ -20909,7 +21172,7 @@ async function promptStoragePreset() {
|
|
|
20909
21172
|
}
|
|
20910
21173
|
return result;
|
|
20911
21174
|
}
|
|
20912
|
-
async function
|
|
21175
|
+
async function promptCustomCdnConfig() {
|
|
20913
21176
|
const cdnEnabled = await clack29.confirm({
|
|
20914
21177
|
message: "Enable CloudFront CDN for fast global delivery?",
|
|
20915
21178
|
initialValue: true
|
|
@@ -21104,7 +21367,7 @@ async function init3(options) {
|
|
|
21104
21367
|
const startTime = Date.now();
|
|
21105
21368
|
clack29.intro(
|
|
21106
21369
|
pc31.bold(
|
|
21107
|
-
options.preview ? "Wraps
|
|
21370
|
+
options.preview ? "Wraps CDN Infrastructure Preview" : "Wraps CDN Infrastructure Setup"
|
|
21108
21371
|
)
|
|
21109
21372
|
);
|
|
21110
21373
|
const progress = new DeploymentProgress();
|
|
@@ -21137,45 +21400,45 @@ async function init3(options) {
|
|
|
21137
21400
|
identity.accountId,
|
|
21138
21401
|
region
|
|
21139
21402
|
);
|
|
21140
|
-
if (existingConnection && hasService(existingConnection, "
|
|
21403
|
+
if (existingConnection && hasService(existingConnection, "cdn")) {
|
|
21141
21404
|
clack29.log.warn(
|
|
21142
|
-
`
|
|
21405
|
+
`CDN service already exists for account ${pc31.cyan(identity.accountId)} in region ${pc31.cyan(region)}`
|
|
21143
21406
|
);
|
|
21144
21407
|
clack29.log.info(
|
|
21145
|
-
`Created: ${existingConnection.services.
|
|
21408
|
+
`Created: ${existingConnection.services.cdn?.deployedAt}`
|
|
21146
21409
|
);
|
|
21147
21410
|
clack29.log.info(
|
|
21148
|
-
`Use ${pc31.cyan("wraps
|
|
21411
|
+
`Use ${pc31.cyan("wraps cdn status")} to view current setup`
|
|
21149
21412
|
);
|
|
21150
21413
|
process.exit(0);
|
|
21151
21414
|
}
|
|
21152
21415
|
let preset = options.preset;
|
|
21153
21416
|
if (!preset) {
|
|
21154
|
-
preset = await
|
|
21417
|
+
preset = await promptCdnPreset();
|
|
21155
21418
|
}
|
|
21156
|
-
let
|
|
21419
|
+
let cdnConfig;
|
|
21157
21420
|
if (preset === "custom") {
|
|
21158
|
-
|
|
21421
|
+
cdnConfig = await promptCustomCdnConfig();
|
|
21159
21422
|
} else {
|
|
21160
|
-
|
|
21423
|
+
cdnConfig = getPreset2(preset);
|
|
21161
21424
|
}
|
|
21162
21425
|
let customDomain = options.domain;
|
|
21163
|
-
if (!customDomain &&
|
|
21426
|
+
if (!customDomain && cdnConfig.cdn.enabled) {
|
|
21164
21427
|
customDomain = await promptCustomDomain();
|
|
21165
21428
|
}
|
|
21166
21429
|
if (customDomain) {
|
|
21167
|
-
|
|
21430
|
+
cdnConfig.cdn.customDomain = customDomain;
|
|
21168
21431
|
}
|
|
21169
21432
|
const estimatedUsage = await promptEstimatedUsage();
|
|
21170
21433
|
progress.info(`
|
|
21171
21434
|
${pc31.bold("Cost Estimate:")}`);
|
|
21172
21435
|
const costSummary = getCostSummary2(
|
|
21173
|
-
|
|
21436
|
+
cdnConfig,
|
|
21174
21437
|
estimatedUsage.storageGB,
|
|
21175
21438
|
estimatedUsage.bandwidthGB
|
|
21176
21439
|
);
|
|
21177
21440
|
clack29.log.info(costSummary);
|
|
21178
|
-
const warnings = validateConfig2(
|
|
21441
|
+
const warnings = validateConfig2(cdnConfig);
|
|
21179
21442
|
if (warnings.length > 0) {
|
|
21180
21443
|
progress.info(`
|
|
21181
21444
|
${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
@@ -21187,8 +21450,8 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21187
21450
|
identity.accountId,
|
|
21188
21451
|
region,
|
|
21189
21452
|
provider,
|
|
21190
|
-
"
|
|
21191
|
-
|
|
21453
|
+
"cdn",
|
|
21454
|
+
cdnConfig,
|
|
21192
21455
|
preset === "custom" ? void 0 : preset,
|
|
21193
21456
|
existingConnection || void 0
|
|
21194
21457
|
);
|
|
@@ -21207,7 +21470,7 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21207
21470
|
region,
|
|
21208
21471
|
accountId: identity.accountId,
|
|
21209
21472
|
vercel: vercelConfig,
|
|
21210
|
-
|
|
21473
|
+
cdnConfig
|
|
21211
21474
|
};
|
|
21212
21475
|
if (options.preview) {
|
|
21213
21476
|
try {
|
|
@@ -21217,10 +21480,10 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21217
21480
|
await ensurePulumiWorkDir();
|
|
21218
21481
|
const stack = await pulumi23.automation.LocalWorkspace.createOrSelectStack(
|
|
21219
21482
|
{
|
|
21220
|
-
stackName: `wraps-
|
|
21221
|
-
projectName: "wraps-
|
|
21483
|
+
stackName: `wraps-cdn-${identity.accountId}-${region}`,
|
|
21484
|
+
projectName: "wraps-cdn",
|
|
21222
21485
|
program: async () => {
|
|
21223
|
-
const result2 = await
|
|
21486
|
+
const result2 = await deployCdnStack(stackConfig);
|
|
21224
21487
|
return {
|
|
21225
21488
|
roleArn: result2.roleArn,
|
|
21226
21489
|
bucketName: result2.bucketName,
|
|
@@ -21252,12 +21515,12 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21252
21515
|
changeSummary: previewResult.changeSummary,
|
|
21253
21516
|
resourceChanges: previewResult.resourceChanges,
|
|
21254
21517
|
costEstimate: costSummary,
|
|
21255
|
-
commandName: "wraps
|
|
21518
|
+
commandName: "wraps cdn init"
|
|
21256
21519
|
});
|
|
21257
21520
|
clack29.outro(
|
|
21258
21521
|
pc31.green("Preview complete. Run without --preview to deploy.")
|
|
21259
21522
|
);
|
|
21260
|
-
trackServiceInit("
|
|
21523
|
+
trackServiceInit("cdn", true, {
|
|
21261
21524
|
preset,
|
|
21262
21525
|
provider,
|
|
21263
21526
|
region,
|
|
@@ -21276,15 +21539,15 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21276
21539
|
let outputs;
|
|
21277
21540
|
try {
|
|
21278
21541
|
outputs = await progress.execute(
|
|
21279
|
-
"Deploying
|
|
21542
|
+
"Deploying CDN infrastructure (this may take 2-3 minutes)",
|
|
21280
21543
|
async () => {
|
|
21281
21544
|
await ensurePulumiWorkDir();
|
|
21282
21545
|
const stack = await pulumi23.automation.LocalWorkspace.createOrSelectStack(
|
|
21283
21546
|
{
|
|
21284
|
-
stackName: `wraps-
|
|
21285
|
-
projectName: "wraps-
|
|
21547
|
+
stackName: `wraps-cdn-${identity.accountId}-${region}`,
|
|
21548
|
+
projectName: "wraps-cdn",
|
|
21286
21549
|
program: async () => {
|
|
21287
|
-
const result = await
|
|
21550
|
+
const result = await deployCdnStack(stackConfig);
|
|
21288
21551
|
return {
|
|
21289
21552
|
roleArn: result.roleArn,
|
|
21290
21553
|
bucketName: result.bucketName,
|
|
@@ -21311,7 +21574,7 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21311
21574
|
}
|
|
21312
21575
|
);
|
|
21313
21576
|
await stack.workspace.selectStack(
|
|
21314
|
-
`wraps-
|
|
21577
|
+
`wraps-cdn-${identity.accountId}-${region}`
|
|
21315
21578
|
);
|
|
21316
21579
|
await stack.setConfig("aws:region", { value: region });
|
|
21317
21580
|
const upResult = await stack.up({ onOutput: () => {
|
|
@@ -21334,7 +21597,7 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21334
21597
|
}
|
|
21335
21598
|
);
|
|
21336
21599
|
} catch (error) {
|
|
21337
|
-
trackServiceInit("
|
|
21600
|
+
trackServiceInit("cdn", false, {
|
|
21338
21601
|
preset,
|
|
21339
21602
|
provider,
|
|
21340
21603
|
region,
|
|
@@ -21347,8 +21610,8 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21347
21610
|
trackError("DEPLOYMENT_FAILED", "storage:init", { step: "deploy" });
|
|
21348
21611
|
throw new Error(`Pulumi deployment failed: ${error.message}`);
|
|
21349
21612
|
}
|
|
21350
|
-
if (metadata.services.
|
|
21351
|
-
metadata.services.
|
|
21613
|
+
if (metadata.services.cdn) {
|
|
21614
|
+
metadata.services.cdn.pulumiStackName = `wraps-cdn-${identity.accountId}-${region}`;
|
|
21352
21615
|
}
|
|
21353
21616
|
await saveConnectionMetadata(metadata);
|
|
21354
21617
|
progress.info("Connection metadata saved");
|
|
@@ -21386,12 +21649,12 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21386
21649
|
);
|
|
21387
21650
|
const dnsRecords = [];
|
|
21388
21651
|
const existingValue = existingCname?.ResourceRecords?.[0]?.Value || null;
|
|
21389
|
-
let
|
|
21652
|
+
let cdnStatus2 = "new";
|
|
21390
21653
|
let cdnConflictReason;
|
|
21391
21654
|
if (existingValue === outputs.distributionDomain) {
|
|
21392
|
-
|
|
21655
|
+
cdnStatus2 = "no_change";
|
|
21393
21656
|
} else if (existingValue) {
|
|
21394
|
-
|
|
21657
|
+
cdnStatus2 = "conflict";
|
|
21395
21658
|
cdnConflictReason = `Currently points to ${existingValue}`;
|
|
21396
21659
|
}
|
|
21397
21660
|
dnsRecords.push({
|
|
@@ -21399,7 +21662,7 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21399
21662
|
type: "CNAME",
|
|
21400
21663
|
proposedValue: outputs.distributionDomain,
|
|
21401
21664
|
existingValue,
|
|
21402
|
-
status:
|
|
21665
|
+
status: cdnStatus2,
|
|
21403
21666
|
conflictReason: cdnConflictReason
|
|
21404
21667
|
});
|
|
21405
21668
|
console.log();
|
|
@@ -21479,7 +21742,7 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21479
21742
|
}
|
|
21480
21743
|
}
|
|
21481
21744
|
}
|
|
21482
|
-
|
|
21745
|
+
displayCdnSuccess({
|
|
21483
21746
|
bucketName: outputs.bucketName,
|
|
21484
21747
|
region: outputs.region,
|
|
21485
21748
|
distributionDomain: outputs.distributionDomain,
|
|
@@ -21494,31 +21757,31 @@ ${pc31.yellow(pc31.bold("Configuration Notes:"))}`);
|
|
|
21494
21757
|
});
|
|
21495
21758
|
const duration = Date.now() - startTime;
|
|
21496
21759
|
const enabledFeatures = [];
|
|
21497
|
-
if (
|
|
21760
|
+
if (cdnConfig.cdn.enabled) {
|
|
21498
21761
|
enabledFeatures.push("cdn");
|
|
21499
21762
|
}
|
|
21500
|
-
if (
|
|
21763
|
+
if (cdnConfig.cdn.customDomain) {
|
|
21501
21764
|
enabledFeatures.push("custom_domain");
|
|
21502
21765
|
}
|
|
21503
|
-
if (
|
|
21766
|
+
if (cdnConfig.versioning) {
|
|
21504
21767
|
enabledFeatures.push("versioning");
|
|
21505
21768
|
}
|
|
21506
|
-
trackServiceInit("
|
|
21769
|
+
trackServiceInit("cdn", true, {
|
|
21507
21770
|
preset,
|
|
21508
21771
|
provider,
|
|
21509
21772
|
region,
|
|
21510
21773
|
features: enabledFeatures,
|
|
21511
21774
|
duration_ms: duration
|
|
21512
21775
|
});
|
|
21513
|
-
trackServiceDeployed("
|
|
21776
|
+
trackServiceDeployed("cdn", {
|
|
21514
21777
|
duration_ms: duration,
|
|
21515
21778
|
region,
|
|
21516
21779
|
features: enabledFeatures,
|
|
21517
21780
|
preset
|
|
21518
21781
|
});
|
|
21519
21782
|
}
|
|
21520
|
-
function
|
|
21521
|
-
clack29.log.success(pc31.green(pc31.bold("
|
|
21783
|
+
function displayCdnSuccess(options) {
|
|
21784
|
+
clack29.log.success(pc31.green(pc31.bold("CDN infrastructure deployed!")));
|
|
21522
21785
|
clack29.log.info(`
|
|
21523
21786
|
${pc31.bold("Infrastructure:")}`);
|
|
21524
21787
|
clack29.log.info(` S3 Bucket: ${pc31.cyan(options.bucketName)}`);
|
|
@@ -21558,7 +21821,7 @@ ${pc31.yellow(pc31.bold("DNS Records Required:"))}`);
|
|
|
21558
21821
|
`);
|
|
21559
21822
|
}
|
|
21560
21823
|
clack29.log.warn(
|
|
21561
|
-
"Custom domain requires manual setup:\n 1. Add the SSL certificate validation DNS record above\n 2. Wait for certificate to be validated (check AWS ACM console)\n 3. Run 'wraps
|
|
21824
|
+
"Custom domain requires manual setup:\n 1. Add the SSL certificate validation DNS record above\n 2. Wait for certificate to be validated (check AWS ACM console)\n 3. Run 'wraps cdn upgrade' to add custom domain to CloudFront\n 4. Add the CDN CNAME record to point your domain to CloudFront"
|
|
21562
21825
|
);
|
|
21563
21826
|
} else if (options.customDomain && !options.dnsAutoCreated && options.distributionDomain) {
|
|
21564
21827
|
clack29.log.info(`
|
|
@@ -21572,19 +21835,19 @@ ${pc31.yellow(pc31.bold("DNS Record Required:"))}`);
|
|
|
21572
21835
|
clack29.log.info(`
|
|
21573
21836
|
${pc31.bold("Next Steps:")}`);
|
|
21574
21837
|
clack29.log.info(
|
|
21575
|
-
` 1. ${pc31.cyan("wraps
|
|
21838
|
+
` 1. ${pc31.cyan("wraps cdn status")} - View your CDN setup`
|
|
21576
21839
|
);
|
|
21577
21840
|
if (options.customDomainPending) {
|
|
21578
21841
|
clack29.log.info(" 2. Add DNS records above and validate SSL certificate");
|
|
21579
21842
|
clack29.log.info(
|
|
21580
|
-
` 3. ${pc31.cyan("wraps
|
|
21843
|
+
` 3. ${pc31.cyan("wraps cdn upgrade")} - Add custom domain to CloudFront`
|
|
21581
21844
|
);
|
|
21582
21845
|
} else if (options.customDomain && !options.dnsAutoCreated) {
|
|
21583
21846
|
clack29.log.info(
|
|
21584
21847
|
" 2. Add DNS record above to point your domain to CloudFront"
|
|
21585
21848
|
);
|
|
21586
21849
|
clack29.log.info(
|
|
21587
|
-
` 3. ${pc31.cyan("wraps
|
|
21850
|
+
` 3. ${pc31.cyan("wraps cdn verify")} - Check DNS propagation`
|
|
21588
21851
|
);
|
|
21589
21852
|
}
|
|
21590
21853
|
const cdnUrl = options.customDomain ? `https://${options.customDomain}` : options.distributionDomain ? `https://${options.distributionDomain}` : null;
|
|
@@ -21596,15 +21859,15 @@ ${pc31.bold("CDN URL:")}`);
|
|
|
21596
21859
|
if (options.customDomainPending) {
|
|
21597
21860
|
clack29.outro(
|
|
21598
21861
|
pc31.yellow(
|
|
21599
|
-
"
|
|
21862
|
+
"CDN deployed! Custom domain pending certificate validation."
|
|
21600
21863
|
)
|
|
21601
21864
|
);
|
|
21602
21865
|
} else {
|
|
21603
|
-
clack29.outro(pc31.green("
|
|
21866
|
+
clack29.outro(pc31.green("CDN is ready!"));
|
|
21604
21867
|
}
|
|
21605
21868
|
}
|
|
21606
21869
|
|
|
21607
|
-
// src/commands/
|
|
21870
|
+
// src/commands/cdn/status.ts
|
|
21608
21871
|
init_esm_shims();
|
|
21609
21872
|
init_client();
|
|
21610
21873
|
init_events();
|
|
@@ -21614,26 +21877,26 @@ init_metadata();
|
|
|
21614
21877
|
import * as clack30 from "@clack/prompts";
|
|
21615
21878
|
import * as pulumi24 from "@pulumi/pulumi";
|
|
21616
21879
|
import pc32 from "picocolors";
|
|
21617
|
-
async function
|
|
21880
|
+
async function cdnStatus(options) {
|
|
21618
21881
|
const startTime = Date.now();
|
|
21619
21882
|
const progress = new DeploymentProgress();
|
|
21620
|
-
clack30.intro(pc32.bold("Wraps
|
|
21883
|
+
clack30.intro(pc32.bold("Wraps CDN Status"));
|
|
21621
21884
|
const identity = await progress.execute(
|
|
21622
|
-
"Loading
|
|
21885
|
+
"Loading CDN infrastructure status",
|
|
21623
21886
|
async () => validateAWSCredentials()
|
|
21624
21887
|
);
|
|
21625
21888
|
let region = options.region || await getAWSRegion();
|
|
21626
21889
|
if (!(options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)) {
|
|
21627
|
-
const
|
|
21890
|
+
const cdnConnections = await findConnectionsWithService(
|
|
21628
21891
|
identity.accountId,
|
|
21629
|
-
"
|
|
21892
|
+
"cdn"
|
|
21630
21893
|
);
|
|
21631
|
-
if (
|
|
21632
|
-
region =
|
|
21633
|
-
} else if (
|
|
21894
|
+
if (cdnConnections.length === 1) {
|
|
21895
|
+
region = cdnConnections[0].region;
|
|
21896
|
+
} else if (cdnConnections.length > 1) {
|
|
21634
21897
|
const selectedRegion = await clack30.select({
|
|
21635
|
-
message: "Multiple
|
|
21636
|
-
options:
|
|
21898
|
+
message: "Multiple CDN deployments found. Which region?",
|
|
21899
|
+
options: cdnConnections.map((conn) => ({
|
|
21637
21900
|
value: conn.region,
|
|
21638
21901
|
label: conn.region
|
|
21639
21902
|
}))
|
|
@@ -21649,22 +21912,22 @@ async function storageStatus(options) {
|
|
|
21649
21912
|
try {
|
|
21650
21913
|
await ensurePulumiWorkDir();
|
|
21651
21914
|
const stack = await pulumi24.automation.LocalWorkspace.selectStack({
|
|
21652
|
-
stackName: `wraps-
|
|
21915
|
+
stackName: `wraps-cdn-${identity.accountId}-${region}`,
|
|
21653
21916
|
workDir: getPulumiWorkDir()
|
|
21654
21917
|
});
|
|
21655
21918
|
stackOutputs = await stack.outputs();
|
|
21656
21919
|
} catch (_error) {
|
|
21657
21920
|
progress.stop();
|
|
21658
|
-
clack30.log.error("No
|
|
21921
|
+
clack30.log.error("No CDN infrastructure found");
|
|
21659
21922
|
console.log(
|
|
21660
21923
|
`
|
|
21661
|
-
Run ${pc32.cyan("wraps
|
|
21924
|
+
Run ${pc32.cyan("wraps cdn init")} to deploy CDN infrastructure.
|
|
21662
21925
|
`
|
|
21663
21926
|
);
|
|
21664
21927
|
process.exit(1);
|
|
21665
21928
|
}
|
|
21666
21929
|
progress.stop();
|
|
21667
|
-
|
|
21930
|
+
displayCdnStatus({
|
|
21668
21931
|
bucketName: stackOutputs.bucketName?.value,
|
|
21669
21932
|
region: stackOutputs.region?.value || region,
|
|
21670
21933
|
distributionId: stackOutputs.distributionId?.value,
|
|
@@ -21686,9 +21949,9 @@ Run ${pc32.cyan("wraps storage init")} to deploy storage infrastructure.
|
|
|
21686
21949
|
});
|
|
21687
21950
|
getTelemetryClient().showFooterOnce();
|
|
21688
21951
|
}
|
|
21689
|
-
function
|
|
21952
|
+
function displayCdnStatus(options) {
|
|
21690
21953
|
clack30.log.info(`
|
|
21691
|
-
${pc32.bold("
|
|
21954
|
+
${pc32.bold("CDN Infrastructure:")}`);
|
|
21692
21955
|
clack30.log.info(` S3 Bucket: ${pc32.cyan(options.bucketName)}`);
|
|
21693
21956
|
clack30.log.info(` Region: ${pc32.cyan(options.region)}`);
|
|
21694
21957
|
clack30.log.info(
|
|
@@ -21755,19 +22018,19 @@ ${pc32.bold("File URLs:")}`);
|
|
|
21755
22018
|
${pc32.bold("Commands:")}`);
|
|
21756
22019
|
if (hasPendingCert) {
|
|
21757
22020
|
clack30.log.info(
|
|
21758
|
-
` ${pc32.cyan("wraps
|
|
22021
|
+
` ${pc32.cyan("wraps cdn upgrade")} - Add custom domain after cert validation`
|
|
21759
22022
|
);
|
|
21760
22023
|
}
|
|
21761
22024
|
clack30.log.info(
|
|
21762
|
-
` ${pc32.cyan("wraps
|
|
22025
|
+
` ${pc32.cyan("wraps cdn verify")} - Check DNS and certificate status`
|
|
21763
22026
|
);
|
|
21764
22027
|
clack30.log.info(
|
|
21765
|
-
` ${pc32.cyan("wraps
|
|
22028
|
+
` ${pc32.cyan("wraps cdn destroy")} - Remove CDN infrastructure`
|
|
21766
22029
|
);
|
|
21767
22030
|
clack30.outro("");
|
|
21768
22031
|
}
|
|
21769
22032
|
|
|
21770
|
-
// src/commands/
|
|
22033
|
+
// src/commands/cdn/sync.ts
|
|
21771
22034
|
init_esm_shims();
|
|
21772
22035
|
import * as clack31 from "@clack/prompts";
|
|
21773
22036
|
import * as pulumi25 from "@pulumi/pulumi";
|
|
@@ -21777,26 +22040,26 @@ init_events();
|
|
|
21777
22040
|
init_aws();
|
|
21778
22041
|
init_fs();
|
|
21779
22042
|
init_metadata();
|
|
21780
|
-
async function
|
|
22043
|
+
async function cdnSync(options) {
|
|
21781
22044
|
const startTime = Date.now();
|
|
21782
22045
|
const progress = new DeploymentProgress();
|
|
21783
|
-
clack31.intro(pc33.bold("Wraps
|
|
22046
|
+
clack31.intro(pc33.bold("Wraps CDN Sync"));
|
|
21784
22047
|
const identity = await progress.execute(
|
|
21785
22048
|
"Validating AWS credentials",
|
|
21786
22049
|
async () => validateAWSCredentials()
|
|
21787
22050
|
);
|
|
21788
22051
|
let region = options.region || await getAWSRegion();
|
|
21789
22052
|
if (!(options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)) {
|
|
21790
|
-
const
|
|
22053
|
+
const cdnConnections = await findConnectionsWithService(
|
|
21791
22054
|
identity.accountId,
|
|
21792
|
-
"
|
|
22055
|
+
"cdn"
|
|
21793
22056
|
);
|
|
21794
|
-
if (
|
|
21795
|
-
region =
|
|
21796
|
-
} else if (
|
|
22057
|
+
if (cdnConnections.length === 1) {
|
|
22058
|
+
region = cdnConnections[0].region;
|
|
22059
|
+
} else if (cdnConnections.length > 1) {
|
|
21797
22060
|
const selectedRegion = await clack31.select({
|
|
21798
|
-
message: "Multiple
|
|
21799
|
-
options:
|
|
22061
|
+
message: "Multiple CDN deployments found. Which region?",
|
|
22062
|
+
options: cdnConnections.map((conn) => ({
|
|
21800
22063
|
value: conn.region,
|
|
21801
22064
|
label: conn.region
|
|
21802
22065
|
}))
|
|
@@ -21809,25 +22072,25 @@ async function storageSync(options) {
|
|
|
21809
22072
|
}
|
|
21810
22073
|
}
|
|
21811
22074
|
const metadata = await loadConnectionMetadata(identity.accountId, region);
|
|
21812
|
-
if (!metadata?.services.
|
|
22075
|
+
if (!metadata?.services.cdn) {
|
|
21813
22076
|
clack31.log.error(
|
|
21814
|
-
`No
|
|
22077
|
+
`No CDN infrastructure found for account ${pc33.cyan(identity.accountId)} in region ${pc33.cyan(region)}`
|
|
21815
22078
|
);
|
|
21816
22079
|
clack31.log.info(
|
|
21817
|
-
`Use ${pc33.cyan("wraps
|
|
22080
|
+
`Use ${pc33.cyan("wraps cdn init")} to deploy CDN infrastructure.`
|
|
21818
22081
|
);
|
|
21819
22082
|
process.exit(1);
|
|
21820
22083
|
}
|
|
21821
|
-
const
|
|
21822
|
-
const
|
|
22084
|
+
const cdnService = metadata.services.cdn;
|
|
22085
|
+
const cdnConfig = cdnService.config;
|
|
21823
22086
|
progress.info(`Found storage deployment in ${pc33.cyan(region)}`);
|
|
21824
22087
|
let certValidated = false;
|
|
21825
22088
|
let existingCertArn;
|
|
21826
|
-
if (
|
|
22089
|
+
if (cdnConfig.cdn.customDomain) {
|
|
21827
22090
|
try {
|
|
21828
22091
|
await ensurePulumiWorkDir();
|
|
21829
22092
|
const checkStack = await pulumi25.automation.LocalWorkspace.selectStack({
|
|
21830
|
-
stackName: `wraps-
|
|
22093
|
+
stackName: `wraps-cdn-${identity.accountId}-${region}`,
|
|
21831
22094
|
workDir: getPulumiWorkDir()
|
|
21832
22095
|
});
|
|
21833
22096
|
const stackOutputs = await checkStack.outputs();
|
|
@@ -21843,7 +22106,7 @@ async function storageSync(options) {
|
|
|
21843
22106
|
certValidated = true;
|
|
21844
22107
|
existingCertArn = stackOutputs.acmCertificateArn.value;
|
|
21845
22108
|
progress.info(
|
|
21846
|
-
`Certificate validated for ${pc33.cyan(
|
|
22109
|
+
`Certificate validated for ${pc33.cyan(cdnConfig.cdn.customDomain)}`
|
|
21847
22110
|
);
|
|
21848
22111
|
}
|
|
21849
22112
|
}
|
|
@@ -21855,20 +22118,20 @@ async function storageSync(options) {
|
|
|
21855
22118
|
region,
|
|
21856
22119
|
accountId: identity.accountId,
|
|
21857
22120
|
vercel: metadata.vercel,
|
|
21858
|
-
|
|
22121
|
+
cdnConfig,
|
|
21859
22122
|
// Pass cert validation flags if cert is validated externally
|
|
21860
22123
|
certValidated,
|
|
21861
22124
|
existingCertArn
|
|
21862
22125
|
};
|
|
21863
22126
|
try {
|
|
21864
|
-
await progress.execute("Syncing
|
|
22127
|
+
await progress.execute("Syncing CDN infrastructure", async () => {
|
|
21865
22128
|
await ensurePulumiWorkDir();
|
|
21866
22129
|
const stack = await pulumi25.automation.LocalWorkspace.createOrSelectStack(
|
|
21867
22130
|
{
|
|
21868
|
-
stackName: `wraps-
|
|
21869
|
-
projectName: "wraps-
|
|
22131
|
+
stackName: `wraps-cdn-${identity.accountId}-${region}`,
|
|
22132
|
+
projectName: "wraps-cdn",
|
|
21870
22133
|
program: async () => {
|
|
21871
|
-
const result2 = await
|
|
22134
|
+
const result2 = await deployCdnStack(stackConfig);
|
|
21872
22135
|
return {
|
|
21873
22136
|
roleArn: result2.roleArn,
|
|
21874
22137
|
bucketName: result2.bucketName,
|
|
@@ -21911,7 +22174,7 @@ async function storageSync(options) {
|
|
|
21911
22174
|
clack31.log.error(`Sync failed: ${error.message}`);
|
|
21912
22175
|
process.exit(1);
|
|
21913
22176
|
}
|
|
21914
|
-
clack31.log.success(pc33.green("
|
|
22177
|
+
clack31.log.success(pc33.green("CDN infrastructure synced!"));
|
|
21915
22178
|
trackCommand("storage:sync", {
|
|
21916
22179
|
success: true,
|
|
21917
22180
|
region,
|
|
@@ -21921,7 +22184,7 @@ async function storageSync(options) {
|
|
|
21921
22184
|
clack31.outro("");
|
|
21922
22185
|
}
|
|
21923
22186
|
|
|
21924
|
-
// src/commands/
|
|
22187
|
+
// src/commands/cdn/upgrade.ts
|
|
21925
22188
|
init_esm_shims();
|
|
21926
22189
|
import * as clack32 from "@clack/prompts";
|
|
21927
22190
|
import * as pulumi26 from "@pulumi/pulumi";
|
|
@@ -21931,12 +22194,12 @@ init_events();
|
|
|
21931
22194
|
init_aws();
|
|
21932
22195
|
init_fs();
|
|
21933
22196
|
init_metadata();
|
|
21934
|
-
async function
|
|
22197
|
+
async function cdnUpgrade(options) {
|
|
21935
22198
|
const startTime = Date.now();
|
|
21936
22199
|
const progress = new DeploymentProgress();
|
|
21937
22200
|
clack32.intro(
|
|
21938
22201
|
pc34.bold(
|
|
21939
|
-
options.preview ? "Wraps
|
|
22202
|
+
options.preview ? "Wraps CDN Upgrade Preview" : "Wraps CDN Upgrade"
|
|
21940
22203
|
)
|
|
21941
22204
|
);
|
|
21942
22205
|
const identity = await progress.execute(
|
|
@@ -21946,16 +22209,16 @@ async function storageUpgrade(options) {
|
|
|
21946
22209
|
progress.info(`Connected to AWS account: ${pc34.cyan(identity.accountId)}`);
|
|
21947
22210
|
let region = options.region || await getAWSRegion();
|
|
21948
22211
|
if (!(options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)) {
|
|
21949
|
-
const
|
|
22212
|
+
const cdnConnections = await findConnectionsWithService(
|
|
21950
22213
|
identity.accountId,
|
|
21951
|
-
"
|
|
22214
|
+
"cdn"
|
|
21952
22215
|
);
|
|
21953
|
-
if (
|
|
21954
|
-
region =
|
|
21955
|
-
} else if (
|
|
22216
|
+
if (cdnConnections.length === 1) {
|
|
22217
|
+
region = cdnConnections[0].region;
|
|
22218
|
+
} else if (cdnConnections.length > 1) {
|
|
21956
22219
|
const selectedRegion = await clack32.select({
|
|
21957
|
-
message: "Multiple
|
|
21958
|
-
options:
|
|
22220
|
+
message: "Multiple CDN deployments found. Which region?",
|
|
22221
|
+
options: cdnConnections.map((conn) => ({
|
|
21959
22222
|
value: conn.region,
|
|
21960
22223
|
label: conn.region
|
|
21961
22224
|
}))
|
|
@@ -21968,22 +22231,22 @@ async function storageUpgrade(options) {
|
|
|
21968
22231
|
}
|
|
21969
22232
|
}
|
|
21970
22233
|
const metadata = await loadConnectionMetadata(identity.accountId, region);
|
|
21971
|
-
if (!metadata?.services.
|
|
22234
|
+
if (!metadata?.services.cdn) {
|
|
21972
22235
|
clack32.log.error(
|
|
21973
|
-
`No
|
|
22236
|
+
`No CDN infrastructure found for account ${pc34.cyan(identity.accountId)} in region ${pc34.cyan(region)}`
|
|
21974
22237
|
);
|
|
21975
22238
|
clack32.log.info(
|
|
21976
|
-
`Use ${pc34.cyan("wraps
|
|
22239
|
+
`Use ${pc34.cyan("wraps cdn init")} to deploy CDN infrastructure.`
|
|
21977
22240
|
);
|
|
21978
22241
|
process.exit(1);
|
|
21979
22242
|
}
|
|
21980
|
-
const
|
|
21981
|
-
const
|
|
22243
|
+
const cdnService = metadata.services.cdn;
|
|
22244
|
+
const cdnConfig = cdnService.config;
|
|
21982
22245
|
let stackOutputs = {};
|
|
21983
22246
|
try {
|
|
21984
22247
|
await ensurePulumiWorkDir();
|
|
21985
22248
|
const stack = await pulumi26.automation.LocalWorkspace.selectStack({
|
|
21986
|
-
stackName: `wraps-
|
|
22249
|
+
stackName: `wraps-cdn-${identity.accountId}-${region}`,
|
|
21987
22250
|
workDir: getPulumiWorkDir()
|
|
21988
22251
|
});
|
|
21989
22252
|
stackOutputs = await stack.outputs();
|
|
@@ -21992,7 +22255,7 @@ async function storageUpgrade(options) {
|
|
|
21992
22255
|
process.exit(1);
|
|
21993
22256
|
}
|
|
21994
22257
|
let cloudFrontHasCustomDomain = false;
|
|
21995
|
-
if (stackOutputs.distributionId?.value &&
|
|
22258
|
+
if (stackOutputs.distributionId?.value && cdnConfig.cdn?.customDomain) {
|
|
21996
22259
|
try {
|
|
21997
22260
|
const { CloudFrontClient, GetDistributionCommand } = await import("@aws-sdk/client-cloudfront");
|
|
21998
22261
|
const cfClient = new CloudFrontClient({ region: "us-east-1" });
|
|
@@ -22003,16 +22266,16 @@ async function storageUpgrade(options) {
|
|
|
22003
22266
|
);
|
|
22004
22267
|
const aliases = cfResponse.Distribution?.DistributionConfig?.Aliases?.Items || [];
|
|
22005
22268
|
cloudFrontHasCustomDomain = aliases.includes(
|
|
22006
|
-
|
|
22269
|
+
cdnConfig.cdn.customDomain
|
|
22007
22270
|
);
|
|
22008
22271
|
} catch {
|
|
22009
22272
|
}
|
|
22010
22273
|
}
|
|
22011
22274
|
const hasPendingCert = stackOutputs.acmCertificateArn?.value && !stackOutputs.customDomain?.value && !cloudFrontHasCustomDomain;
|
|
22012
22275
|
const pendingDomain = stackOutputs.customDomainPending?.value || (hasPendingCert && stackOutputs.acmCertificateValidationRecords?.value?.[0]?.name ? stackOutputs.acmCertificateValidationRecords.value[0].name.replace(/^_[^.]+\./, "").replace(/\.$/, "") : void 0);
|
|
22013
|
-
const activeCustomDomain = cloudFrontHasCustomDomain ?
|
|
22276
|
+
const activeCustomDomain = cloudFrontHasCustomDomain ? cdnConfig.cdn?.customDomain : stackOutputs.customDomain?.value;
|
|
22014
22277
|
if (!hasPendingCert) {
|
|
22015
|
-
clack32.log.info("No pending upgrades found for
|
|
22278
|
+
clack32.log.info("No pending upgrades found for CDN infrastructure.");
|
|
22016
22279
|
clack32.log.info(
|
|
22017
22280
|
`
|
|
22018
22281
|
Current configuration:
|
|
@@ -22059,7 +22322,7 @@ Pending domain: ${pc34.cyan(pendingDomain)}`);
|
|
|
22059
22322
|
clack32.log.info(
|
|
22060
22323
|
"After adding the DNS record, wait for validation (typically 5-30 minutes)."
|
|
22061
22324
|
);
|
|
22062
|
-
clack32.log.info(`Then run ${pc34.cyan("wraps
|
|
22325
|
+
clack32.log.info(`Then run ${pc34.cyan("wraps cdn upgrade")} again.
|
|
22063
22326
|
`);
|
|
22064
22327
|
process.exit(0);
|
|
22065
22328
|
}
|
|
@@ -22081,9 +22344,9 @@ Ready to add custom domain: ${pc34.cyan(pendingDomain)}`);
|
|
|
22081
22344
|
}
|
|
22082
22345
|
}
|
|
22083
22346
|
const updatedConfig = {
|
|
22084
|
-
...
|
|
22347
|
+
...cdnConfig,
|
|
22085
22348
|
cdn: {
|
|
22086
|
-
...
|
|
22349
|
+
...cdnConfig.cdn,
|
|
22087
22350
|
customDomain: pendingDomain
|
|
22088
22351
|
}
|
|
22089
22352
|
};
|
|
@@ -22092,8 +22355,8 @@ Ready to add custom domain: ${pc34.cyan(pendingDomain)}`);
|
|
|
22092
22355
|
region,
|
|
22093
22356
|
accountId: identity.accountId,
|
|
22094
22357
|
vercel: metadata.vercel,
|
|
22095
|
-
|
|
22096
|
-
// Tell
|
|
22358
|
+
cdnConfig: updatedConfig,
|
|
22359
|
+
// Tell deployCdnStack that the cert is validated externally (manual DNS)
|
|
22097
22360
|
certValidated: true,
|
|
22098
22361
|
existingCertArn: stackOutputs.acmCertificateArn.value
|
|
22099
22362
|
};
|
|
@@ -22123,10 +22386,10 @@ Ready to add custom domain: ${pc34.cyan(pendingDomain)}`);
|
|
|
22123
22386
|
await ensurePulumiWorkDir();
|
|
22124
22387
|
const stack = await pulumi26.automation.LocalWorkspace.createOrSelectStack(
|
|
22125
22388
|
{
|
|
22126
|
-
stackName: `wraps-
|
|
22127
|
-
projectName: "wraps-
|
|
22389
|
+
stackName: `wraps-cdn-${identity.accountId}-${region}`,
|
|
22390
|
+
projectName: "wraps-cdn",
|
|
22128
22391
|
program: async () => {
|
|
22129
|
-
const result = await
|
|
22392
|
+
const result = await deployCdnStack(stackConfig);
|
|
22130
22393
|
return {
|
|
22131
22394
|
roleArn: result.roleArn,
|
|
22132
22395
|
bucketName: result.bucketName,
|
|
@@ -22169,8 +22432,8 @@ Ready to add custom domain: ${pc34.cyan(pendingDomain)}`);
|
|
|
22169
22432
|
clack32.log.error(`Upgrade failed: ${error.message}`);
|
|
22170
22433
|
process.exit(1);
|
|
22171
22434
|
}
|
|
22172
|
-
if (metadata.services.
|
|
22173
|
-
metadata.services.
|
|
22435
|
+
if (metadata.services.cdn) {
|
|
22436
|
+
metadata.services.cdn.config = updatedConfig;
|
|
22174
22437
|
await saveConnectionMetadata(metadata);
|
|
22175
22438
|
}
|
|
22176
22439
|
clack32.log.success(`
|
|
@@ -22201,7 +22464,7 @@ ${pc34.green("")} ${pc34.bold("Upgrade complete!")}
|
|
|
22201
22464
|
clack32.outro("");
|
|
22202
22465
|
}
|
|
22203
22466
|
|
|
22204
|
-
// src/commands/
|
|
22467
|
+
// src/commands/cdn/verify.ts
|
|
22205
22468
|
init_esm_shims();
|
|
22206
22469
|
init_events();
|
|
22207
22470
|
init_aws();
|
|
@@ -22257,26 +22520,26 @@ async function checkDistributionStatus(distributionId, region) {
|
|
|
22257
22520
|
return { status: "ERROR", enabled: false, aliases: [] };
|
|
22258
22521
|
}
|
|
22259
22522
|
}
|
|
22260
|
-
async function
|
|
22523
|
+
async function cdnVerify(options) {
|
|
22261
22524
|
const startTime = Date.now();
|
|
22262
22525
|
const progress = new DeploymentProgress();
|
|
22263
|
-
clack33.intro(pc35.bold("Wraps
|
|
22526
|
+
clack33.intro(pc35.bold("Wraps CDN Verification"));
|
|
22264
22527
|
const identity = await progress.execute(
|
|
22265
|
-
"Loading
|
|
22528
|
+
"Loading CDN infrastructure",
|
|
22266
22529
|
async () => validateAWSCredentials()
|
|
22267
22530
|
);
|
|
22268
22531
|
let region = options.region || await getAWSRegion();
|
|
22269
22532
|
if (!(options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)) {
|
|
22270
|
-
const
|
|
22533
|
+
const cdnConnections = await findConnectionsWithService(
|
|
22271
22534
|
identity.accountId,
|
|
22272
|
-
"
|
|
22535
|
+
"cdn"
|
|
22273
22536
|
);
|
|
22274
|
-
if (
|
|
22275
|
-
region =
|
|
22276
|
-
} else if (
|
|
22537
|
+
if (cdnConnections.length === 1) {
|
|
22538
|
+
region = cdnConnections[0].region;
|
|
22539
|
+
} else if (cdnConnections.length > 1) {
|
|
22277
22540
|
const selectedRegion = await clack33.select({
|
|
22278
|
-
message: "Multiple
|
|
22279
|
-
options:
|
|
22541
|
+
message: "Multiple CDN deployments found. Which region?",
|
|
22542
|
+
options: cdnConnections.map((conn) => ({
|
|
22280
22543
|
value: conn.region,
|
|
22281
22544
|
label: conn.region
|
|
22282
22545
|
}))
|
|
@@ -22292,16 +22555,16 @@ async function storageVerify(options) {
|
|
|
22292
22555
|
try {
|
|
22293
22556
|
await ensurePulumiWorkDir();
|
|
22294
22557
|
const stack = await pulumi27.automation.LocalWorkspace.selectStack({
|
|
22295
|
-
stackName: `wraps-
|
|
22558
|
+
stackName: `wraps-cdn-${identity.accountId}-${region}`,
|
|
22296
22559
|
workDir: getPulumiWorkDir()
|
|
22297
22560
|
});
|
|
22298
22561
|
stackOutputs = await stack.outputs();
|
|
22299
22562
|
} catch (_error) {
|
|
22300
22563
|
progress.stop();
|
|
22301
|
-
clack33.log.error("No
|
|
22564
|
+
clack33.log.error("No CDN infrastructure found");
|
|
22302
22565
|
console.log(
|
|
22303
22566
|
`
|
|
22304
|
-
Run ${pc35.cyan("wraps
|
|
22567
|
+
Run ${pc35.cyan("wraps cdn init")} to deploy CDN infrastructure.
|
|
22305
22568
|
`
|
|
22306
22569
|
);
|
|
22307
22570
|
process.exit(1);
|
|
@@ -22384,7 +22647,7 @@ ${pc35.bold("Custom Domain DNS:")}`);
|
|
|
22384
22647
|
` ${pc35.yellow("!")} CloudFront alias not configured for ${targetDomain}`
|
|
22385
22648
|
);
|
|
22386
22649
|
clack33.log.info(
|
|
22387
|
-
` Run ${pc35.cyan("wraps
|
|
22650
|
+
` Run ${pc35.cyan("wraps cdn upgrade")} to add the custom domain to CloudFront.`
|
|
22388
22651
|
);
|
|
22389
22652
|
allPassed = false;
|
|
22390
22653
|
}
|
|
@@ -22425,12 +22688,12 @@ ${pc35.bold("Certificate Validation DNS:")}`);
|
|
|
22425
22688
|
const cdnUrl = targetDomain ? `https://${targetDomain}` : distributionDomain ? `https://${distributionDomain}` : null;
|
|
22426
22689
|
if (cdnUrl) {
|
|
22427
22690
|
clack33.log.info(`
|
|
22428
|
-
Your
|
|
22691
|
+
Your CDN is accessible at: ${pc35.cyan(cdnUrl)}`);
|
|
22429
22692
|
}
|
|
22430
22693
|
if (stackOutputsStale) {
|
|
22431
22694
|
clack33.log.info(
|
|
22432
22695
|
`
|
|
22433
|
-
${pc35.dim("Tip:")} Run ${pc35.cyan("wraps
|
|
22696
|
+
${pc35.dim("Tip:")} Run ${pc35.cyan("wraps cdn sync")} to update infrastructure state.`
|
|
22434
22697
|
);
|
|
22435
22698
|
}
|
|
22436
22699
|
} else {
|
|
@@ -22438,7 +22701,7 @@ ${pc35.dim("Tip:")} Run ${pc35.cyan("wraps storage sync")} to update infrastruct
|
|
|
22438
22701
|
if (validationRecords && validationRecords.length > 0) {
|
|
22439
22702
|
clack33.log.info("\nDNS records may take up to 48 hours to propagate.");
|
|
22440
22703
|
clack33.log.info(
|
|
22441
|
-
`Run ${pc35.cyan("wraps
|
|
22704
|
+
`Run ${pc35.cyan("wraps cdn verify")} again to check status.`
|
|
22442
22705
|
);
|
|
22443
22706
|
}
|
|
22444
22707
|
}
|
|
@@ -22600,7 +22863,7 @@ function showHelp() {
|
|
|
22600
22863
|
` ${pc38.cyan("sms")} SMS infrastructure (AWS End User Messaging)`
|
|
22601
22864
|
);
|
|
22602
22865
|
console.log(
|
|
22603
|
-
` ${pc38.cyan("
|
|
22866
|
+
` ${pc38.cyan("cdn")} CDN infrastructure (AWS S3 + CloudFront)
|
|
22604
22867
|
`
|
|
22605
22868
|
);
|
|
22606
22869
|
console.log("Email Commands:");
|
|
@@ -22649,24 +22912,24 @@ function showHelp() {
|
|
|
22649
22912
|
` ${pc38.cyan("sms destroy")} Remove SMS infrastructure
|
|
22650
22913
|
`
|
|
22651
22914
|
);
|
|
22652
|
-
console.log("
|
|
22915
|
+
console.log("CDN Commands:");
|
|
22653
22916
|
console.log(
|
|
22654
|
-
` ${pc38.cyan("
|
|
22917
|
+
` ${pc38.cyan("cdn init")} Deploy CDN infrastructure (S3 + CloudFront)`
|
|
22655
22918
|
);
|
|
22656
22919
|
console.log(
|
|
22657
|
-
` ${pc38.cyan("
|
|
22920
|
+
` ${pc38.cyan("cdn status")} Show CDN infrastructure details`
|
|
22658
22921
|
);
|
|
22659
22922
|
console.log(
|
|
22660
|
-
` ${pc38.cyan("
|
|
22923
|
+
` ${pc38.cyan("cdn verify")} Check DNS and certificate status`
|
|
22661
22924
|
);
|
|
22662
22925
|
console.log(
|
|
22663
|
-
` ${pc38.cyan("
|
|
22926
|
+
` ${pc38.cyan("cdn upgrade")} Add custom domain after cert validation`
|
|
22664
22927
|
);
|
|
22665
22928
|
console.log(
|
|
22666
|
-
` ${pc38.cyan("
|
|
22929
|
+
` ${pc38.cyan("cdn sync")} Sync infrastructure with current config`
|
|
22667
22930
|
);
|
|
22668
22931
|
console.log(
|
|
22669
|
-
` ${pc38.cyan("
|
|
22932
|
+
` ${pc38.cyan("cdn destroy")} Remove CDN infrastructure
|
|
22670
22933
|
`
|
|
22671
22934
|
);
|
|
22672
22935
|
console.log("Local Development:");
|
|
@@ -22874,8 +23137,8 @@ if (!primaryCommand) {
|
|
|
22874
23137
|
hint: "AWS End User Messaging"
|
|
22875
23138
|
},
|
|
22876
23139
|
{
|
|
22877
|
-
value: "
|
|
22878
|
-
label: "Deploy
|
|
23140
|
+
value: "cdn-init",
|
|
23141
|
+
label: "Deploy CDN",
|
|
22879
23142
|
hint: "AWS S3 + CloudFront CDN"
|
|
22880
23143
|
},
|
|
22881
23144
|
{
|
|
@@ -22933,7 +23196,7 @@ if (!primaryCommand) {
|
|
|
22933
23196
|
yes: flags.yes
|
|
22934
23197
|
});
|
|
22935
23198
|
break;
|
|
22936
|
-
case "
|
|
23199
|
+
case "cdn-init":
|
|
22937
23200
|
await init3({
|
|
22938
23201
|
provider: flags.provider,
|
|
22939
23202
|
region: flags.region,
|
|
@@ -23233,7 +23496,7 @@ Run ${pc38.cyan("wraps --help")} for available commands.
|
|
|
23233
23496
|
});
|
|
23234
23497
|
return;
|
|
23235
23498
|
}
|
|
23236
|
-
if (primaryCommand === "
|
|
23499
|
+
if (primaryCommand === "cdn" && subCommand) {
|
|
23237
23500
|
switch (subCommand) {
|
|
23238
23501
|
case "init":
|
|
23239
23502
|
await init3({
|
|
@@ -23246,36 +23509,36 @@ Run ${pc38.cyan("wraps --help")} for available commands.
|
|
|
23246
23509
|
});
|
|
23247
23510
|
break;
|
|
23248
23511
|
case "status":
|
|
23249
|
-
await
|
|
23512
|
+
await cdnStatus({
|
|
23250
23513
|
region: flags.region
|
|
23251
23514
|
});
|
|
23252
23515
|
break;
|
|
23253
23516
|
case "verify":
|
|
23254
|
-
await
|
|
23517
|
+
await cdnVerify({
|
|
23255
23518
|
region: flags.region
|
|
23256
23519
|
});
|
|
23257
23520
|
break;
|
|
23258
23521
|
case "upgrade":
|
|
23259
|
-
await
|
|
23522
|
+
await cdnUpgrade({
|
|
23260
23523
|
region: flags.region,
|
|
23261
23524
|
yes: flags.yes,
|
|
23262
23525
|
preview: flags.preview
|
|
23263
23526
|
});
|
|
23264
23527
|
break;
|
|
23265
23528
|
case "sync":
|
|
23266
|
-
await
|
|
23529
|
+
await cdnSync({
|
|
23267
23530
|
region: flags.region
|
|
23268
23531
|
});
|
|
23269
23532
|
break;
|
|
23270
23533
|
case "destroy":
|
|
23271
|
-
await
|
|
23534
|
+
await cdnDestroy({
|
|
23272
23535
|
force: flags.force,
|
|
23273
23536
|
region: flags.region,
|
|
23274
23537
|
preview: flags.preview
|
|
23275
23538
|
});
|
|
23276
23539
|
break;
|
|
23277
23540
|
default:
|
|
23278
|
-
clack36.log.error(`Unknown
|
|
23541
|
+
clack36.log.error(`Unknown cdn command: ${subCommand}`);
|
|
23279
23542
|
console.log(
|
|
23280
23543
|
`
|
|
23281
23544
|
Run ${pc38.cyan("wraps --help")} for available commands.
|
|
@@ -23283,12 +23546,12 @@ Run ${pc38.cyan("wraps --help")} for available commands.
|
|
|
23283
23546
|
);
|
|
23284
23547
|
process.exit(1);
|
|
23285
23548
|
}
|
|
23286
|
-
const
|
|
23287
|
-
const
|
|
23288
|
-
trackCommand(
|
|
23549
|
+
const cdnDuration = Date.now() - startTime;
|
|
23550
|
+
const cdnCommandName = `cdn:${subCommand}`;
|
|
23551
|
+
trackCommand(cdnCommandName, {
|
|
23289
23552
|
success: true,
|
|
23290
|
-
duration_ms:
|
|
23291
|
-
service: "
|
|
23553
|
+
duration_ms: cdnDuration,
|
|
23554
|
+
service: "cdn"
|
|
23292
23555
|
});
|
|
23293
23556
|
return;
|
|
23294
23557
|
}
|
|
@@ -23411,7 +23674,7 @@ Available commands: ${pc38.cyan("enable")}, ${pc38.cyan("disable")}, ${pc38.cyan
|
|
|
23411
23674
|
// Show help for service without subcommand
|
|
23412
23675
|
case "email":
|
|
23413
23676
|
case "sms":
|
|
23414
|
-
case "
|
|
23677
|
+
case "cdn":
|
|
23415
23678
|
case "aws":
|
|
23416
23679
|
console.log(
|
|
23417
23680
|
`
|