@wraps.dev/cli 2.18.6 → 2.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1844 -1594
- package/dist/cli.js.map +1 -1
- package/dist/console/assets/avif_enc-_C3kZEkJ.js +2 -0
- package/dist/console/assets/avif_enc_mt-CMxXiuUp.js +2 -0
- package/dist/console/assets/avif_enc_mt-CZ_pikvB.js +2 -0
- package/dist/console/assets/avif_enc_mt.worker-a9dNT6Io.js +1 -0
- package/dist/console/assets/index-BGDIROiI.css +1 -0
- package/dist/console/assets/{index-CNmw7YHP.js → index-C0zX5_O9.js} +1 -1
- package/dist/console/assets/{index-Ccg-KFP7.js → index-CRudvOG_.js} +1 -1
- package/dist/console/assets/index-Cto6vYr9.js +2 -0
- package/dist/console/assets/index-dD2Skj3E.js +127 -0
- package/dist/console/assets/{optimizer.worker-CeufIUo5.js → optimizer.worker-DZcD7Kza.js} +1 -1
- package/dist/console/index.html +2 -2
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/inbound-processor/.bundled +1 -1
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/package.json +1 -1
- package/dist/console/assets/avif_enc-GHSQWwbh.js +0 -2
- package/dist/console/assets/avif_enc_mt-BB9NYOXn.js +0 -2
- package/dist/console/assets/avif_enc_mt-DL2yi-mK.js +0 -2
- package/dist/console/assets/avif_enc_mt.worker-DASj3syc.js +0 -1
- package/dist/console/assets/index-41T2hONy.js +0 -497
- package/dist/console/assets/index-Cjum_-Wu.js +0 -2
- package/dist/console/assets/index-DF7apuYC.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -66,1533 +66,736 @@ var init_ci_detection = __esm({
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
// src/utils/shared/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
deleteMetadata: () => deleteMetadata,
|
|
74
|
-
downloadMetadata: () => downloadMetadata,
|
|
75
|
-
ensureStateBucket: () => ensureStateBucket,
|
|
76
|
-
getS3BackendUrl: () => getS3BackendUrl,
|
|
77
|
-
getStateBucketName: () => getStateBucketName,
|
|
78
|
-
migrateLocalPulumiState: () => migrateLocalPulumiState,
|
|
79
|
-
needsMigration: () => needsMigration,
|
|
80
|
-
stateBucketExists: () => stateBucketExists,
|
|
81
|
-
uploadMetadata: () => uploadMetadata
|
|
82
|
-
});
|
|
83
|
-
import { existsSync, statSync } from "fs";
|
|
84
|
-
import { readdir, writeFile } from "fs/promises";
|
|
69
|
+
// src/utils/shared/aws-detection.ts
|
|
70
|
+
import { execSync } from "child_process";
|
|
71
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
72
|
+
import { homedir } from "os";
|
|
85
73
|
import { join } from "path";
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
75
|
+
async function isAWSCLIInstalled() {
|
|
76
|
+
try {
|
|
77
|
+
execSync("aws --version", { stdio: "pipe" });
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
88
80
|
return false;
|
|
89
81
|
}
|
|
90
|
-
const metadataError = error;
|
|
91
|
-
return metadataError.$metadata?.httpStatusCode === 404;
|
|
92
|
-
}
|
|
93
|
-
function getStateBucketName(accountId, region) {
|
|
94
|
-
return `wraps-state-${accountId}-${region}`;
|
|
95
|
-
}
|
|
96
|
-
function getS3BackendUrl(accountId, region) {
|
|
97
|
-
return `s3://${getStateBucketName(accountId, region)}`;
|
|
98
82
|
}
|
|
99
|
-
async function
|
|
100
|
-
const { S3Client: S3Client2, HeadBucketCommand } = await import("@aws-sdk/client-s3");
|
|
101
|
-
const client = new S3Client2({ region });
|
|
102
|
-
const bucketName = getStateBucketName(accountId, region);
|
|
83
|
+
async function getAWSCLIVersion() {
|
|
103
84
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
throw error;
|
|
85
|
+
const output3 = execSync("aws --version", { encoding: "utf-8" });
|
|
86
|
+
const match = output3.match(/aws-cli\/(\d+\.\d+\.\d+)/);
|
|
87
|
+
return match ? match[1] : null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
111
90
|
}
|
|
112
91
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
} catch (error) {
|
|
129
|
-
const isNotFound = error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchBucket" || has404StatusCode(error));
|
|
130
|
-
if (!isNotFound) {
|
|
131
|
-
throw error;
|
|
92
|
+
function detectCredentialSource() {
|
|
93
|
+
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
94
|
+
return "environment";
|
|
95
|
+
}
|
|
96
|
+
if (process.env.AWS_SSO_ACCOUNT_ID || process.env.AWS_SSO_SESSION) {
|
|
97
|
+
return "sso";
|
|
98
|
+
}
|
|
99
|
+
if (process.env.AWS_PROFILE) {
|
|
100
|
+
return "profile";
|
|
101
|
+
}
|
|
102
|
+
const credentialsPath = join(homedir(), ".aws", "credentials");
|
|
103
|
+
if (existsSync(credentialsPath)) {
|
|
104
|
+
const content = readFileSync(credentialsPath, "utf-8");
|
|
105
|
+
if (content.includes("[default]")) {
|
|
106
|
+
return "profile";
|
|
132
107
|
}
|
|
133
108
|
}
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
LocationConstraint: region
|
|
138
|
-
};
|
|
109
|
+
const ssoCachePath = join(homedir(), ".aws", "sso", "cache");
|
|
110
|
+
if (existsSync(ssoCachePath)) {
|
|
111
|
+
return "sso";
|
|
139
112
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
ServerSideEncryptionConfiguration: {
|
|
145
|
-
Rules: [
|
|
146
|
-
{
|
|
147
|
-
ApplyServerSideEncryptionByDefault: {
|
|
148
|
-
SSEAlgorithm: "AES256"
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
]
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
);
|
|
155
|
-
await client.send(
|
|
156
|
-
new PutBucketVersioningCommand({
|
|
157
|
-
Bucket: bucketName,
|
|
158
|
-
VersioningConfiguration: {
|
|
159
|
-
Status: "Enabled"
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
);
|
|
163
|
-
await client.send(
|
|
164
|
-
new PutPublicAccessBlockCommand({
|
|
165
|
-
Bucket: bucketName,
|
|
166
|
-
PublicAccessBlockConfiguration: {
|
|
167
|
-
BlockPublicAcls: true,
|
|
168
|
-
BlockPublicPolicy: true,
|
|
169
|
-
IgnorePublicAcls: true,
|
|
170
|
-
RestrictPublicBuckets: true
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
);
|
|
174
|
-
await client.send(
|
|
175
|
-
new PutBucketTaggingCommand({
|
|
176
|
-
Bucket: bucketName,
|
|
177
|
-
Tagging: {
|
|
178
|
-
TagSet: [
|
|
179
|
-
{ Key: "ManagedBy", Value: "wraps-cli" },
|
|
180
|
-
{ Key: "Purpose", Value: "state" }
|
|
181
|
-
]
|
|
182
|
-
}
|
|
183
|
-
})
|
|
184
|
-
);
|
|
185
|
-
return bucketName;
|
|
186
|
-
}
|
|
187
|
-
async function uploadMetadata(bucketName, metadata) {
|
|
188
|
-
const { S3Client: S3Client2, PutObjectCommand } = await import("@aws-sdk/client-s3");
|
|
189
|
-
const client = new S3Client2({ region: metadata.region });
|
|
190
|
-
const key = `metadata/${metadata.accountId}-${metadata.region}.json`;
|
|
191
|
-
await client.send(
|
|
192
|
-
new PutObjectCommand({
|
|
193
|
-
Bucket: bucketName,
|
|
194
|
-
Key: key,
|
|
195
|
-
Body: JSON.stringify(metadata, null, 2),
|
|
196
|
-
ContentType: "application/json"
|
|
197
|
-
})
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
async function deleteMetadata(bucketName, accountId, region) {
|
|
201
|
-
const { S3Client: S3Client2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
|
202
|
-
const client = new S3Client2({ region });
|
|
203
|
-
const key = `metadata/${accountId}-${region}.json`;
|
|
204
|
-
await client.send(
|
|
205
|
-
new DeleteObjectCommand({
|
|
206
|
-
Bucket: bucketName,
|
|
207
|
-
Key: key
|
|
208
|
-
})
|
|
209
|
-
);
|
|
113
|
+
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_EXECUTION_ENV) {
|
|
114
|
+
return "instance";
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
210
117
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const bucketName = getStateBucketName(accountId, region);
|
|
215
|
-
const prefix = ".pulumi/locks/";
|
|
216
|
-
const response = await client.send(
|
|
217
|
-
new ListObjectsV2Command2({ Bucket: bucketName, Prefix: prefix })
|
|
218
|
-
);
|
|
219
|
-
const lockObjects = response.Contents ?? [];
|
|
220
|
-
if (lockObjects.length === 0) {
|
|
221
|
-
return 0;
|
|
118
|
+
function detectHostingProvider() {
|
|
119
|
+
if (process.env.VERCEL || process.env.VERCEL_ENV) {
|
|
120
|
+
return "vercel";
|
|
222
121
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
await client.send(
|
|
226
|
-
new DeleteObjectCommand({ Bucket: bucketName, Key: obj.Key })
|
|
227
|
-
);
|
|
228
|
-
}
|
|
122
|
+
if (process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PROJECT_ID) {
|
|
123
|
+
return "railway";
|
|
229
124
|
}
|
|
230
|
-
|
|
125
|
+
if (process.env.NETLIFY || process.env.NETLIFY_DEV) {
|
|
126
|
+
return "netlify";
|
|
127
|
+
}
|
|
128
|
+
if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV || process.env.ECS_CONTAINER_METADATA_URI || process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
|
|
129
|
+
return "aws";
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
231
132
|
}
|
|
232
|
-
async function
|
|
233
|
-
const { S3Client: S3Client2, GetObjectCommand: GetObjectCommand2 } = await import("@aws-sdk/client-s3");
|
|
234
|
-
const client = new S3Client2({ region });
|
|
235
|
-
const key = `metadata/${accountId}-${region}.json`;
|
|
133
|
+
async function validateCredentials() {
|
|
236
134
|
try {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
const body = await response.Body?.transformToString();
|
|
244
|
-
if (!body) {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
return JSON.parse(body);
|
|
248
|
-
} catch (error) {
|
|
249
|
-
if (error instanceof Error && (error.name === "NoSuchKey" || has404StatusCode(error))) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
throw error;
|
|
135
|
+
const sts = new STSClient({ region: "us-east-1" });
|
|
136
|
+
const identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
137
|
+
return identity.Account || null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
253
140
|
}
|
|
254
141
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
142
|
+
function getCurrentProfile() {
|
|
143
|
+
return process.env.AWS_PROFILE || "default";
|
|
144
|
+
}
|
|
145
|
+
function getCurrentRegion() {
|
|
146
|
+
if (process.env.AWS_REGION) {
|
|
147
|
+
return process.env.AWS_REGION;
|
|
259
148
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return false;
|
|
149
|
+
if (process.env.AWS_DEFAULT_REGION) {
|
|
150
|
+
return process.env.AWS_DEFAULT_REGION;
|
|
263
151
|
}
|
|
264
152
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const matching = files.filter(
|
|
271
|
-
(f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
|
|
272
|
-
);
|
|
273
|
-
if (matching.length > 0) {
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return false;
|
|
153
|
+
const region = execSync("aws configure get region", {
|
|
154
|
+
encoding: "utf-8",
|
|
155
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
156
|
+
}).trim();
|
|
157
|
+
return region || null;
|
|
279
158
|
} catch {
|
|
280
|
-
return
|
|
159
|
+
return null;
|
|
281
160
|
}
|
|
282
161
|
}
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
});
|
|
304
|
-
const state = await localStack.exportStack();
|
|
305
|
-
const s3Stack = await pulumi31.LocalWorkspace.createOrSelectStack(
|
|
306
|
-
{
|
|
307
|
-
stackName,
|
|
308
|
-
projectName,
|
|
309
|
-
program: async () => ({})
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
workDir: localPulumiDir,
|
|
313
|
-
envVars: {
|
|
314
|
-
PULUMI_BACKEND_URL: `s3://${bucketName}`,
|
|
315
|
-
PULUMI_CONFIG_PASSPHRASE: ""
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
);
|
|
319
|
-
await s3Stack.importStack(state);
|
|
320
|
-
} catch (error) {
|
|
321
|
-
console.error(
|
|
322
|
-
`Warning: Failed to migrate stack ${stackName}: ${error instanceof Error ? error.message : error}`
|
|
323
|
-
);
|
|
162
|
+
function parseSSOProfiles() {
|
|
163
|
+
const configPath = join(homedir(), ".aws", "config");
|
|
164
|
+
if (!existsSync(configPath)) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const content = readFileSync(configPath, "utf-8");
|
|
168
|
+
const profiles = [];
|
|
169
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
170
|
+
const sessionSections = content.split(/^\[/m).filter(Boolean);
|
|
171
|
+
for (const section of sessionSections) {
|
|
172
|
+
const lines = section.split("\n");
|
|
173
|
+
const header = lines[0]?.replace("]", "").trim();
|
|
174
|
+
if (header?.startsWith("sso-session ")) {
|
|
175
|
+
const sessionName = header.replace("sso-session ", "");
|
|
176
|
+
const config2 = {};
|
|
177
|
+
for (const line of lines.slice(1)) {
|
|
178
|
+
const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
|
|
179
|
+
if (match) {
|
|
180
|
+
config2[match[1]] = match[2];
|
|
181
|
+
}
|
|
324
182
|
}
|
|
183
|
+
sessionMap.set(sessionName, {
|
|
184
|
+
startUrl: config2.sso_start_url || "",
|
|
185
|
+
region: config2.sso_region || ""
|
|
186
|
+
});
|
|
325
187
|
}
|
|
326
188
|
}
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
189
|
+
const sections = content.split(/^\[/m).filter(Boolean);
|
|
190
|
+
for (const section of sections) {
|
|
191
|
+
const lines = section.split("\n");
|
|
192
|
+
const header = lines[0]?.replace("]", "").trim();
|
|
193
|
+
if (!header?.startsWith("profile ") && header !== "default") {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const profileName = header === "default" ? "default" : header.replace("profile ", "");
|
|
197
|
+
const config2 = {};
|
|
198
|
+
for (const line of lines.slice(1)) {
|
|
199
|
+
const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
|
|
200
|
+
if (match) {
|
|
201
|
+
config2[match[1]] = match[2];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (config2.sso_start_url || config2.sso_session) {
|
|
205
|
+
let ssoStartUrl = config2.sso_start_url || "";
|
|
206
|
+
let ssoRegion = config2.sso_region || "";
|
|
207
|
+
if (config2.sso_session) {
|
|
208
|
+
const session = sessionMap.get(config2.sso_session);
|
|
209
|
+
if (session) {
|
|
210
|
+
ssoStartUrl = ssoStartUrl || session.startUrl;
|
|
211
|
+
ssoRegion = ssoRegion || session.region;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
profiles.push({
|
|
215
|
+
name: profileName,
|
|
216
|
+
ssoStartUrl,
|
|
217
|
+
ssoRegion,
|
|
218
|
+
ssoAccountId: config2.sso_account_id || "",
|
|
219
|
+
ssoRoleName: config2.sso_role_name || "",
|
|
220
|
+
region: config2.region,
|
|
221
|
+
ssoSession: config2.sso_session
|
|
222
|
+
});
|
|
223
|
+
}
|
|
360
224
|
}
|
|
225
|
+
return profiles;
|
|
361
226
|
}
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
if (!
|
|
365
|
-
return
|
|
227
|
+
function parseSSOSessions() {
|
|
228
|
+
const configPath = join(homedir(), ".aws", "config");
|
|
229
|
+
if (!existsSync(configPath)) {
|
|
230
|
+
return [];
|
|
366
231
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
232
|
+
const content = readFileSync(configPath, "utf-8");
|
|
233
|
+
const sessions = [];
|
|
234
|
+
const sections = content.split(/^\[/m).filter(Boolean);
|
|
235
|
+
for (const section of sections) {
|
|
236
|
+
const lines = section.split("\n");
|
|
237
|
+
const header = lines[0]?.replace("]", "").trim();
|
|
238
|
+
if (!header?.startsWith("sso-session ")) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const sessionName = header.replace("sso-session ", "");
|
|
242
|
+
const config2 = {};
|
|
243
|
+
for (const line of lines.slice(1)) {
|
|
244
|
+
const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
|
|
245
|
+
if (match) {
|
|
246
|
+
config2[match[1]] = match[2];
|
|
377
247
|
}
|
|
378
248
|
}
|
|
249
|
+
sessions.push({
|
|
250
|
+
name: sessionName,
|
|
251
|
+
ssoStartUrl: config2.sso_start_url || "",
|
|
252
|
+
ssoRegion: config2.sso_region || "",
|
|
253
|
+
ssoRegistrationScopes: config2.sso_registration_scopes?.split(",").map((s) => s.trim())
|
|
254
|
+
});
|
|
379
255
|
}
|
|
380
|
-
|
|
381
|
-
return count;
|
|
256
|
+
return sessions;
|
|
382
257
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
258
|
+
function checkSSOTokenStatus(startUrl) {
|
|
259
|
+
const ssoCachePath = join(homedir(), ".aws", "sso", "cache");
|
|
260
|
+
if (!existsSync(ssoCachePath)) {
|
|
261
|
+
return {
|
|
262
|
+
valid: false,
|
|
263
|
+
expiresAt: null,
|
|
264
|
+
expired: true,
|
|
265
|
+
minutesRemaining: null,
|
|
266
|
+
startUrl: null
|
|
267
|
+
};
|
|
388
268
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
} = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
|
|
399
|
-
const bucketName = await ensureStateBucket2(
|
|
400
|
-
options.accountId,
|
|
401
|
-
options.region
|
|
402
|
-
);
|
|
403
|
-
const shouldMigrate = await needsMigration2(
|
|
404
|
-
pulumiDir,
|
|
405
|
-
options.accountId,
|
|
406
|
-
options.region
|
|
407
|
-
);
|
|
408
|
-
if (shouldMigrate) {
|
|
409
|
-
process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
|
|
410
|
-
await migrateLocalPulumiState2(
|
|
411
|
-
pulumiDir,
|
|
412
|
-
bucketName,
|
|
413
|
-
options.accountId,
|
|
414
|
-
options.region
|
|
415
|
-
);
|
|
269
|
+
try {
|
|
270
|
+
const cacheFiles = readdirSync(ssoCachePath).filter(
|
|
271
|
+
(f) => f.endsWith(".json")
|
|
272
|
+
);
|
|
273
|
+
for (const file of cacheFiles) {
|
|
274
|
+
const content = readFileSync(join(ssoCachePath, file), "utf-8");
|
|
275
|
+
const token = JSON.parse(content);
|
|
276
|
+
if (!(token.accessToken && token.expiresAt)) {
|
|
277
|
+
continue;
|
|
416
278
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
`S3 state backend unavailable (${error instanceof Error ? error.message : error}). Using local state.`
|
|
279
|
+
if (startUrl && token.startUrl !== startUrl) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const expiresAt = new Date(token.expiresAt);
|
|
283
|
+
const now = /* @__PURE__ */ new Date();
|
|
284
|
+
const expired = expiresAt <= now;
|
|
285
|
+
const minutesRemaining = Math.floor(
|
|
286
|
+
(expiresAt.getTime() - now.getTime()) / 6e4
|
|
426
287
|
);
|
|
288
|
+
return {
|
|
289
|
+
valid: !expired,
|
|
290
|
+
expiresAt,
|
|
291
|
+
expired,
|
|
292
|
+
minutesRemaining,
|
|
293
|
+
startUrl: token.startUrl || null
|
|
294
|
+
};
|
|
427
295
|
}
|
|
296
|
+
} catch {
|
|
428
297
|
}
|
|
429
|
-
|
|
298
|
+
return {
|
|
299
|
+
valid: false,
|
|
300
|
+
expiresAt: null,
|
|
301
|
+
expired: true,
|
|
302
|
+
minutesRemaining: null,
|
|
303
|
+
startUrl: null
|
|
304
|
+
};
|
|
430
305
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
init_esm_shims();
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// src/utils/shared/config.ts
|
|
439
|
-
import { existsSync as existsSync3 } from "fs";
|
|
440
|
-
import { chmod, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
441
|
-
import { join as join3 } from "path";
|
|
442
|
-
function getApiBaseUrl() {
|
|
443
|
-
return process.env.WRAPS_API_URL || "https://api.wraps.dev";
|
|
306
|
+
function getActiveSSOProfile(profiles) {
|
|
307
|
+
const currentProfile = process.env.AWS_PROFILE || "default";
|
|
308
|
+
return profiles.find((p) => p.name === currentProfile) || null;
|
|
444
309
|
}
|
|
445
|
-
function
|
|
446
|
-
|
|
310
|
+
function getSSOLoginCommand(profile) {
|
|
311
|
+
if (profile && profile !== "default") {
|
|
312
|
+
return `aws sso login --profile ${profile}`;
|
|
313
|
+
}
|
|
314
|
+
return "aws sso login";
|
|
447
315
|
}
|
|
448
|
-
function
|
|
449
|
-
return
|
|
450
|
-
}
|
|
451
|
-
async function readAuthConfig() {
|
|
452
|
-
const path3 = getConfigPath();
|
|
453
|
-
if (!existsSync3(path3)) {
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
try {
|
|
457
|
-
const content = await readFile(path3, "utf-8");
|
|
458
|
-
return JSON.parse(content);
|
|
459
|
-
} catch {
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
316
|
+
function formatSSOProfile(profile) {
|
|
317
|
+
return `${profile.name} (${profile.ssoAccountId} / ${profile.ssoRoleName})`;
|
|
462
318
|
}
|
|
463
|
-
async function
|
|
464
|
-
await
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
319
|
+
async function detectAWSState() {
|
|
320
|
+
const [cliInstalled, cliVersion, accountId] = await Promise.all([
|
|
321
|
+
isAWSCLIInstalled(),
|
|
322
|
+
getAWSCLIVersion(),
|
|
323
|
+
validateCredentials()
|
|
324
|
+
]);
|
|
325
|
+
const credentialSource = detectCredentialSource();
|
|
326
|
+
const detectedProvider = detectHostingProvider();
|
|
327
|
+
const region = getCurrentRegion();
|
|
328
|
+
const profileName = getCurrentProfile();
|
|
329
|
+
const ssoProfiles = parseSSOProfiles();
|
|
330
|
+
const ssoSessions = parseSSOSessions();
|
|
331
|
+
const activeProfile = getActiveSSOProfile(ssoProfiles);
|
|
332
|
+
const tokenStatus = ssoProfiles.length > 0 ? checkSSOTokenStatus(activeProfile?.ssoStartUrl) : null;
|
|
333
|
+
const isUsingSSO = credentialSource === "sso" || activeProfile !== null && accountId !== null;
|
|
334
|
+
return {
|
|
335
|
+
cliInstalled,
|
|
336
|
+
cliVersion,
|
|
337
|
+
credentialsConfigured: accountId !== null,
|
|
338
|
+
credentialSource: isUsingSSO ? "sso" : accountId !== null ? credentialSource : null,
|
|
339
|
+
profileName,
|
|
340
|
+
accountId,
|
|
341
|
+
detectedProvider,
|
|
342
|
+
region,
|
|
343
|
+
sso: {
|
|
344
|
+
configured: ssoProfiles.length > 0,
|
|
345
|
+
profiles: ssoProfiles,
|
|
346
|
+
sessions: ssoSessions,
|
|
347
|
+
tokenStatus,
|
|
348
|
+
activeProfile: isUsingSSO ? activeProfile : null
|
|
349
|
+
}
|
|
350
|
+
};
|
|
470
351
|
}
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
existing.auth = void 0;
|
|
475
|
-
await saveAuthConfig(existing);
|
|
476
|
-
}
|
|
352
|
+
function hasCredentialsFile() {
|
|
353
|
+
const credentialsPath = join(homedir(), ".aws", "credentials");
|
|
354
|
+
return existsSync(credentialsPath);
|
|
477
355
|
}
|
|
478
|
-
function
|
|
479
|
-
|
|
356
|
+
function hasConfigFile() {
|
|
357
|
+
const configPath = join(homedir(), ".aws", "config");
|
|
358
|
+
return existsSync(configPath);
|
|
480
359
|
}
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
360
|
+
function getConfiguredProfiles() {
|
|
361
|
+
const profiles = [];
|
|
362
|
+
const credentialsPath = join(homedir(), ".aws", "credentials");
|
|
363
|
+
if (existsSync(credentialsPath)) {
|
|
364
|
+
const content = readFileSync(credentialsPath, "utf-8");
|
|
365
|
+
const matches = content.matchAll(/\[([^\]]+)\]/g);
|
|
366
|
+
for (const match of matches) {
|
|
367
|
+
profiles.push(match[1]);
|
|
368
|
+
}
|
|
489
369
|
}
|
|
490
|
-
|
|
491
|
-
|
|
370
|
+
const configPath = join(homedir(), ".aws", "config");
|
|
371
|
+
if (existsSync(configPath)) {
|
|
372
|
+
const content = readFileSync(configPath, "utf-8");
|
|
373
|
+
const matches = content.matchAll(/\[profile ([^\]]+)\]/g);
|
|
374
|
+
for (const match of matches) {
|
|
375
|
+
if (!profiles.includes(match[1])) {
|
|
376
|
+
profiles.push(match[1]);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
492
379
|
}
|
|
493
|
-
return
|
|
380
|
+
return profiles;
|
|
494
381
|
}
|
|
495
|
-
var
|
|
496
|
-
|
|
497
|
-
"src/utils/shared/config.ts"() {
|
|
382
|
+
var init_aws_detection = __esm({
|
|
383
|
+
"src/utils/shared/aws-detection.ts"() {
|
|
498
384
|
"use strict";
|
|
499
385
|
init_esm_shims();
|
|
500
|
-
init_fs();
|
|
501
|
-
CONFIG_FILE = "config.json";
|
|
502
386
|
}
|
|
503
387
|
});
|
|
504
388
|
|
|
505
|
-
// src/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
389
|
+
// src/utils/shared/json-output.ts
|
|
390
|
+
function isJsonMode() {
|
|
391
|
+
return _jsonMode;
|
|
392
|
+
}
|
|
393
|
+
function setJsonMode(enabled) {
|
|
394
|
+
_jsonMode = enabled;
|
|
395
|
+
}
|
|
396
|
+
function jsonSuccess(command, data) {
|
|
397
|
+
const output3 = { success: true, command, data };
|
|
398
|
+
console.log(JSON.stringify(output3));
|
|
399
|
+
}
|
|
400
|
+
function jsonError(command, error) {
|
|
401
|
+
const output3 = { success: false, command, error };
|
|
402
|
+
console.log(JSON.stringify(output3));
|
|
403
|
+
}
|
|
404
|
+
var _jsonMode;
|
|
405
|
+
var init_json_output = __esm({
|
|
406
|
+
"src/utils/shared/json-output.ts"() {
|
|
511
407
|
"use strict";
|
|
512
408
|
init_esm_shims();
|
|
513
|
-
|
|
514
|
-
enabled: true,
|
|
515
|
-
anonymousId: uuidv4(),
|
|
516
|
-
notificationShown: false
|
|
517
|
-
};
|
|
518
|
-
TelemetryConfigManager = class {
|
|
519
|
-
config;
|
|
520
|
-
constructor(options) {
|
|
521
|
-
this.config = new Conf({
|
|
522
|
-
projectName: "wraps",
|
|
523
|
-
configName: "telemetry",
|
|
524
|
-
defaults: CONFIG_DEFAULTS,
|
|
525
|
-
cwd: options?.cwd
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Check if telemetry is enabled
|
|
530
|
-
*/
|
|
531
|
-
isEnabled() {
|
|
532
|
-
return this.config.get("enabled");
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Enable or disable telemetry
|
|
536
|
-
*/
|
|
537
|
-
setEnabled(enabled) {
|
|
538
|
-
this.config.set("enabled", enabled);
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* Get the anonymous user ID
|
|
542
|
-
*/
|
|
543
|
-
getAnonymousId() {
|
|
544
|
-
return this.config.get("anonymousId");
|
|
545
|
-
}
|
|
546
|
-
/**
|
|
547
|
-
* Check if the first-run notification has been shown
|
|
548
|
-
*/
|
|
549
|
-
hasShownNotification() {
|
|
550
|
-
return this.config.get("notificationShown");
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Mark the first-run notification as shown
|
|
554
|
-
*/
|
|
555
|
-
markNotificationShown() {
|
|
556
|
-
this.config.set("notificationShown", true);
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Get the full path to the configuration file
|
|
560
|
-
*/
|
|
561
|
-
getConfigPath() {
|
|
562
|
-
return this.config.path;
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Reset configuration to defaults
|
|
566
|
-
*/
|
|
567
|
-
reset() {
|
|
568
|
-
this.config.clear();
|
|
569
|
-
this.config.set({
|
|
570
|
-
...CONFIG_DEFAULTS,
|
|
571
|
-
anonymousId: uuidv4()
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
};
|
|
409
|
+
_jsonMode = false;
|
|
575
410
|
}
|
|
576
411
|
});
|
|
577
412
|
|
|
578
|
-
// src/
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
413
|
+
// src/utils/shared/errors.ts
|
|
414
|
+
var errors_exports = {};
|
|
415
|
+
__export(errors_exports, {
|
|
416
|
+
WrapsError: () => WrapsError,
|
|
417
|
+
awsErrorToWrapsError: () => awsErrorToWrapsError,
|
|
418
|
+
classifyDNSError: () => classifyDNSError,
|
|
419
|
+
errors: () => errors,
|
|
420
|
+
handleCLIError: () => handleCLIError,
|
|
421
|
+
isAWSError: () => isAWSError,
|
|
422
|
+
isAWSNotFoundError: () => isAWSNotFoundError,
|
|
423
|
+
isPulumiError: () => isPulumiError,
|
|
424
|
+
parseAWSError: () => parseAWSError,
|
|
425
|
+
parsePulumiError: () => parsePulumiError,
|
|
426
|
+
redactSensitiveValues: () => redactSensitiveValues,
|
|
427
|
+
sanitizeErrorMessage: () => sanitizeErrorMessage
|
|
428
|
+
});
|
|
429
|
+
import * as clack from "@clack/prompts";
|
|
582
430
|
import pc from "picocolors";
|
|
583
|
-
function
|
|
584
|
-
if (!
|
|
585
|
-
|
|
431
|
+
function isAWSError(error) {
|
|
432
|
+
if (!(error instanceof Error)) {
|
|
433
|
+
return false;
|
|
586
434
|
}
|
|
587
|
-
|
|
435
|
+
const awsErrorNames = [
|
|
436
|
+
"ExpiredTokenException",
|
|
437
|
+
"InvalidClientTokenId",
|
|
438
|
+
"AccessDenied",
|
|
439
|
+
"AccessDeniedException",
|
|
440
|
+
"UnauthorizedAccess",
|
|
441
|
+
"InvalidAccessKeyId",
|
|
442
|
+
"SignatureDoesNotMatch",
|
|
443
|
+
"UnrecognizedClientException",
|
|
444
|
+
"CredentialsError",
|
|
445
|
+
"TokenRefreshRequired",
|
|
446
|
+
"SSOTokenExpired"
|
|
447
|
+
];
|
|
448
|
+
return awsErrorNames.includes(error.name) || "$metadata" in error;
|
|
588
449
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
"use strict";
|
|
593
|
-
init_esm_shims();
|
|
594
|
-
init_ci_detection();
|
|
595
|
-
init_config();
|
|
596
|
-
init_config2();
|
|
597
|
-
DEFAULT_ENDPOINT = process.env.WRAPS_TELEMETRY_URL || "https://wraps.dev/api/telemetry";
|
|
598
|
-
DEFAULT_TIMEOUT = 2e3;
|
|
599
|
-
TelemetryClient = class {
|
|
600
|
-
config;
|
|
601
|
-
endpoint;
|
|
602
|
-
timeout;
|
|
603
|
-
debug;
|
|
604
|
-
enabled;
|
|
605
|
-
eventQueue = [];
|
|
606
|
-
flushTimer;
|
|
607
|
-
hasShownFooter = false;
|
|
608
|
-
userId;
|
|
609
|
-
userIdResolved = false;
|
|
610
|
-
constructor(options = {}) {
|
|
611
|
-
this.config = new TelemetryConfigManager();
|
|
612
|
-
this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
|
|
613
|
-
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
614
|
-
this.debug = options.debug || process.env.WRAPS_TELEMETRY_DEBUG === "1";
|
|
615
|
-
this.enabled = this.shouldBeEnabled();
|
|
616
|
-
this.resolveUserId();
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Resolve authenticated user identity from CLI auth config.
|
|
620
|
-
* Uses the first organization ID as the user identifier,
|
|
621
|
-
* linking CLI telemetry to the same org tracked on the web dashboard.
|
|
622
|
-
*/
|
|
623
|
-
async resolveUserId() {
|
|
624
|
-
try {
|
|
625
|
-
const config2 = await readAuthConfig();
|
|
626
|
-
if (config2?.auth?.token && config2.auth.organizations?.length) {
|
|
627
|
-
this.userId = config2.auth.organizations[0].id;
|
|
628
|
-
}
|
|
629
|
-
} catch {
|
|
630
|
-
} finally {
|
|
631
|
-
this.userIdResolved = true;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Determine if telemetry should be enabled based on environment and config
|
|
636
|
-
*/
|
|
637
|
-
shouldBeEnabled() {
|
|
638
|
-
if (process.env.DO_NOT_TRACK === "1") {
|
|
639
|
-
return false;
|
|
640
|
-
}
|
|
641
|
-
if (process.env.WRAPS_TELEMETRY_DISABLED === "1") {
|
|
642
|
-
return false;
|
|
643
|
-
}
|
|
644
|
-
if (isCI()) {
|
|
645
|
-
return false;
|
|
646
|
-
}
|
|
647
|
-
if (!this.config.isEnabled()) {
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
return true;
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Track an event
|
|
654
|
-
*
|
|
655
|
-
* @param event - Event name in format "category:action" (e.g., "command:init")
|
|
656
|
-
* @param properties - Additional event properties (no PII)
|
|
657
|
-
*/
|
|
658
|
-
track(event, properties) {
|
|
659
|
-
const telemetryEvent = {
|
|
660
|
-
event,
|
|
661
|
-
properties: {
|
|
662
|
-
...properties,
|
|
663
|
-
cli_version: this.getCLIVersion(),
|
|
664
|
-
os: process.platform,
|
|
665
|
-
node_version: process.version,
|
|
666
|
-
ci: isCI()
|
|
667
|
-
},
|
|
668
|
-
anonymousId: this.config.getAnonymousId(),
|
|
669
|
-
...this.userId ? { userId: this.userId } : {},
|
|
670
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
671
|
-
};
|
|
672
|
-
if (this.debug) {
|
|
673
|
-
console.log(
|
|
674
|
-
"[Telemetry Debug] Event:",
|
|
675
|
-
JSON.stringify(telemetryEvent, null, 2)
|
|
676
|
-
);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
if (!this.enabled) {
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
this.eventQueue.push(telemetryEvent);
|
|
683
|
-
if (this.flushTimer) {
|
|
684
|
-
clearTimeout(this.flushTimer);
|
|
685
|
-
}
|
|
686
|
-
this.flushTimer = setTimeout(() => this.flush(), 100);
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Flush queued events to server
|
|
690
|
-
*/
|
|
691
|
-
async flush() {
|
|
692
|
-
if (this.eventQueue.length === 0) {
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
const eventsToSend = [...this.eventQueue];
|
|
696
|
-
this.eventQueue = [];
|
|
697
|
-
try {
|
|
698
|
-
const controller = new AbortController();
|
|
699
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
700
|
-
const requestBody = {
|
|
701
|
-
events: eventsToSend,
|
|
702
|
-
batch: true
|
|
703
|
-
};
|
|
704
|
-
await fetch(this.endpoint, {
|
|
705
|
-
method: "POST",
|
|
706
|
-
headers: {
|
|
707
|
-
"Content-Type": "application/json"
|
|
708
|
-
},
|
|
709
|
-
body: JSON.stringify(requestBody),
|
|
710
|
-
signal: controller.signal
|
|
711
|
-
});
|
|
712
|
-
clearTimeout(timeoutId);
|
|
713
|
-
} catch (error) {
|
|
714
|
-
if (this.debug) {
|
|
715
|
-
console.error("[Telemetry Debug] Failed to send events:", error);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Flush and wait for all events to be sent
|
|
721
|
-
* Should be called before CLI exits
|
|
722
|
-
*/
|
|
723
|
-
async shutdown() {
|
|
724
|
-
if (this.flushTimer) {
|
|
725
|
-
clearTimeout(this.flushTimer);
|
|
726
|
-
}
|
|
727
|
-
if (!this.userIdResolved) {
|
|
728
|
-
await new Promise((resolve) => {
|
|
729
|
-
const check2 = () => {
|
|
730
|
-
if (this.userIdResolved) {
|
|
731
|
-
return resolve();
|
|
732
|
-
}
|
|
733
|
-
setTimeout(check2, 10);
|
|
734
|
-
};
|
|
735
|
-
check2();
|
|
736
|
-
setTimeout(resolve, 100);
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
if (this.userId) {
|
|
740
|
-
for (const evt of this.eventQueue) {
|
|
741
|
-
if (!evt.userId) {
|
|
742
|
-
evt.userId = this.userId;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
await this.flush();
|
|
747
|
-
}
|
|
748
|
-
/**
|
|
749
|
-
* Enable telemetry.
|
|
750
|
-
* Returns null if enabled, or a string describing why an env override prevented it.
|
|
751
|
-
*/
|
|
752
|
-
enable() {
|
|
753
|
-
this.config.setEnabled(true);
|
|
754
|
-
const override = this.getEnvOverride();
|
|
755
|
-
if (override) {
|
|
756
|
-
this.enabled = false;
|
|
757
|
-
return override;
|
|
758
|
-
}
|
|
759
|
-
this.enabled = true;
|
|
760
|
-
return null;
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Check if an environment variable is overriding the config.
|
|
764
|
-
* Returns a human-readable reason, or null if no override.
|
|
765
|
-
*/
|
|
766
|
-
getEnvOverride() {
|
|
767
|
-
if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
|
|
768
|
-
return "DO_NOT_TRACK environment variable is set";
|
|
769
|
-
}
|
|
770
|
-
if (process.env.WRAPS_TELEMETRY_DISABLED === "1") {
|
|
771
|
-
return "WRAPS_TELEMETRY_DISABLED environment variable is set";
|
|
772
|
-
}
|
|
773
|
-
if (isCI()) {
|
|
774
|
-
return "CI environment detected";
|
|
775
|
-
}
|
|
776
|
-
return null;
|
|
777
|
-
}
|
|
778
|
-
/**
|
|
779
|
-
* Disable telemetry
|
|
780
|
-
*/
|
|
781
|
-
disable() {
|
|
782
|
-
this.config.setEnabled(false);
|
|
783
|
-
this.enabled = false;
|
|
784
|
-
this.eventQueue = [];
|
|
785
|
-
}
|
|
786
|
-
/**
|
|
787
|
-
* Check if telemetry is enabled
|
|
788
|
-
*/
|
|
789
|
-
isEnabled() {
|
|
790
|
-
return this.enabled;
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Get config file path
|
|
794
|
-
*/
|
|
795
|
-
getConfigPath() {
|
|
796
|
-
return this.config.getConfigPath();
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Show first-run notification
|
|
800
|
-
*/
|
|
801
|
-
shouldShowNotification() {
|
|
802
|
-
return this.enabled && !this.config.hasShownNotification();
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Mark notification as shown
|
|
806
|
-
*/
|
|
807
|
-
markNotificationShown() {
|
|
808
|
-
this.config.markNotificationShown();
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* Show promotional footer once per CLI session.
|
|
812
|
-
* Call this after successful completion of status/list commands.
|
|
813
|
-
* Returns true if footer was shown, false if already shown this session.
|
|
814
|
-
*/
|
|
815
|
-
showFooterOnce() {
|
|
816
|
-
if (this.hasShownFooter) {
|
|
817
|
-
return false;
|
|
818
|
-
}
|
|
819
|
-
this.hasShownFooter = true;
|
|
820
|
-
console.log();
|
|
821
|
-
console.log(pc.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
822
|
-
console.log("\u{1F4CA} Wraps Platform \u2014 analytics, templates, automations");
|
|
823
|
-
console.log(` From $10/mo \u2192 ${pc.cyan("https://wraps.dev/platform")}`);
|
|
824
|
-
console.log();
|
|
825
|
-
console.log(`\u{1F4AC} ${pc.cyan("hey@wraps.sh")}`);
|
|
826
|
-
console.log(pc.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
827
|
-
return true;
|
|
828
|
-
}
|
|
829
|
-
/**
|
|
830
|
-
* Get CLI version from package.json
|
|
831
|
-
*/
|
|
832
|
-
getCLIVersion() {
|
|
833
|
-
try {
|
|
834
|
-
const __filename3 = fileURLToPath2(import.meta.url);
|
|
835
|
-
const __dirname4 = dirname(__filename3);
|
|
836
|
-
const pkg = JSON.parse(
|
|
837
|
-
readFileSync(join4(__dirname4, "../package.json"), "utf-8")
|
|
838
|
-
);
|
|
839
|
-
return pkg.version;
|
|
840
|
-
} catch {
|
|
841
|
-
return "unknown";
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
};
|
|
845
|
-
telemetryInstance = null;
|
|
450
|
+
function classifyDNSError(error) {
|
|
451
|
+
if (!(error instanceof Error)) {
|
|
452
|
+
return "unknown";
|
|
846
453
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
sanitized.email = void 0;
|
|
856
|
-
client.track(`command:${command}`, sanitized);
|
|
857
|
-
}
|
|
858
|
-
function trackServiceInit(service, success, metadata) {
|
|
859
|
-
const client = getTelemetryClient();
|
|
860
|
-
client.track("service:init", {
|
|
861
|
-
service,
|
|
862
|
-
success,
|
|
863
|
-
...metadata
|
|
864
|
-
});
|
|
454
|
+
const code = error.code;
|
|
455
|
+
if (code === "ENOTFOUND" || code === "ENODATA") {
|
|
456
|
+
return "missing";
|
|
457
|
+
}
|
|
458
|
+
if (code === "ETIMEOUT" || code === "ESERVFAIL" || code === "ECONNREFUSED") {
|
|
459
|
+
return "network";
|
|
460
|
+
}
|
|
461
|
+
return "unknown";
|
|
865
462
|
}
|
|
866
|
-
function
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
...metadata
|
|
871
|
-
});
|
|
463
|
+
function isAWSNotFoundError(error) {
|
|
464
|
+
if (!(error instanceof Error)) return false;
|
|
465
|
+
const awsError = error;
|
|
466
|
+
return error.name === "NotFoundException" || error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.name === "ResourceNotFoundException" || awsError.$metadata?.httpStatusCode === 404;
|
|
872
467
|
}
|
|
873
|
-
function
|
|
874
|
-
|
|
875
|
-
client.track("error:occurred", {
|
|
876
|
-
error_code: errorCode,
|
|
877
|
-
command,
|
|
878
|
-
...metadata
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
function trackFeature(feature, metadata) {
|
|
882
|
-
const client = getTelemetryClient();
|
|
883
|
-
client.track(`feature:${feature}`, metadata || {});
|
|
884
|
-
}
|
|
885
|
-
function trackServiceUpgrade(service, metadata) {
|
|
886
|
-
const client = getTelemetryClient();
|
|
887
|
-
client.track("service:upgraded", {
|
|
888
|
-
service,
|
|
889
|
-
...metadata
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
function trackServiceRemoved(service, metadata) {
|
|
893
|
-
const client = getTelemetryClient();
|
|
894
|
-
client.track("service:removed", {
|
|
895
|
-
service,
|
|
896
|
-
...metadata
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
var init_events = __esm({
|
|
900
|
-
"src/telemetry/events.ts"() {
|
|
901
|
-
"use strict";
|
|
902
|
-
init_esm_shims();
|
|
903
|
-
init_client();
|
|
904
|
-
}
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
// src/utils/shared/json-output.ts
|
|
908
|
-
function isJsonMode() {
|
|
909
|
-
return _jsonMode;
|
|
910
|
-
}
|
|
911
|
-
function setJsonMode(enabled) {
|
|
912
|
-
_jsonMode = enabled;
|
|
913
|
-
}
|
|
914
|
-
function jsonSuccess(command, data) {
|
|
915
|
-
const output3 = { success: true, command, data };
|
|
916
|
-
console.log(JSON.stringify(output3));
|
|
917
|
-
}
|
|
918
|
-
function jsonError(command, error) {
|
|
919
|
-
const output3 = { success: false, command, error };
|
|
920
|
-
console.log(JSON.stringify(output3));
|
|
921
|
-
}
|
|
922
|
-
var _jsonMode;
|
|
923
|
-
var init_json_output = __esm({
|
|
924
|
-
"src/utils/shared/json-output.ts"() {
|
|
925
|
-
"use strict";
|
|
926
|
-
init_esm_shims();
|
|
927
|
-
_jsonMode = false;
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
// src/utils/shared/aws-detection.ts
|
|
932
|
-
import { execSync } from "child_process";
|
|
933
|
-
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
934
|
-
import { homedir as homedir2 } from "os";
|
|
935
|
-
import { join as join5 } from "path";
|
|
936
|
-
import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
937
|
-
async function isAWSCLIInstalled() {
|
|
938
|
-
try {
|
|
939
|
-
execSync("aws --version", { stdio: "pipe" });
|
|
940
|
-
return true;
|
|
941
|
-
} catch {
|
|
468
|
+
function isPulumiError(error) {
|
|
469
|
+
if (!(error instanceof Error)) {
|
|
942
470
|
return false;
|
|
943
471
|
}
|
|
472
|
+
return error.message?.includes("pulumi") || error.message?.includes("Pulumi") || error.message?.includes("resource") || error.message?.includes("creating") || error.message?.includes("AccessDenied");
|
|
944
473
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
474
|
+
function parseAWSError(error) {
|
|
475
|
+
const errorName = error.name || "UnknownError";
|
|
476
|
+
const actionMatch = error.message?.match(/when calling the (\w+) operation/i);
|
|
477
|
+
const action = actionMatch?.[1];
|
|
478
|
+
const resourceMatch = error.message?.match(/resource[:\s]+([^\s,]+)/i);
|
|
479
|
+
const resource = resourceMatch?.[1];
|
|
480
|
+
return { code: errorName, action, resource };
|
|
953
481
|
}
|
|
954
|
-
function
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (content.includes("[default]")) {
|
|
968
|
-
return "profile";
|
|
482
|
+
function parsePulumiError(error) {
|
|
483
|
+
const message = error.message || "";
|
|
484
|
+
if (message.includes("AccessDenied") || message.includes("access denied")) {
|
|
485
|
+
const actionMatch = message.match(
|
|
486
|
+
/(?:action|operation)[:\s]+["']?(\w+:\w+)["']?/i
|
|
487
|
+
);
|
|
488
|
+
if (actionMatch) {
|
|
489
|
+
const [service] = actionMatch[1].split(":");
|
|
490
|
+
return {
|
|
491
|
+
code: "IAM_PERMISSION_DENIED",
|
|
492
|
+
iamAction: actionMatch[1],
|
|
493
|
+
service
|
|
494
|
+
};
|
|
969
495
|
}
|
|
496
|
+
if (message.includes("ses:") || message.includes("SES")) {
|
|
497
|
+
return { code: "SES_PERMISSION_DENIED", service: "ses" };
|
|
498
|
+
}
|
|
499
|
+
if (message.includes("dynamodb:") || message.includes("DynamoDB")) {
|
|
500
|
+
return { code: "DYNAMODB_PERMISSION_DENIED", service: "dynamodb" };
|
|
501
|
+
}
|
|
502
|
+
if (message.includes("lambda:") || message.includes("Lambda")) {
|
|
503
|
+
return { code: "LAMBDA_PERMISSION_DENIED", service: "lambda" };
|
|
504
|
+
}
|
|
505
|
+
if (message.includes("events:") || message.includes("EventBridge")) {
|
|
506
|
+
return { code: "EVENTBRIDGE_PERMISSION_DENIED", service: "events" };
|
|
507
|
+
}
|
|
508
|
+
if (message.includes("sqs:") || message.includes("SQS")) {
|
|
509
|
+
return { code: "SQS_PERMISSION_DENIED", service: "sqs" };
|
|
510
|
+
}
|
|
511
|
+
if (message.includes("iam:") || message.includes("IAM")) {
|
|
512
|
+
return { code: "IAM_PERMISSION_DENIED", service: "iam" };
|
|
513
|
+
}
|
|
514
|
+
return { code: "IAM_PERMISSION_DENIED" };
|
|
970
515
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
function detectHostingProvider() {
|
|
981
|
-
if (process.env.VERCEL || process.env.VERCEL_ENV) {
|
|
982
|
-
return "vercel";
|
|
983
|
-
}
|
|
984
|
-
if (process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PROJECT_ID) {
|
|
985
|
-
return "railway";
|
|
986
|
-
}
|
|
987
|
-
if (process.env.NETLIFY || process.env.NETLIFY_DEV) {
|
|
988
|
-
return "netlify";
|
|
989
|
-
}
|
|
990
|
-
if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV || process.env.ECS_CONTAINER_METADATA_URI || process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
|
|
991
|
-
return "aws";
|
|
516
|
+
if (message.includes("AlreadyExists") || message.includes("already exists") || message.includes("already exist") || message.includes("ResourceConflictException") || message.includes("ResourceInUse") || message.includes("EntityAlreadyExists")) {
|
|
517
|
+
const nameMatch = message.match(/error creating '([^']+)'/);
|
|
518
|
+
const typeMatch = message.match(/\((aws:[^)]+)\)/);
|
|
519
|
+
return {
|
|
520
|
+
code: "RESOURCE_CONFLICT",
|
|
521
|
+
resourceName: nameMatch?.[1],
|
|
522
|
+
resourceType: typeMatch?.[1]
|
|
523
|
+
};
|
|
992
524
|
}
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
async function validateCredentials() {
|
|
996
|
-
try {
|
|
997
|
-
const sts = new STSClient({ region: "us-east-1" });
|
|
998
|
-
const identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
999
|
-
return identity.Account || null;
|
|
1000
|
-
} catch {
|
|
1001
|
-
return null;
|
|
525
|
+
if (message.includes("stack is currently locked")) {
|
|
526
|
+
return { code: "STACK_LOCKED" };
|
|
1002
527
|
}
|
|
528
|
+
return { code: "PULUMI_ERROR" };
|
|
1003
529
|
}
|
|
1004
|
-
function
|
|
1005
|
-
|
|
530
|
+
function redactSensitiveValues(input) {
|
|
531
|
+
let message = input;
|
|
532
|
+
message = message.replace(/\b\d{12}\b/g, "[ACCOUNT_ID]");
|
|
533
|
+
message = message.replace(
|
|
534
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
535
|
+
"[EMAIL]"
|
|
536
|
+
);
|
|
537
|
+
message = message.replace(
|
|
538
|
+
/(?<!\.amazonaws\.com|\.aws\.amazon\.com)\b[a-zA-Z0-9][a-zA-Z0-9-]+\.[a-zA-Z]{2,}\b/g,
|
|
539
|
+
(match) => {
|
|
540
|
+
if (match.includes("amazonaws") || match.includes("aws.amazon")) {
|
|
541
|
+
return match;
|
|
542
|
+
}
|
|
543
|
+
return "[DOMAIN]";
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
message = message.replace(
|
|
547
|
+
/arn:aws:[^:]+:[^:]*:\d{12}:/g,
|
|
548
|
+
"arn:aws:[SERVICE]:[REGION]:[ACCOUNT_ID]:"
|
|
549
|
+
);
|
|
550
|
+
return message;
|
|
1006
551
|
}
|
|
1007
|
-
function
|
|
1008
|
-
if (
|
|
1009
|
-
return
|
|
1010
|
-
}
|
|
1011
|
-
if (process.env.AWS_DEFAULT_REGION) {
|
|
1012
|
-
return process.env.AWS_DEFAULT_REGION;
|
|
552
|
+
function sanitizeErrorMessage(error) {
|
|
553
|
+
if (!error) {
|
|
554
|
+
return "Unknown error";
|
|
1013
555
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
}).trim();
|
|
1019
|
-
return region || null;
|
|
1020
|
-
} catch {
|
|
1021
|
-
return null;
|
|
556
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
557
|
+
const redacted = redactSensitiveValues(raw);
|
|
558
|
+
if (redacted.length > 500) {
|
|
559
|
+
return `${redacted.slice(0, 500)}...`;
|
|
1022
560
|
}
|
|
561
|
+
return redacted;
|
|
1023
562
|
}
|
|
1024
|
-
function
|
|
1025
|
-
const
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
563
|
+
function handleCLIError(error, command) {
|
|
564
|
+
const cmdContext = command || "unknown";
|
|
565
|
+
if (isJsonMode()) {
|
|
566
|
+
let code = "UNKNOWN_ERROR";
|
|
567
|
+
let message = "An unexpected error occurred";
|
|
568
|
+
let suggestion;
|
|
569
|
+
let docsUrl;
|
|
570
|
+
if (error instanceof WrapsError) {
|
|
571
|
+
trackError(error.code, cmdContext);
|
|
572
|
+
code = error.code;
|
|
573
|
+
message = error.message;
|
|
574
|
+
suggestion = error.suggestion;
|
|
575
|
+
docsUrl = error.docsUrl;
|
|
576
|
+
} else if (isAWSError(error)) {
|
|
577
|
+
const parsed = parseAWSError(error);
|
|
578
|
+
code = `AWS_${parsed.code}`;
|
|
579
|
+
trackError(code, cmdContext, { action: parsed.action });
|
|
580
|
+
const wrapsErr = awsErrorToWrapsError(parsed.code, parsed.action, error);
|
|
581
|
+
message = wrapsErr.message;
|
|
582
|
+
suggestion = wrapsErr.suggestion;
|
|
583
|
+
docsUrl = wrapsErr.docsUrl;
|
|
584
|
+
} else if (isPulumiError(error)) {
|
|
585
|
+
const parsed = parsePulumiError(error);
|
|
586
|
+
code = `PULUMI_${parsed.code}`;
|
|
587
|
+
trackError(code, cmdContext, {
|
|
588
|
+
iamAction: parsed.iamAction,
|
|
589
|
+
service: parsed.service,
|
|
590
|
+
errorType: error?.constructor?.name
|
|
1048
591
|
});
|
|
592
|
+
const wrapsErr = pulumiErrorToWrapsError(
|
|
593
|
+
parsed.code,
|
|
594
|
+
parsed.iamAction,
|
|
595
|
+
parsed.service,
|
|
596
|
+
parsed.resourceName,
|
|
597
|
+
parsed.resourceType,
|
|
598
|
+
error?.message
|
|
599
|
+
);
|
|
600
|
+
message = wrapsErr.message;
|
|
601
|
+
suggestion = wrapsErr.suggestion;
|
|
602
|
+
docsUrl = wrapsErr.docsUrl;
|
|
603
|
+
} else {
|
|
604
|
+
trackError("UNHANDLED_ERROR", cmdContext, {
|
|
605
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
606
|
+
message: sanitizeErrorMessage(error)
|
|
607
|
+
});
|
|
608
|
+
message = error instanceof Error ? error.message : String(error || message);
|
|
1049
609
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
config2[match[1]] = match[2];
|
|
610
|
+
jsonError(cmdContext, { code, message, suggestion, docsUrl });
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
console.error("");
|
|
614
|
+
if (error instanceof WrapsError) {
|
|
615
|
+
trackError(error.code, cmdContext);
|
|
616
|
+
clack.log.error(error.message);
|
|
617
|
+
if (error.suggestion) {
|
|
618
|
+
console.log(`
|
|
619
|
+
${pc.yellow("Suggestion:")}`);
|
|
620
|
+
const lines = error.suggestion.split("\n");
|
|
621
|
+
for (const line of lines) {
|
|
622
|
+
console.log(` ${pc.white(line)}`);
|
|
1064
623
|
}
|
|
624
|
+
console.log();
|
|
1065
625
|
}
|
|
1066
|
-
if (
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const session = sessionMap.get(config2.sso_session);
|
|
1071
|
-
if (session) {
|
|
1072
|
-
ssoStartUrl = ssoStartUrl || session.startUrl;
|
|
1073
|
-
ssoRegion = ssoRegion || session.region;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
profiles.push({
|
|
1077
|
-
name: profileName,
|
|
1078
|
-
ssoStartUrl,
|
|
1079
|
-
ssoRegion,
|
|
1080
|
-
ssoAccountId: config2.sso_account_id || "",
|
|
1081
|
-
ssoRoleName: config2.sso_role_name || "",
|
|
1082
|
-
region: config2.region,
|
|
1083
|
-
ssoSession: config2.sso_session
|
|
1084
|
-
});
|
|
626
|
+
if (error.docsUrl) {
|
|
627
|
+
console.log(`${pc.dim("Documentation:")}`);
|
|
628
|
+
console.log(` ${pc.blue(error.docsUrl)}
|
|
629
|
+
`);
|
|
1085
630
|
}
|
|
631
|
+
process.exit(1);
|
|
1086
632
|
}
|
|
1087
|
-
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const lines = section.split("\n");
|
|
1099
|
-
const header = lines[0]?.replace("]", "").trim();
|
|
1100
|
-
if (!header?.startsWith("sso-session ")) {
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
const sessionName = header.replace("sso-session ", "");
|
|
1104
|
-
const config2 = {};
|
|
1105
|
-
for (const line of lines.slice(1)) {
|
|
1106
|
-
const match = line.match(/^\s*([^=\s]+)\s*=\s*(.+?)\s*$/);
|
|
1107
|
-
if (match) {
|
|
1108
|
-
config2[match[1]] = match[2];
|
|
633
|
+
if (isAWSError(error)) {
|
|
634
|
+
const { code, action } = parseAWSError(error);
|
|
635
|
+
trackError(`AWS_${code}`, cmdContext, { action });
|
|
636
|
+
const wrapsError = awsErrorToWrapsError(code, action, error);
|
|
637
|
+
clack.log.error(wrapsError.message);
|
|
638
|
+
if (wrapsError.suggestion) {
|
|
639
|
+
console.log(`
|
|
640
|
+
${pc.yellow("Suggestion:")}`);
|
|
641
|
+
const lines = wrapsError.suggestion.split("\n");
|
|
642
|
+
for (const line of lines) {
|
|
643
|
+
console.log(` ${pc.white(line)}`);
|
|
1109
644
|
}
|
|
645
|
+
console.log();
|
|
1110
646
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
return sessions;
|
|
1119
|
-
}
|
|
1120
|
-
function checkSSOTokenStatus(startUrl) {
|
|
1121
|
-
const ssoCachePath = join5(homedir2(), ".aws", "sso", "cache");
|
|
1122
|
-
if (!existsSync4(ssoCachePath)) {
|
|
1123
|
-
return {
|
|
1124
|
-
valid: false,
|
|
1125
|
-
expiresAt: null,
|
|
1126
|
-
expired: true,
|
|
1127
|
-
minutesRemaining: null,
|
|
1128
|
-
startUrl: null
|
|
1129
|
-
};
|
|
647
|
+
if (wrapsError.docsUrl) {
|
|
648
|
+
console.log(`${pc.dim("Documentation:")}`);
|
|
649
|
+
console.log(` ${pc.blue(wrapsError.docsUrl)}
|
|
650
|
+
`);
|
|
651
|
+
}
|
|
652
|
+
process.exit(1);
|
|
1130
653
|
}
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
654
|
+
if (isPulumiError(error)) {
|
|
655
|
+
const { code, iamAction, service, resourceName, resourceType } = parsePulumiError(error);
|
|
656
|
+
trackError(`PULUMI_${code}`, cmdContext, {
|
|
657
|
+
iamAction,
|
|
658
|
+
service,
|
|
659
|
+
errorType: error?.constructor?.name
|
|
660
|
+
});
|
|
661
|
+
const wrapsError = pulumiErrorToWrapsError(
|
|
662
|
+
code,
|
|
663
|
+
iamAction,
|
|
664
|
+
service,
|
|
665
|
+
resourceName,
|
|
666
|
+
resourceType,
|
|
667
|
+
error?.message
|
|
1134
668
|
);
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
continue;
|
|
669
|
+
clack.log.error(wrapsError.message);
|
|
670
|
+
if (wrapsError.suggestion) {
|
|
671
|
+
console.log(`
|
|
672
|
+
${pc.yellow("Suggestion:")}`);
|
|
673
|
+
const lines = wrapsError.suggestion.split("\n");
|
|
674
|
+
for (const line of lines) {
|
|
675
|
+
console.log(` ${pc.white(line)}`);
|
|
1143
676
|
}
|
|
1144
|
-
|
|
1145
|
-
const now = /* @__PURE__ */ new Date();
|
|
1146
|
-
const expired = expiresAt <= now;
|
|
1147
|
-
const minutesRemaining = Math.floor(
|
|
1148
|
-
(expiresAt.getTime() - now.getTime()) / 6e4
|
|
1149
|
-
);
|
|
1150
|
-
return {
|
|
1151
|
-
valid: !expired,
|
|
1152
|
-
expiresAt,
|
|
1153
|
-
expired,
|
|
1154
|
-
minutesRemaining,
|
|
1155
|
-
startUrl: token.startUrl || null
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
} catch {
|
|
1159
|
-
}
|
|
1160
|
-
return {
|
|
1161
|
-
valid: false,
|
|
1162
|
-
expiresAt: null,
|
|
1163
|
-
expired: true,
|
|
1164
|
-
minutesRemaining: null,
|
|
1165
|
-
startUrl: null
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
function getActiveSSOProfile(profiles) {
|
|
1169
|
-
const currentProfile = process.env.AWS_PROFILE || "default";
|
|
1170
|
-
return profiles.find((p) => p.name === currentProfile) || null;
|
|
1171
|
-
}
|
|
1172
|
-
function getSSOLoginCommand(profile) {
|
|
1173
|
-
if (profile && profile !== "default") {
|
|
1174
|
-
return `aws sso login --profile ${profile}`;
|
|
1175
|
-
}
|
|
1176
|
-
return "aws sso login";
|
|
1177
|
-
}
|
|
1178
|
-
function formatSSOProfile(profile) {
|
|
1179
|
-
return `${profile.name} (${profile.ssoAccountId} / ${profile.ssoRoleName})`;
|
|
1180
|
-
}
|
|
1181
|
-
async function detectAWSState() {
|
|
1182
|
-
const [cliInstalled, cliVersion, accountId] = await Promise.all([
|
|
1183
|
-
isAWSCLIInstalled(),
|
|
1184
|
-
getAWSCLIVersion(),
|
|
1185
|
-
validateCredentials()
|
|
1186
|
-
]);
|
|
1187
|
-
const credentialSource = detectCredentialSource();
|
|
1188
|
-
const detectedProvider = detectHostingProvider();
|
|
1189
|
-
const region = getCurrentRegion();
|
|
1190
|
-
const profileName = getCurrentProfile();
|
|
1191
|
-
const ssoProfiles = parseSSOProfiles();
|
|
1192
|
-
const ssoSessions = parseSSOSessions();
|
|
1193
|
-
const activeProfile = getActiveSSOProfile(ssoProfiles);
|
|
1194
|
-
const tokenStatus = ssoProfiles.length > 0 ? checkSSOTokenStatus(activeProfile?.ssoStartUrl) : null;
|
|
1195
|
-
const isUsingSSO = credentialSource === "sso" || activeProfile !== null && accountId !== null;
|
|
1196
|
-
return {
|
|
1197
|
-
cliInstalled,
|
|
1198
|
-
cliVersion,
|
|
1199
|
-
credentialsConfigured: accountId !== null,
|
|
1200
|
-
credentialSource: isUsingSSO ? "sso" : accountId !== null ? credentialSource : null,
|
|
1201
|
-
profileName,
|
|
1202
|
-
accountId,
|
|
1203
|
-
detectedProvider,
|
|
1204
|
-
region,
|
|
1205
|
-
sso: {
|
|
1206
|
-
configured: ssoProfiles.length > 0,
|
|
1207
|
-
profiles: ssoProfiles,
|
|
1208
|
-
sessions: ssoSessions,
|
|
1209
|
-
tokenStatus,
|
|
1210
|
-
activeProfile: isUsingSSO ? activeProfile : null
|
|
677
|
+
console.log();
|
|
1211
678
|
}
|
|
1212
|
-
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
return existsSync4(credentialsPath);
|
|
1217
|
-
}
|
|
1218
|
-
function hasConfigFile() {
|
|
1219
|
-
const configPath = join5(homedir2(), ".aws", "config");
|
|
1220
|
-
return existsSync4(configPath);
|
|
1221
|
-
}
|
|
1222
|
-
function getConfiguredProfiles() {
|
|
1223
|
-
const profiles = [];
|
|
1224
|
-
const credentialsPath = join5(homedir2(), ".aws", "credentials");
|
|
1225
|
-
if (existsSync4(credentialsPath)) {
|
|
1226
|
-
const content = readFileSync2(credentialsPath, "utf-8");
|
|
1227
|
-
const matches = content.matchAll(/\[([^\]]+)\]/g);
|
|
1228
|
-
for (const match of matches) {
|
|
1229
|
-
profiles.push(match[1]);
|
|
679
|
+
if (wrapsError.docsUrl) {
|
|
680
|
+
console.log(`${pc.dim("Documentation:")}`);
|
|
681
|
+
console.log(` ${pc.blue(wrapsError.docsUrl)}
|
|
682
|
+
`);
|
|
1230
683
|
}
|
|
684
|
+
process.exit(1);
|
|
1231
685
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
686
|
+
trackError("UNHANDLED_ERROR", cmdContext, {
|
|
687
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
688
|
+
message: sanitizeErrorMessage(error)
|
|
689
|
+
});
|
|
690
|
+
clack.log.error("An unexpected error occurred");
|
|
691
|
+
if (error instanceof Error) {
|
|
692
|
+
console.error(pc.dim(error.message));
|
|
693
|
+
} else if (typeof error === "string") {
|
|
694
|
+
console.error(error);
|
|
1241
695
|
}
|
|
1242
|
-
|
|
696
|
+
console.log(`
|
|
697
|
+
${pc.dim("If this persists, please report at:")}`);
|
|
698
|
+
console.log(` ${pc.blue("https://github.com/wraps-team/wraps/issues")}
|
|
699
|
+
`);
|
|
700
|
+
process.exit(1);
|
|
1243
701
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
702
|
+
function awsErrorToWrapsError(code, action, originalError) {
|
|
703
|
+
switch (code) {
|
|
704
|
+
// Credential / token errors — these mean the request never reached the API
|
|
705
|
+
case "ExpiredTokenException":
|
|
706
|
+
case "TokenRefreshRequired":
|
|
707
|
+
case "SSOTokenExpired":
|
|
708
|
+
return errors.sessionTokenExpired();
|
|
709
|
+
case "InvalidClientTokenId":
|
|
710
|
+
case "InvalidAccessKeyId":
|
|
711
|
+
case "SignatureDoesNotMatch":
|
|
712
|
+
case "UnrecognizedClientException":
|
|
713
|
+
return errors.accessKeyInvalid();
|
|
714
|
+
// IAM permission errors — request reached AWS but was denied
|
|
715
|
+
case "AccessDenied":
|
|
716
|
+
case "AccessDeniedException":
|
|
717
|
+
case "UnauthorizedAccess":
|
|
718
|
+
return errors.iamPermissionDenied(
|
|
719
|
+
action || "unknown",
|
|
720
|
+
"AWS resource",
|
|
721
|
+
"Ensure your IAM user/role has the required permissions."
|
|
722
|
+
);
|
|
723
|
+
// SES SendEmail errors — request reached SES but was rejected
|
|
724
|
+
case "MessageRejected":
|
|
725
|
+
return errors.sesMessageRejected(sanitizeErrorMessage(originalError));
|
|
726
|
+
case "MailFromDomainNotVerifiedException":
|
|
727
|
+
return errors.sesMailFromNotVerified(sanitizeErrorMessage(originalError));
|
|
728
|
+
case "AccountSendingPausedException":
|
|
729
|
+
return errors.sesAccountSendingPaused();
|
|
730
|
+
case "ConfigurationSetSendingPausedException":
|
|
731
|
+
return errors.sesConfigSetSendingPaused();
|
|
732
|
+
case "ConfigurationSetDoesNotExistException":
|
|
733
|
+
return errors.sesConfigSetMissing(sanitizeErrorMessage(originalError));
|
|
734
|
+
// Throughput / quota errors
|
|
735
|
+
case "Throttling":
|
|
736
|
+
case "ThrottlingException":
|
|
737
|
+
case "TooManyRequestsException":
|
|
738
|
+
return errors.awsThrottled(action);
|
|
739
|
+
case "LimitExceededException":
|
|
740
|
+
case "ServiceQuotaExceededException":
|
|
741
|
+
return errors.awsLimitExceeded(
|
|
742
|
+
action,
|
|
743
|
+
sanitizeErrorMessage(originalError)
|
|
744
|
+
);
|
|
745
|
+
// Anything else — surface the real error instead of lying about credentials
|
|
746
|
+
default:
|
|
747
|
+
return errors.awsUnknownError(
|
|
748
|
+
code,
|
|
749
|
+
action,
|
|
750
|
+
sanitizeErrorMessage(originalError)
|
|
751
|
+
);
|
|
1248
752
|
}
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
// src/utils/shared/errors.ts
|
|
1252
|
-
var errors_exports = {};
|
|
1253
|
-
__export(errors_exports, {
|
|
1254
|
-
WrapsError: () => WrapsError,
|
|
1255
|
-
classifyDNSError: () => classifyDNSError,
|
|
1256
|
-
errors: () => errors,
|
|
1257
|
-
handleCLIError: () => handleCLIError,
|
|
1258
|
-
isAWSError: () => isAWSError,
|
|
1259
|
-
isAWSNotFoundError: () => isAWSNotFoundError,
|
|
1260
|
-
isPulumiError: () => isPulumiError,
|
|
1261
|
-
parseAWSError: () => parseAWSError,
|
|
1262
|
-
parsePulumiError: () => parsePulumiError,
|
|
1263
|
-
sanitizeErrorMessage: () => sanitizeErrorMessage
|
|
1264
|
-
});
|
|
1265
|
-
import * as clack4 from "@clack/prompts";
|
|
1266
|
-
import pc5 from "picocolors";
|
|
1267
|
-
function isAWSError(error) {
|
|
1268
|
-
if (!(error instanceof Error)) {
|
|
1269
|
-
return false;
|
|
1270
|
-
}
|
|
1271
|
-
const awsErrorNames = [
|
|
1272
|
-
"ExpiredTokenException",
|
|
1273
|
-
"InvalidClientTokenId",
|
|
1274
|
-
"AccessDenied",
|
|
1275
|
-
"AccessDeniedException",
|
|
1276
|
-
"UnauthorizedAccess",
|
|
1277
|
-
"InvalidAccessKeyId",
|
|
1278
|
-
"SignatureDoesNotMatch",
|
|
1279
|
-
"UnrecognizedClientException",
|
|
1280
|
-
"CredentialsError",
|
|
1281
|
-
"TokenRefreshRequired",
|
|
1282
|
-
"SSOTokenExpired"
|
|
1283
|
-
];
|
|
1284
|
-
return awsErrorNames.includes(error.name) || "$metadata" in error;
|
|
1285
|
-
}
|
|
1286
|
-
function classifyDNSError(error) {
|
|
1287
|
-
if (!(error instanceof Error)) {
|
|
1288
|
-
return "unknown";
|
|
1289
|
-
}
|
|
1290
|
-
const code = error.code;
|
|
1291
|
-
if (code === "ENOTFOUND" || code === "ENODATA") {
|
|
1292
|
-
return "missing";
|
|
1293
|
-
}
|
|
1294
|
-
if (code === "ETIMEOUT" || code === "ESERVFAIL" || code === "ECONNREFUSED") {
|
|
1295
|
-
return "network";
|
|
1296
|
-
}
|
|
1297
|
-
return "unknown";
|
|
1298
|
-
}
|
|
1299
|
-
function isAWSNotFoundError(error) {
|
|
1300
|
-
if (!(error instanceof Error)) return false;
|
|
1301
|
-
const awsError = error;
|
|
1302
|
-
return error.name === "NotFoundException" || error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.name === "ResourceNotFoundException" || awsError.$metadata?.httpStatusCode === 404;
|
|
1303
|
-
}
|
|
1304
|
-
function isPulumiError(error) {
|
|
1305
|
-
if (!(error instanceof Error)) {
|
|
1306
|
-
return false;
|
|
1307
|
-
}
|
|
1308
|
-
return error.message?.includes("pulumi") || error.message?.includes("Pulumi") || error.message?.includes("resource") || error.message?.includes("creating") || error.message?.includes("AccessDenied");
|
|
1309
|
-
}
|
|
1310
|
-
function parseAWSError(error) {
|
|
1311
|
-
const errorName = error.name || "UnknownError";
|
|
1312
|
-
const actionMatch = error.message?.match(/when calling the (\w+) operation/i);
|
|
1313
|
-
const action = actionMatch?.[1];
|
|
1314
|
-
const resourceMatch = error.message?.match(/resource[:\s]+([^\s,]+)/i);
|
|
1315
|
-
const resource = resourceMatch?.[1];
|
|
1316
|
-
return { code: errorName, action, resource };
|
|
1317
|
-
}
|
|
1318
|
-
function parsePulumiError(error) {
|
|
1319
|
-
const message = error.message || "";
|
|
1320
|
-
if (message.includes("AccessDenied") || message.includes("access denied")) {
|
|
1321
|
-
const actionMatch = message.match(
|
|
1322
|
-
/(?:action|operation)[:\s]+["']?(\w+:\w+)["']?/i
|
|
1323
|
-
);
|
|
1324
|
-
if (actionMatch) {
|
|
1325
|
-
const [service] = actionMatch[1].split(":");
|
|
1326
|
-
return {
|
|
1327
|
-
code: "IAM_PERMISSION_DENIED",
|
|
1328
|
-
iamAction: actionMatch[1],
|
|
1329
|
-
service
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
if (message.includes("ses:") || message.includes("SES")) {
|
|
1333
|
-
return { code: "SES_PERMISSION_DENIED", service: "ses" };
|
|
1334
|
-
}
|
|
1335
|
-
if (message.includes("dynamodb:") || message.includes("DynamoDB")) {
|
|
1336
|
-
return { code: "DYNAMODB_PERMISSION_DENIED", service: "dynamodb" };
|
|
1337
|
-
}
|
|
1338
|
-
if (message.includes("lambda:") || message.includes("Lambda")) {
|
|
1339
|
-
return { code: "LAMBDA_PERMISSION_DENIED", service: "lambda" };
|
|
1340
|
-
}
|
|
1341
|
-
if (message.includes("events:") || message.includes("EventBridge")) {
|
|
1342
|
-
return { code: "EVENTBRIDGE_PERMISSION_DENIED", service: "events" };
|
|
1343
|
-
}
|
|
1344
|
-
if (message.includes("sqs:") || message.includes("SQS")) {
|
|
1345
|
-
return { code: "SQS_PERMISSION_DENIED", service: "sqs" };
|
|
1346
|
-
}
|
|
1347
|
-
if (message.includes("iam:") || message.includes("IAM")) {
|
|
1348
|
-
return { code: "IAM_PERMISSION_DENIED", service: "iam" };
|
|
1349
|
-
}
|
|
1350
|
-
return { code: "IAM_PERMISSION_DENIED" };
|
|
1351
|
-
}
|
|
1352
|
-
if (message.includes("AlreadyExists") || message.includes("already exists") || message.includes("already exist") || message.includes("ResourceConflictException") || message.includes("ResourceInUse") || message.includes("EntityAlreadyExists")) {
|
|
1353
|
-
const nameMatch = message.match(/error creating '([^']+)'/);
|
|
1354
|
-
const typeMatch = message.match(/\((aws:[^)]+)\)/);
|
|
1355
|
-
return {
|
|
1356
|
-
code: "RESOURCE_CONFLICT",
|
|
1357
|
-
resourceName: nameMatch?.[1],
|
|
1358
|
-
resourceType: typeMatch?.[1]
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
|
-
if (message.includes("stack is currently locked")) {
|
|
1362
|
-
return { code: "STACK_LOCKED" };
|
|
1363
|
-
}
|
|
1364
|
-
return { code: "PULUMI_ERROR" };
|
|
1365
|
-
}
|
|
1366
|
-
function sanitizeErrorMessage(error) {
|
|
1367
|
-
if (!error) {
|
|
1368
|
-
return "Unknown error";
|
|
1369
|
-
}
|
|
1370
|
-
let message = error instanceof Error ? error.message : String(error);
|
|
1371
|
-
message = message.replace(/\b\d{12}\b/g, "[ACCOUNT_ID]");
|
|
1372
|
-
message = message.replace(
|
|
1373
|
-
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
1374
|
-
"[EMAIL]"
|
|
1375
|
-
);
|
|
1376
|
-
message = message.replace(
|
|
1377
|
-
/(?<!\.amazonaws\.com|\.aws\.amazon\.com)\b[a-zA-Z0-9][a-zA-Z0-9-]+\.[a-zA-Z]{2,}\b/g,
|
|
1378
|
-
(match) => {
|
|
1379
|
-
if (match.includes("amazonaws") || match.includes("aws.amazon")) {
|
|
1380
|
-
return match;
|
|
1381
|
-
}
|
|
1382
|
-
return "[DOMAIN]";
|
|
1383
|
-
}
|
|
1384
|
-
);
|
|
1385
|
-
message = message.replace(
|
|
1386
|
-
/arn:aws:[^:]+:[^:]*:\d{12}:/g,
|
|
1387
|
-
"arn:aws:[SERVICE]:[REGION]:[ACCOUNT_ID]:"
|
|
1388
|
-
);
|
|
1389
|
-
if (message.length > 500) {
|
|
1390
|
-
message = `${message.slice(0, 500)}...`;
|
|
1391
|
-
}
|
|
1392
|
-
return message;
|
|
1393
753
|
}
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
});
|
|
1423
|
-
const wrapsErr = pulumiErrorToWrapsError(
|
|
1424
|
-
parsed.code,
|
|
1425
|
-
parsed.iamAction,
|
|
1426
|
-
parsed.service,
|
|
1427
|
-
parsed.resourceName,
|
|
1428
|
-
parsed.resourceType
|
|
754
|
+
function pulumiErrorToWrapsError(code, iamAction, service, resourceName, resourceType, originalMessage) {
|
|
755
|
+
switch (code) {
|
|
756
|
+
case "RESOURCE_CONFLICT":
|
|
757
|
+
return errors.resourceConflict(
|
|
758
|
+
resourceName || "unknown resource",
|
|
759
|
+
resourceType
|
|
760
|
+
);
|
|
761
|
+
case "STACK_LOCKED":
|
|
762
|
+
return errors.stackLocked();
|
|
763
|
+
case "SES_PERMISSION_DENIED":
|
|
764
|
+
return errors.sesPermissionDenied(iamAction || "unknown");
|
|
765
|
+
case "DYNAMODB_PERMISSION_DENIED":
|
|
766
|
+
return errors.dynamoDBPermissionDenied();
|
|
767
|
+
case "LAMBDA_PERMISSION_DENIED":
|
|
768
|
+
return errors.lambdaPermissionDenied();
|
|
769
|
+
case "EVENTBRIDGE_PERMISSION_DENIED":
|
|
770
|
+
return errors.eventBridgePermissionDenied();
|
|
771
|
+
case "SQS_PERMISSION_DENIED":
|
|
772
|
+
return errors.sqsPermissionDenied();
|
|
773
|
+
case "IAM_PERMISSION_DENIED":
|
|
774
|
+
return errors.iamPermissionDenied(
|
|
775
|
+
iamAction || "unknown",
|
|
776
|
+
"AWS resource",
|
|
777
|
+
service ? `Your IAM user/role needs ${service.toUpperCase()} permissions.` : "Ensure your IAM user/role has the required permissions."
|
|
778
|
+
);
|
|
779
|
+
default:
|
|
780
|
+
return errors.pulumiError(
|
|
781
|
+
originalMessage ? sanitizeErrorMessage(originalMessage) : "Deployment failed"
|
|
1429
782
|
);
|
|
1430
|
-
message = wrapsErr.message;
|
|
1431
|
-
suggestion = wrapsErr.suggestion;
|
|
1432
|
-
docsUrl = wrapsErr.docsUrl;
|
|
1433
|
-
} else {
|
|
1434
|
-
trackError("UNHANDLED_ERROR", cmdContext, {
|
|
1435
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1436
|
-
message: sanitizeErrorMessage(error)
|
|
1437
|
-
});
|
|
1438
|
-
message = error instanceof Error ? error.message : String(error || message);
|
|
1439
|
-
}
|
|
1440
|
-
jsonError(cmdContext, { code, message, suggestion, docsUrl });
|
|
1441
|
-
process.exit(1);
|
|
1442
|
-
}
|
|
1443
|
-
console.error("");
|
|
1444
|
-
if (error instanceof WrapsError) {
|
|
1445
|
-
trackError(error.code, cmdContext);
|
|
1446
|
-
clack4.log.error(error.message);
|
|
1447
|
-
if (error.suggestion) {
|
|
1448
|
-
console.log(`
|
|
1449
|
-
${pc5.yellow("Suggestion:")}`);
|
|
1450
|
-
const lines = error.suggestion.split("\n");
|
|
1451
|
-
for (const line of lines) {
|
|
1452
|
-
console.log(` ${pc5.white(line)}`);
|
|
1453
|
-
}
|
|
1454
|
-
console.log();
|
|
1455
|
-
}
|
|
1456
|
-
if (error.docsUrl) {
|
|
1457
|
-
console.log(`${pc5.dim("Documentation:")}`);
|
|
1458
|
-
console.log(` ${pc5.blue(error.docsUrl)}
|
|
1459
|
-
`);
|
|
1460
|
-
}
|
|
1461
|
-
process.exit(1);
|
|
1462
783
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
console.log(`${pc5.dim("Documentation:")}`);
|
|
1479
|
-
console.log(` ${pc5.blue(wrapsError.docsUrl)}
|
|
1480
|
-
`);
|
|
1481
|
-
}
|
|
1482
|
-
process.exit(1);
|
|
1483
|
-
}
|
|
1484
|
-
if (isPulumiError(error)) {
|
|
1485
|
-
const { code, iamAction, service, resourceName, resourceType } = parsePulumiError(error);
|
|
1486
|
-
trackError(`PULUMI_${code}`, cmdContext, {
|
|
1487
|
-
iamAction,
|
|
1488
|
-
service,
|
|
1489
|
-
errorType: error?.constructor?.name
|
|
1490
|
-
});
|
|
1491
|
-
const wrapsError = pulumiErrorToWrapsError(
|
|
1492
|
-
code,
|
|
1493
|
-
iamAction,
|
|
1494
|
-
service,
|
|
1495
|
-
resourceName,
|
|
1496
|
-
resourceType
|
|
1497
|
-
);
|
|
1498
|
-
clack4.log.error(wrapsError.message);
|
|
1499
|
-
if (wrapsError.suggestion) {
|
|
1500
|
-
console.log(`
|
|
1501
|
-
${pc5.yellow("Suggestion:")}`);
|
|
1502
|
-
const lines = wrapsError.suggestion.split("\n");
|
|
1503
|
-
for (const line of lines) {
|
|
1504
|
-
console.log(` ${pc5.white(line)}`);
|
|
1505
|
-
}
|
|
1506
|
-
console.log();
|
|
1507
|
-
}
|
|
1508
|
-
if (wrapsError.docsUrl) {
|
|
1509
|
-
console.log(`${pc5.dim("Documentation:")}`);
|
|
1510
|
-
console.log(` ${pc5.blue(wrapsError.docsUrl)}
|
|
1511
|
-
`);
|
|
1512
|
-
}
|
|
1513
|
-
process.exit(1);
|
|
1514
|
-
}
|
|
1515
|
-
trackError("UNHANDLED_ERROR", cmdContext, {
|
|
1516
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1517
|
-
message: sanitizeErrorMessage(error)
|
|
1518
|
-
});
|
|
1519
|
-
clack4.log.error("An unexpected error occurred");
|
|
1520
|
-
if (error instanceof Error) {
|
|
1521
|
-
console.error(pc5.dim(error.message));
|
|
1522
|
-
} else if (typeof error === "string") {
|
|
1523
|
-
console.error(error);
|
|
1524
|
-
}
|
|
1525
|
-
console.log(`
|
|
1526
|
-
${pc5.dim("If this persists, please report at:")}`);
|
|
1527
|
-
console.log(` ${pc5.blue("https://github.com/wraps-team/wraps/issues")}
|
|
1528
|
-
`);
|
|
1529
|
-
process.exit(1);
|
|
1530
|
-
}
|
|
1531
|
-
function awsErrorToWrapsError(code, action) {
|
|
1532
|
-
switch (code) {
|
|
1533
|
-
case "ExpiredTokenException":
|
|
1534
|
-
case "TokenRefreshRequired":
|
|
1535
|
-
case "SSOTokenExpired":
|
|
1536
|
-
return errors.sessionTokenExpired();
|
|
1537
|
-
case "InvalidClientTokenId":
|
|
1538
|
-
case "InvalidAccessKeyId":
|
|
1539
|
-
case "SignatureDoesNotMatch":
|
|
1540
|
-
return errors.accessKeyInvalid();
|
|
1541
|
-
case "AccessDenied":
|
|
1542
|
-
case "AccessDeniedException":
|
|
1543
|
-
case "UnauthorizedAccess":
|
|
1544
|
-
return errors.iamPermissionDenied(
|
|
1545
|
-
action || "unknown",
|
|
1546
|
-
"AWS resource",
|
|
1547
|
-
"Ensure your IAM user/role has the required permissions."
|
|
1548
|
-
);
|
|
1549
|
-
default:
|
|
1550
|
-
return errors.noAWSCredentials();
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
function pulumiErrorToWrapsError(code, iamAction, service, resourceName, resourceType) {
|
|
1554
|
-
switch (code) {
|
|
1555
|
-
case "RESOURCE_CONFLICT":
|
|
1556
|
-
return errors.resourceConflict(
|
|
1557
|
-
resourceName || "unknown resource",
|
|
1558
|
-
resourceType
|
|
1559
|
-
);
|
|
1560
|
-
case "STACK_LOCKED":
|
|
1561
|
-
return errors.stackLocked();
|
|
1562
|
-
case "SES_PERMISSION_DENIED":
|
|
1563
|
-
return errors.sesPermissionDenied(iamAction || "unknown");
|
|
1564
|
-
case "DYNAMODB_PERMISSION_DENIED":
|
|
1565
|
-
return errors.dynamoDBPermissionDenied();
|
|
1566
|
-
case "LAMBDA_PERMISSION_DENIED":
|
|
1567
|
-
return errors.lambdaPermissionDenied();
|
|
1568
|
-
case "EVENTBRIDGE_PERMISSION_DENIED":
|
|
1569
|
-
return errors.eventBridgePermissionDenied();
|
|
1570
|
-
case "SQS_PERMISSION_DENIED":
|
|
1571
|
-
return errors.sqsPermissionDenied();
|
|
1572
|
-
case "IAM_PERMISSION_DENIED":
|
|
1573
|
-
return errors.iamPermissionDenied(
|
|
1574
|
-
iamAction || "unknown",
|
|
1575
|
-
"AWS resource",
|
|
1576
|
-
service ? `Your IAM user/role needs ${service.toUpperCase()} permissions.` : "Ensure your IAM user/role has the required permissions."
|
|
1577
|
-
);
|
|
1578
|
-
default:
|
|
1579
|
-
return errors.pulumiError("Deployment failed");
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
var WrapsError, errors;
|
|
1583
|
-
var init_errors = __esm({
|
|
1584
|
-
"src/utils/shared/errors.ts"() {
|
|
1585
|
-
"use strict";
|
|
1586
|
-
init_esm_shims();
|
|
1587
|
-
init_events();
|
|
1588
|
-
init_json_output();
|
|
1589
|
-
WrapsError = class extends Error {
|
|
1590
|
-
constructor(message, code, suggestion, docsUrl) {
|
|
1591
|
-
super(message);
|
|
1592
|
-
this.code = code;
|
|
1593
|
-
this.suggestion = suggestion;
|
|
1594
|
-
this.docsUrl = docsUrl;
|
|
1595
|
-
this.name = "WrapsError";
|
|
784
|
+
}
|
|
785
|
+
var WrapsError, errors;
|
|
786
|
+
var init_errors = __esm({
|
|
787
|
+
"src/utils/shared/errors.ts"() {
|
|
788
|
+
"use strict";
|
|
789
|
+
init_esm_shims();
|
|
790
|
+
init_events();
|
|
791
|
+
init_json_output();
|
|
792
|
+
WrapsError = class extends Error {
|
|
793
|
+
constructor(message, code, suggestion, docsUrl) {
|
|
794
|
+
super(message);
|
|
795
|
+
this.code = code;
|
|
796
|
+
this.suggestion = suggestion;
|
|
797
|
+
this.docsUrl = docsUrl;
|
|
798
|
+
this.name = "WrapsError";
|
|
1596
799
|
}
|
|
1597
800
|
};
|
|
1598
801
|
errors = {
|
|
@@ -1747,6 +950,61 @@ View required SES permissions:
|
|
|
1747
950
|
wraps permissions --service email --json`,
|
|
1748
951
|
"https://wraps.dev/docs/guides/aws-setup/permissions"
|
|
1749
952
|
),
|
|
953
|
+
// SES SendEmail rejection errors — request reached SES but the send failed
|
|
954
|
+
// for a reason unrelated to credentials.
|
|
955
|
+
sesMessageRejected: (detail) => new WrapsError(
|
|
956
|
+
`SES rejected the message: ${detail}`,
|
|
957
|
+
"SES_MESSAGE_REJECTED",
|
|
958
|
+
"Common causes:\n \u2022 Account is in the SES sandbox and the recipient is not a verified address\n \u2022 Sender identity (domain or email) is not verified for sending\n \u2022 The sender domain is verified for receiving but not for sending\n\nCheck status:\n wraps email status\n wraps email doctor\n\nRequest production access (exit sandbox):\n https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html",
|
|
959
|
+
"https://wraps.dev/docs/guides/email/troubleshooting"
|
|
960
|
+
),
|
|
961
|
+
sesMailFromNotVerified: (detail) => new WrapsError(
|
|
962
|
+
`SES MAIL FROM domain is not verified: ${detail}`,
|
|
963
|
+
"SES_MAIL_FROM_NOT_VERIFIED",
|
|
964
|
+
"The custom MAIL FROM domain configured for this identity is not fully verified.\n\nCheck DNS records:\n wraps email verify\n\nOr remove the custom MAIL FROM domain in the SES console and retry.",
|
|
965
|
+
"https://docs.aws.amazon.com/ses/latest/dg/mail-from.html"
|
|
966
|
+
),
|
|
967
|
+
sesAccountSendingPaused: () => new WrapsError(
|
|
968
|
+
"SES account-level sending is paused",
|
|
969
|
+
"SES_ACCOUNT_SENDING_PAUSED",
|
|
970
|
+
"Your SES account is currently paused from sending email. This is usually caused by:\n \u2022 A high bounce or complaint rate\n \u2022 An AWS-initiated review\n\nCheck the SES console \u2192 Reputation Dashboard for details, then resume sending once the issue is resolved.",
|
|
971
|
+
"https://docs.aws.amazon.com/ses/latest/dg/reputationdashboard.html"
|
|
972
|
+
),
|
|
973
|
+
sesConfigSetSendingPaused: () => new WrapsError(
|
|
974
|
+
"SES configuration set sending is paused",
|
|
975
|
+
"SES_CONFIG_SET_SENDING_PAUSED",
|
|
976
|
+
"The configuration set used for this send has sending paused. Resume it in the SES console under Configuration Sets, or send without specifying the paused configuration set.",
|
|
977
|
+
"https://docs.aws.amazon.com/ses/latest/dg/using-configuration-sets.html"
|
|
978
|
+
),
|
|
979
|
+
sesConfigSetMissing: (detail) => new WrapsError(
|
|
980
|
+
`SES configuration set does not exist: ${detail}`,
|
|
981
|
+
"SES_CONFIG_SET_MISSING",
|
|
982
|
+
"The configuration set referenced by this send does not exist in the current region. Create it in the SES console, switch regions, or remove the ConfigurationSetName from the request.",
|
|
983
|
+
"https://docs.aws.amazon.com/ses/latest/dg/using-configuration-sets.html"
|
|
984
|
+
),
|
|
985
|
+
// Generic AWS error fallbacks — used by awsErrorToWrapsError when no specific
|
|
986
|
+
// mapping exists. These NEVER claim credentials are missing.
|
|
987
|
+
awsThrottled: (action) => new WrapsError(
|
|
988
|
+
`AWS request was throttled${action ? ` (${action})` : ""}`,
|
|
989
|
+
"AWS_THROTTLED",
|
|
990
|
+
"AWS is rate-limiting requests to this API. Wait a moment and retry.\n\nIf this happens repeatedly, request a service quota increase in the AWS console.",
|
|
991
|
+
"https://docs.aws.amazon.com/general/latest/gr/api-retries.html"
|
|
992
|
+
),
|
|
993
|
+
awsLimitExceeded: (action, detail) => new WrapsError(
|
|
994
|
+
`AWS service limit exceeded${action ? ` (${action})` : ""}${detail ? `: ${detail}` : ""}`,
|
|
995
|
+
"AWS_LIMIT_EXCEEDED",
|
|
996
|
+
"You've hit a service quota for this AWS API.\n\nRequest a quota increase in the AWS console:\n Service Quotas \u2192 AWS Services \u2192 (your service)",
|
|
997
|
+
"https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html"
|
|
998
|
+
),
|
|
999
|
+
awsUnknownError: (code, action, detail) => new WrapsError(
|
|
1000
|
+
`AWS API error: ${code}${action ? ` (${action})` : ""}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
1001
|
+
`AWS_${code}`,
|
|
1002
|
+
`This is an AWS API error, not a credentials problem. Look up "${code}" in the AWS documentation for the failing service.
|
|
1003
|
+
|
|
1004
|
+
If you believe this is a Wraps bug, report it at:
|
|
1005
|
+
https://github.com/wraps-team/wraps/issues`,
|
|
1006
|
+
"https://wraps.dev/docs/guides/aws-setup/troubleshooting"
|
|
1007
|
+
),
|
|
1750
1008
|
dynamoDBPermissionDenied: () => new WrapsError(
|
|
1751
1009
|
"DynamoDB permission denied",
|
|
1752
1010
|
"DYNAMODB_PERMISSION_DENIED",
|
|
@@ -1862,6 +1120,7 @@ __export(aws_exports, {
|
|
|
1862
1120
|
getSESAccountStatus: () => getSESAccountStatus,
|
|
1863
1121
|
isSESSandbox: () => isSESSandbox,
|
|
1864
1122
|
listSESDomains: () => listSESDomains,
|
|
1123
|
+
resolveAWSCredentialsToEnv: () => resolveAWSCredentialsToEnv,
|
|
1865
1124
|
validateAWSCredentials: () => validateAWSCredentials,
|
|
1866
1125
|
validateAWSCredentialsWithDetails: () => validateAWSCredentialsWithDetails
|
|
1867
1126
|
});
|
|
@@ -1929,127 +1188,999 @@ async function validateAWSCredentialsWithDetails() {
|
|
|
1929
1188
|
case "UnrecognizedClientException":
|
|
1930
1189
|
throw errors.accessKeyInvalid();
|
|
1931
1190
|
}
|
|
1932
|
-
}
|
|
1933
|
-
throw errors.noAWSCredentials();
|
|
1191
|
+
}
|
|
1192
|
+
throw errors.noAWSCredentials();
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
async function resolveAWSCredentialsToEnv() {
|
|
1196
|
+
delete process.env.AWS_PROFILE;
|
|
1197
|
+
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const sts = new STSClient2({});
|
|
1201
|
+
const provider = sts.config.credentials;
|
|
1202
|
+
if (!provider) {
|
|
1203
|
+
throw errors.noAWSCredentials();
|
|
1204
|
+
}
|
|
1205
|
+
let creds;
|
|
1206
|
+
try {
|
|
1207
|
+
creds = typeof provider === "function" ? await provider() : provider;
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
if (error instanceof Error) {
|
|
1210
|
+
if (error.name === "ExpiredTokenException" || error.name === "TokenRefreshRequired" || error.name === "SSOTokenExpired") {
|
|
1211
|
+
throw errors.sessionTokenExpired();
|
|
1212
|
+
}
|
|
1213
|
+
if (error.message?.includes("Could not load credentials")) {
|
|
1214
|
+
throw errors.credentialsFileMissing();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
throw errors.noAWSCredentials();
|
|
1218
|
+
}
|
|
1219
|
+
process.env.AWS_ACCESS_KEY_ID = creds.accessKeyId;
|
|
1220
|
+
process.env.AWS_SECRET_ACCESS_KEY = creds.secretAccessKey;
|
|
1221
|
+
if (creds.sessionToken) {
|
|
1222
|
+
process.env.AWS_SESSION_TOKEN = creds.sessionToken;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async function checkRegion(region) {
|
|
1226
|
+
return SES_REGIONS.includes(region);
|
|
1227
|
+
}
|
|
1228
|
+
async function getAWSRegion() {
|
|
1229
|
+
if (process.env.AWS_REGION) {
|
|
1230
|
+
return process.env.AWS_REGION;
|
|
1231
|
+
}
|
|
1232
|
+
if (process.env.AWS_DEFAULT_REGION) {
|
|
1233
|
+
return process.env.AWS_DEFAULT_REGION;
|
|
1234
|
+
}
|
|
1235
|
+
return "us-east-1";
|
|
1236
|
+
}
|
|
1237
|
+
async function listSESDomains(region) {
|
|
1238
|
+
const ses = new SESClient({ region });
|
|
1239
|
+
try {
|
|
1240
|
+
const identitiesResponse = await ses.send(
|
|
1241
|
+
new ListIdentitiesCommand({
|
|
1242
|
+
IdentityType: "Domain"
|
|
1243
|
+
})
|
|
1244
|
+
);
|
|
1245
|
+
const identities = identitiesResponse.Identities || [];
|
|
1246
|
+
if (identities.length === 0) {
|
|
1247
|
+
return [];
|
|
1248
|
+
}
|
|
1249
|
+
const attributesResponse = await ses.send(
|
|
1250
|
+
new GetIdentityVerificationAttributesCommand({
|
|
1251
|
+
Identities: identities
|
|
1252
|
+
})
|
|
1253
|
+
);
|
|
1254
|
+
const attributes = attributesResponse.VerificationAttributes || {};
|
|
1255
|
+
return identities.map((domain) => ({
|
|
1256
|
+
domain,
|
|
1257
|
+
verified: attributes[domain]?.VerificationStatus === "Success"
|
|
1258
|
+
}));
|
|
1259
|
+
} catch {
|
|
1260
|
+
return [];
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async function getSESAccountStatus(region) {
|
|
1264
|
+
const sesv22 = new SESv2Client({ region });
|
|
1265
|
+
try {
|
|
1266
|
+
const response = await sesv22.send(new GetAccountCommand({}));
|
|
1267
|
+
return {
|
|
1268
|
+
isSandbox: !response.ProductionAccessEnabled,
|
|
1269
|
+
sendQuota: response.SendQuota ? {
|
|
1270
|
+
max24HourSend: response.SendQuota.Max24HourSend ?? 0,
|
|
1271
|
+
maxSendRate: response.SendQuota.MaxSendRate ?? 0,
|
|
1272
|
+
sentLast24Hours: response.SendQuota.SentLast24Hours ?? 0
|
|
1273
|
+
} : void 0,
|
|
1274
|
+
enforcementStatus: response.EnforcementStatus
|
|
1275
|
+
};
|
|
1276
|
+
} catch {
|
|
1277
|
+
return { isSandbox: true, sandboxUncertain: true };
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async function isSESSandbox(region) {
|
|
1281
|
+
const status2 = await getSESAccountStatus(region);
|
|
1282
|
+
return status2.isSandbox;
|
|
1283
|
+
}
|
|
1284
|
+
async function getACMCertificateStatus(certificateArn) {
|
|
1285
|
+
const acm3 = new ACMClient({ region: "us-east-1" });
|
|
1286
|
+
try {
|
|
1287
|
+
const response = await acm3.send(
|
|
1288
|
+
new DescribeCertificateCommand({
|
|
1289
|
+
CertificateArn: certificateArn
|
|
1290
|
+
})
|
|
1291
|
+
);
|
|
1292
|
+
const certificate = response.Certificate;
|
|
1293
|
+
if (!certificate) {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
const validationRecords = certificate.DomainValidationOptions?.map((option) => ({
|
|
1297
|
+
name: option.ResourceRecord?.Name || "",
|
|
1298
|
+
type: option.ResourceRecord?.Type || "",
|
|
1299
|
+
value: option.ResourceRecord?.Value || ""
|
|
1300
|
+
})) || [];
|
|
1301
|
+
return {
|
|
1302
|
+
status: certificate.Status || "UNKNOWN",
|
|
1303
|
+
domainName: certificate.DomainName || "",
|
|
1304
|
+
validationRecords
|
|
1305
|
+
};
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
console.error("Error getting ACM certificate status:", error);
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
var SES_REGIONS;
|
|
1312
|
+
var init_aws = __esm({
|
|
1313
|
+
"src/utils/shared/aws.ts"() {
|
|
1314
|
+
"use strict";
|
|
1315
|
+
init_esm_shims();
|
|
1316
|
+
init_aws_detection();
|
|
1317
|
+
init_errors();
|
|
1318
|
+
SES_REGIONS = [
|
|
1319
|
+
"us-east-1",
|
|
1320
|
+
"us-east-2",
|
|
1321
|
+
"us-west-1",
|
|
1322
|
+
"us-west-2",
|
|
1323
|
+
"af-south-1",
|
|
1324
|
+
"ap-east-1",
|
|
1325
|
+
"ap-south-1",
|
|
1326
|
+
"ap-northeast-1",
|
|
1327
|
+
"ap-northeast-2",
|
|
1328
|
+
"ap-northeast-3",
|
|
1329
|
+
"ap-southeast-1",
|
|
1330
|
+
"ap-southeast-2",
|
|
1331
|
+
"ap-southeast-3",
|
|
1332
|
+
"ca-central-1",
|
|
1333
|
+
"eu-central-1",
|
|
1334
|
+
"eu-west-1",
|
|
1335
|
+
"eu-west-2",
|
|
1336
|
+
"eu-west-3",
|
|
1337
|
+
"eu-south-1",
|
|
1338
|
+
"eu-north-1",
|
|
1339
|
+
"me-south-1",
|
|
1340
|
+
"sa-east-1"
|
|
1341
|
+
];
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
// src/utils/shared/s3-state.ts
|
|
1346
|
+
var s3_state_exports = {};
|
|
1347
|
+
__export(s3_state_exports, {
|
|
1348
|
+
clearS3StackLocks: () => clearS3StackLocks,
|
|
1349
|
+
deleteMetadata: () => deleteMetadata,
|
|
1350
|
+
downloadMetadata: () => downloadMetadata,
|
|
1351
|
+
ensureStateBucket: () => ensureStateBucket,
|
|
1352
|
+
getS3BackendUrl: () => getS3BackendUrl,
|
|
1353
|
+
getStateBucketName: () => getStateBucketName,
|
|
1354
|
+
migrateLocalPulumiState: () => migrateLocalPulumiState,
|
|
1355
|
+
needsMigration: () => needsMigration,
|
|
1356
|
+
stateBucketExists: () => stateBucketExists,
|
|
1357
|
+
uploadMetadata: () => uploadMetadata
|
|
1358
|
+
});
|
|
1359
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
1360
|
+
import { readdir, writeFile } from "fs/promises";
|
|
1361
|
+
import { join as join2 } from "path";
|
|
1362
|
+
function has404StatusCode(error) {
|
|
1363
|
+
if (!(error instanceof Error)) {
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
const metadataError = error;
|
|
1367
|
+
return metadataError.$metadata?.httpStatusCode === 404;
|
|
1368
|
+
}
|
|
1369
|
+
function getStateBucketName(accountId, region) {
|
|
1370
|
+
return `wraps-state-${accountId}-${region}`;
|
|
1371
|
+
}
|
|
1372
|
+
function getS3BackendUrl(accountId, region) {
|
|
1373
|
+
return `s3://${getStateBucketName(accountId, region)}`;
|
|
1374
|
+
}
|
|
1375
|
+
async function stateBucketExists(accountId, region) {
|
|
1376
|
+
const { S3Client: S3Client2, HeadBucketCommand } = await import("@aws-sdk/client-s3");
|
|
1377
|
+
const client = new S3Client2({ region });
|
|
1378
|
+
const bucketName = getStateBucketName(accountId, region);
|
|
1379
|
+
try {
|
|
1380
|
+
await client.send(new HeadBucketCommand({ Bucket: bucketName }));
|
|
1381
|
+
return true;
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchBucket" || has404StatusCode(error))) {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
throw error;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async function ensureStateBucket(accountId, region) {
|
|
1390
|
+
const {
|
|
1391
|
+
S3Client: S3Client2,
|
|
1392
|
+
HeadBucketCommand,
|
|
1393
|
+
CreateBucketCommand,
|
|
1394
|
+
PutBucketEncryptionCommand,
|
|
1395
|
+
PutBucketVersioningCommand,
|
|
1396
|
+
PutPublicAccessBlockCommand,
|
|
1397
|
+
PutBucketTaggingCommand
|
|
1398
|
+
} = await import("@aws-sdk/client-s3");
|
|
1399
|
+
const client = new S3Client2({ region });
|
|
1400
|
+
const bucketName = getStateBucketName(accountId, region);
|
|
1401
|
+
try {
|
|
1402
|
+
await client.send(new HeadBucketCommand({ Bucket: bucketName }));
|
|
1403
|
+
return bucketName;
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
const isNotFound = error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchBucket" || has404StatusCode(error));
|
|
1406
|
+
if (!isNotFound) {
|
|
1407
|
+
throw error;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
const createParams = { Bucket: bucketName };
|
|
1411
|
+
if (region !== "us-east-1") {
|
|
1412
|
+
createParams.CreateBucketConfiguration = {
|
|
1413
|
+
LocationConstraint: region
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
await client.send(new CreateBucketCommand(createParams));
|
|
1417
|
+
await client.send(
|
|
1418
|
+
new PutBucketEncryptionCommand({
|
|
1419
|
+
Bucket: bucketName,
|
|
1420
|
+
ServerSideEncryptionConfiguration: {
|
|
1421
|
+
Rules: [
|
|
1422
|
+
{
|
|
1423
|
+
ApplyServerSideEncryptionByDefault: {
|
|
1424
|
+
SSEAlgorithm: "AES256"
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
]
|
|
1428
|
+
}
|
|
1429
|
+
})
|
|
1430
|
+
);
|
|
1431
|
+
await client.send(
|
|
1432
|
+
new PutBucketVersioningCommand({
|
|
1433
|
+
Bucket: bucketName,
|
|
1434
|
+
VersioningConfiguration: {
|
|
1435
|
+
Status: "Enabled"
|
|
1436
|
+
}
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
1439
|
+
await client.send(
|
|
1440
|
+
new PutPublicAccessBlockCommand({
|
|
1441
|
+
Bucket: bucketName,
|
|
1442
|
+
PublicAccessBlockConfiguration: {
|
|
1443
|
+
BlockPublicAcls: true,
|
|
1444
|
+
BlockPublicPolicy: true,
|
|
1445
|
+
IgnorePublicAcls: true,
|
|
1446
|
+
RestrictPublicBuckets: true
|
|
1447
|
+
}
|
|
1448
|
+
})
|
|
1449
|
+
);
|
|
1450
|
+
await client.send(
|
|
1451
|
+
new PutBucketTaggingCommand({
|
|
1452
|
+
Bucket: bucketName,
|
|
1453
|
+
Tagging: {
|
|
1454
|
+
TagSet: [
|
|
1455
|
+
{ Key: "ManagedBy", Value: "wraps-cli" },
|
|
1456
|
+
{ Key: "Purpose", Value: "state" }
|
|
1457
|
+
]
|
|
1458
|
+
}
|
|
1459
|
+
})
|
|
1460
|
+
);
|
|
1461
|
+
return bucketName;
|
|
1462
|
+
}
|
|
1463
|
+
async function uploadMetadata(bucketName, metadata) {
|
|
1464
|
+
const { S3Client: S3Client2, PutObjectCommand } = await import("@aws-sdk/client-s3");
|
|
1465
|
+
const client = new S3Client2({ region: metadata.region });
|
|
1466
|
+
const key = `metadata/${metadata.accountId}-${metadata.region}.json`;
|
|
1467
|
+
await client.send(
|
|
1468
|
+
new PutObjectCommand({
|
|
1469
|
+
Bucket: bucketName,
|
|
1470
|
+
Key: key,
|
|
1471
|
+
Body: JSON.stringify(metadata, null, 2),
|
|
1472
|
+
ContentType: "application/json"
|
|
1473
|
+
})
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
async function deleteMetadata(bucketName, accountId, region) {
|
|
1477
|
+
const { S3Client: S3Client2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
|
1478
|
+
const client = new S3Client2({ region });
|
|
1479
|
+
const key = `metadata/${accountId}-${region}.json`;
|
|
1480
|
+
await client.send(
|
|
1481
|
+
new DeleteObjectCommand({
|
|
1482
|
+
Bucket: bucketName,
|
|
1483
|
+
Key: key
|
|
1484
|
+
})
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
async function clearS3StackLocks(accountId, region) {
|
|
1488
|
+
const { S3Client: S3Client2, ListObjectsV2Command: ListObjectsV2Command2, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
|
1489
|
+
const client = new S3Client2({ region });
|
|
1490
|
+
const bucketName = getStateBucketName(accountId, region);
|
|
1491
|
+
const prefix = ".pulumi/locks/";
|
|
1492
|
+
const response = await client.send(
|
|
1493
|
+
new ListObjectsV2Command2({ Bucket: bucketName, Prefix: prefix })
|
|
1494
|
+
);
|
|
1495
|
+
const lockObjects = response.Contents ?? [];
|
|
1496
|
+
if (lockObjects.length === 0) {
|
|
1497
|
+
return 0;
|
|
1498
|
+
}
|
|
1499
|
+
for (const obj of lockObjects) {
|
|
1500
|
+
if (obj.Key) {
|
|
1501
|
+
await client.send(
|
|
1502
|
+
new DeleteObjectCommand({ Bucket: bucketName, Key: obj.Key })
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return lockObjects.length;
|
|
1507
|
+
}
|
|
1508
|
+
async function downloadMetadata(bucketName, accountId, region) {
|
|
1509
|
+
const { S3Client: S3Client2, GetObjectCommand: GetObjectCommand2 } = await import("@aws-sdk/client-s3");
|
|
1510
|
+
const client = new S3Client2({ region });
|
|
1511
|
+
const key = `metadata/${accountId}-${region}.json`;
|
|
1512
|
+
try {
|
|
1513
|
+
const response = await client.send(
|
|
1514
|
+
new GetObjectCommand2({
|
|
1515
|
+
Bucket: bucketName,
|
|
1516
|
+
Key: key
|
|
1517
|
+
})
|
|
1518
|
+
);
|
|
1519
|
+
const body = await response.Body?.transformToString();
|
|
1520
|
+
if (!body) {
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
return JSON.parse(body);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
if (error instanceof Error && (error.name === "NoSuchKey" || has404StatusCode(error))) {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
throw error;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async function needsMigration(localPulumiDir, accountId, region) {
|
|
1532
|
+
const markerPath = join2(localPulumiDir, `.migrated-${accountId}-${region}`);
|
|
1533
|
+
if (existsSync2(markerPath)) {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
const stacksDir = join2(localPulumiDir, ".pulumi", "stacks");
|
|
1537
|
+
if (!existsSync2(stacksDir)) {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
try {
|
|
1541
|
+
const entries = await readdir(stacksDir);
|
|
1542
|
+
for (const entry of entries) {
|
|
1543
|
+
const entryPath = join2(stacksDir, entry);
|
|
1544
|
+
if (statSync(entryPath).isDirectory()) {
|
|
1545
|
+
const files = await readdir(entryPath);
|
|
1546
|
+
const matching = files.filter(
|
|
1547
|
+
(f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
|
|
1548
|
+
);
|
|
1549
|
+
if (matching.length > 0) {
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return false;
|
|
1555
|
+
} catch {
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async function migrateLocalPulumiState(localPulumiDir, bucketName, accountId, region) {
|
|
1560
|
+
const pulumi31 = await import("@pulumi/pulumi/automation/index.js");
|
|
1561
|
+
const stacksDir = join2(localPulumiDir, ".pulumi", "stacks");
|
|
1562
|
+
const entries = await readdir(stacksDir);
|
|
1563
|
+
for (const entry of entries) {
|
|
1564
|
+
const entryPath = join2(stacksDir, entry);
|
|
1565
|
+
if (!statSync(entryPath).isDirectory()) {
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
const projectName = entry;
|
|
1569
|
+
const files = await readdir(entryPath);
|
|
1570
|
+
const stackFiles = files.filter(
|
|
1571
|
+
(f) => f.includes(accountId) && f.includes(region) && f.endsWith(".json")
|
|
1572
|
+
);
|
|
1573
|
+
for (const stackFile of stackFiles) {
|
|
1574
|
+
const stackName = stackFile.replace(".json", "");
|
|
1575
|
+
try {
|
|
1576
|
+
const localStack = await pulumi31.LocalWorkspace.selectStack({
|
|
1577
|
+
stackName,
|
|
1578
|
+
workDir: localPulumiDir
|
|
1579
|
+
});
|
|
1580
|
+
const state = await localStack.exportStack();
|
|
1581
|
+
const s3Stack = await pulumi31.LocalWorkspace.createOrSelectStack(
|
|
1582
|
+
{
|
|
1583
|
+
stackName,
|
|
1584
|
+
projectName,
|
|
1585
|
+
program: async () => ({})
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
workDir: localPulumiDir,
|
|
1589
|
+
envVars: {
|
|
1590
|
+
PULUMI_BACKEND_URL: `s3://${bucketName}`,
|
|
1591
|
+
PULUMI_CONFIG_PASSPHRASE: ""
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
);
|
|
1595
|
+
await s3Stack.importStack(state);
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
console.error(
|
|
1598
|
+
`Warning: Failed to migrate stack ${stackName}: ${error instanceof Error ? error.message : error}`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const markerPath = join2(localPulumiDir, `.migrated-${accountId}-${region}`);
|
|
1604
|
+
await writeFile(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
|
|
1605
|
+
}
|
|
1606
|
+
var init_s3_state = __esm({
|
|
1607
|
+
"src/utils/shared/s3-state.ts"() {
|
|
1608
|
+
"use strict";
|
|
1609
|
+
init_esm_shims();
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
// src/utils/shared/fs.ts
|
|
1614
|
+
var fs_exports = {};
|
|
1615
|
+
__export(fs_exports, {
|
|
1616
|
+
clearLocalStackLocks: () => clearLocalStackLocks,
|
|
1617
|
+
ensurePulumiWorkDir: () => ensurePulumiWorkDir,
|
|
1618
|
+
ensureWrapsDir: () => ensureWrapsDir,
|
|
1619
|
+
getPulumiWorkDir: () => getPulumiWorkDir,
|
|
1620
|
+
getWrapsDir: () => getWrapsDir
|
|
1621
|
+
});
|
|
1622
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1623
|
+
import { mkdir, readdir as readdir2, rm } from "fs/promises";
|
|
1624
|
+
import { homedir as homedir2 } from "os";
|
|
1625
|
+
import { join as join3 } from "path";
|
|
1626
|
+
function getWrapsDir() {
|
|
1627
|
+
return join3(homedir2(), ".wraps");
|
|
1628
|
+
}
|
|
1629
|
+
function getPulumiWorkDir() {
|
|
1630
|
+
return join3(getWrapsDir(), "pulumi");
|
|
1631
|
+
}
|
|
1632
|
+
async function ensureWrapsDir() {
|
|
1633
|
+
const wrapsDir = getWrapsDir();
|
|
1634
|
+
if (!existsSync3(wrapsDir)) {
|
|
1635
|
+
await mkdir(wrapsDir, { recursive: true });
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
async function clearLocalStackLocks() {
|
|
1639
|
+
const locksDir = join3(getPulumiWorkDir(), ".pulumi", "locks");
|
|
1640
|
+
if (!existsSync3(locksDir)) {
|
|
1641
|
+
return 0;
|
|
1642
|
+
}
|
|
1643
|
+
let count = 0;
|
|
1644
|
+
async function walkAndDelete(dir) {
|
|
1645
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1646
|
+
for (const entry of entries) {
|
|
1647
|
+
const fullPath = join3(dir, entry.name);
|
|
1648
|
+
if (entry.isDirectory()) {
|
|
1649
|
+
await walkAndDelete(fullPath);
|
|
1650
|
+
} else if (entry.name.endsWith(".json")) {
|
|
1651
|
+
await rm(fullPath);
|
|
1652
|
+
count++;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
await walkAndDelete(locksDir);
|
|
1657
|
+
return count;
|
|
1658
|
+
}
|
|
1659
|
+
async function ensurePulumiWorkDir(options) {
|
|
1660
|
+
await ensureWrapsDir();
|
|
1661
|
+
const pulumiDir = getPulumiWorkDir();
|
|
1662
|
+
if (!existsSync3(pulumiDir)) {
|
|
1663
|
+
await mkdir(pulumiDir, { recursive: true });
|
|
1664
|
+
}
|
|
1665
|
+
process.env.PULUMI_CONFIG_PASSPHRASE = "";
|
|
1666
|
+
if (options?.accountId && options?.region) {
|
|
1667
|
+
const { resolveAWSCredentialsToEnv: resolveAWSCredentialsToEnv2 } = await Promise.resolve().then(() => (init_aws(), aws_exports));
|
|
1668
|
+
await resolveAWSCredentialsToEnv2();
|
|
1669
|
+
}
|
|
1670
|
+
const useS3 = options?.accountId && options?.region && process.env.WRAPS_LOCAL_ONLY !== "1";
|
|
1671
|
+
if (useS3) {
|
|
1672
|
+
try {
|
|
1673
|
+
const {
|
|
1674
|
+
ensureStateBucket: ensureStateBucket2,
|
|
1675
|
+
getS3BackendUrl: getS3BackendUrl2,
|
|
1676
|
+
needsMigration: needsMigration2,
|
|
1677
|
+
migrateLocalPulumiState: migrateLocalPulumiState2
|
|
1678
|
+
} = await Promise.resolve().then(() => (init_s3_state(), s3_state_exports));
|
|
1679
|
+
const bucketName = await ensureStateBucket2(
|
|
1680
|
+
options.accountId,
|
|
1681
|
+
options.region
|
|
1682
|
+
);
|
|
1683
|
+
const shouldMigrate = await needsMigration2(
|
|
1684
|
+
pulumiDir,
|
|
1685
|
+
options.accountId,
|
|
1686
|
+
options.region
|
|
1687
|
+
);
|
|
1688
|
+
if (shouldMigrate) {
|
|
1689
|
+
process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
|
|
1690
|
+
await migrateLocalPulumiState2(
|
|
1691
|
+
pulumiDir,
|
|
1692
|
+
bucketName,
|
|
1693
|
+
options.accountId,
|
|
1694
|
+
options.region
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
process.env.PULUMI_BACKEND_URL = getS3BackendUrl2(
|
|
1698
|
+
options.accountId,
|
|
1699
|
+
options.region
|
|
1700
|
+
);
|
|
1701
|
+
return;
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
const clack54 = await import("@clack/prompts");
|
|
1704
|
+
clack54.log.warn(
|
|
1705
|
+
`S3 state backend unavailable (${error instanceof Error ? error.message : error}). Using local state.`
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
|
|
1710
|
+
}
|
|
1711
|
+
var init_fs = __esm({
|
|
1712
|
+
"src/utils/shared/fs.ts"() {
|
|
1713
|
+
"use strict";
|
|
1714
|
+
init_esm_shims();
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
// src/utils/shared/config.ts
|
|
1719
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1720
|
+
import { chmod, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
1721
|
+
import { join as join4 } from "path";
|
|
1722
|
+
function getApiBaseUrl() {
|
|
1723
|
+
return process.env.WRAPS_API_URL || "https://api.wraps.dev";
|
|
1724
|
+
}
|
|
1725
|
+
function getAppBaseUrl() {
|
|
1726
|
+
return process.env.WRAPS_APP_URL || "https://app.wraps.dev";
|
|
1727
|
+
}
|
|
1728
|
+
function getConfigPath() {
|
|
1729
|
+
return join4(getWrapsDir(), CONFIG_FILE);
|
|
1730
|
+
}
|
|
1731
|
+
async function readAuthConfig() {
|
|
1732
|
+
const path3 = getConfigPath();
|
|
1733
|
+
if (!existsSync4(path3)) {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
try {
|
|
1737
|
+
const content = await readFile(path3, "utf-8");
|
|
1738
|
+
return JSON.parse(content);
|
|
1739
|
+
} catch {
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async function saveAuthConfig(config2) {
|
|
1744
|
+
await ensureWrapsDir();
|
|
1745
|
+
const path3 = getConfigPath();
|
|
1746
|
+
const existing = await readAuthConfig();
|
|
1747
|
+
const merged = existing ? { ...existing, ...config2 } : config2;
|
|
1748
|
+
await writeFile2(path3, JSON.stringify(merged, null, 2), "utf-8");
|
|
1749
|
+
await chmod(path3, 384);
|
|
1750
|
+
}
|
|
1751
|
+
async function clearAuthConfig() {
|
|
1752
|
+
const existing = await readAuthConfig();
|
|
1753
|
+
if (existing) {
|
|
1754
|
+
existing.auth = void 0;
|
|
1755
|
+
await saveAuthConfig(existing);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function resolveToken(flags2) {
|
|
1759
|
+
return flags2?.token || process.env.WRAPS_API_KEY || null;
|
|
1760
|
+
}
|
|
1761
|
+
async function resolveTokenAsync(flags2) {
|
|
1762
|
+
const sync = resolveToken(flags2);
|
|
1763
|
+
if (sync) {
|
|
1764
|
+
return sync;
|
|
1765
|
+
}
|
|
1766
|
+
const config2 = await readAuthConfig();
|
|
1767
|
+
if (!config2?.auth?.token) {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
if (config2.auth.expiresAt && new Date(config2.auth.expiresAt) <= /* @__PURE__ */ new Date()) {
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
return config2.auth.token;
|
|
1774
|
+
}
|
|
1775
|
+
var CONFIG_FILE;
|
|
1776
|
+
var init_config = __esm({
|
|
1777
|
+
"src/utils/shared/config.ts"() {
|
|
1778
|
+
"use strict";
|
|
1779
|
+
init_esm_shims();
|
|
1780
|
+
init_fs();
|
|
1781
|
+
CONFIG_FILE = "config.json";
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
// src/telemetry/config.ts
|
|
1786
|
+
import Conf from "conf";
|
|
1787
|
+
import { v4 as uuidv4 } from "uuid";
|
|
1788
|
+
var CONFIG_DEFAULTS, TelemetryConfigManager;
|
|
1789
|
+
var init_config2 = __esm({
|
|
1790
|
+
"src/telemetry/config.ts"() {
|
|
1791
|
+
"use strict";
|
|
1792
|
+
init_esm_shims();
|
|
1793
|
+
CONFIG_DEFAULTS = {
|
|
1794
|
+
enabled: true,
|
|
1795
|
+
anonymousId: uuidv4(),
|
|
1796
|
+
notificationShown: false
|
|
1797
|
+
};
|
|
1798
|
+
TelemetryConfigManager = class {
|
|
1799
|
+
config;
|
|
1800
|
+
constructor(options) {
|
|
1801
|
+
this.config = new Conf({
|
|
1802
|
+
projectName: "wraps",
|
|
1803
|
+
configName: "telemetry",
|
|
1804
|
+
defaults: CONFIG_DEFAULTS,
|
|
1805
|
+
cwd: options?.cwd
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Check if telemetry is enabled
|
|
1810
|
+
*/
|
|
1811
|
+
isEnabled() {
|
|
1812
|
+
return this.config.get("enabled");
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Enable or disable telemetry
|
|
1816
|
+
*/
|
|
1817
|
+
setEnabled(enabled) {
|
|
1818
|
+
this.config.set("enabled", enabled);
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Get the anonymous user ID
|
|
1822
|
+
*/
|
|
1823
|
+
getAnonymousId() {
|
|
1824
|
+
return this.config.get("anonymousId");
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Check if the first-run notification has been shown
|
|
1828
|
+
*/
|
|
1829
|
+
hasShownNotification() {
|
|
1830
|
+
return this.config.get("notificationShown");
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Mark the first-run notification as shown
|
|
1834
|
+
*/
|
|
1835
|
+
markNotificationShown() {
|
|
1836
|
+
this.config.set("notificationShown", true);
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Get the full path to the configuration file
|
|
1840
|
+
*/
|
|
1841
|
+
getConfigPath() {
|
|
1842
|
+
return this.config.path;
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Reset configuration to defaults
|
|
1846
|
+
*/
|
|
1847
|
+
reset() {
|
|
1848
|
+
this.config.clear();
|
|
1849
|
+
this.config.set({
|
|
1850
|
+
...CONFIG_DEFAULTS,
|
|
1851
|
+
anonymousId: uuidv4()
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
// src/telemetry/client.ts
|
|
1859
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1860
|
+
import { dirname, join as join5 } from "path";
|
|
1861
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1862
|
+
import pc2 from "picocolors";
|
|
1863
|
+
function getTelemetryClient() {
|
|
1864
|
+
if (!telemetryInstance) {
|
|
1865
|
+
telemetryInstance = new TelemetryClient();
|
|
1866
|
+
}
|
|
1867
|
+
return telemetryInstance;
|
|
1868
|
+
}
|
|
1869
|
+
var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, TelemetryClient, telemetryInstance;
|
|
1870
|
+
var init_client = __esm({
|
|
1871
|
+
"src/telemetry/client.ts"() {
|
|
1872
|
+
"use strict";
|
|
1873
|
+
init_esm_shims();
|
|
1874
|
+
init_ci_detection();
|
|
1875
|
+
init_config();
|
|
1876
|
+
init_config2();
|
|
1877
|
+
DEFAULT_ENDPOINT = process.env.WRAPS_TELEMETRY_URL || "https://wraps.dev/api/telemetry";
|
|
1878
|
+
DEFAULT_TIMEOUT = 2e3;
|
|
1879
|
+
TelemetryClient = class {
|
|
1880
|
+
config;
|
|
1881
|
+
endpoint;
|
|
1882
|
+
timeout;
|
|
1883
|
+
debug;
|
|
1884
|
+
enabled;
|
|
1885
|
+
eventQueue = [];
|
|
1886
|
+
flushTimer;
|
|
1887
|
+
hasShownFooter = false;
|
|
1888
|
+
userId;
|
|
1889
|
+
userIdResolved = false;
|
|
1890
|
+
constructor(options = {}) {
|
|
1891
|
+
this.config = new TelemetryConfigManager();
|
|
1892
|
+
this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
|
|
1893
|
+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
1894
|
+
this.debug = options.debug || process.env.WRAPS_TELEMETRY_DEBUG === "1";
|
|
1895
|
+
this.enabled = this.shouldBeEnabled();
|
|
1896
|
+
this.resolveUserId();
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Resolve authenticated user identity from CLI auth config.
|
|
1900
|
+
* Uses the first organization ID as the user identifier,
|
|
1901
|
+
* linking CLI telemetry to the same org tracked on the web dashboard.
|
|
1902
|
+
*/
|
|
1903
|
+
async resolveUserId() {
|
|
1904
|
+
try {
|
|
1905
|
+
const config2 = await readAuthConfig();
|
|
1906
|
+
if (config2?.auth?.token && config2.auth.organizations?.length) {
|
|
1907
|
+
this.userId = config2.auth.organizations[0].id;
|
|
1908
|
+
}
|
|
1909
|
+
} catch {
|
|
1910
|
+
} finally {
|
|
1911
|
+
this.userIdResolved = true;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Determine if telemetry should be enabled based on environment and config
|
|
1916
|
+
*/
|
|
1917
|
+
shouldBeEnabled() {
|
|
1918
|
+
if (process.env.DO_NOT_TRACK === "1") {
|
|
1919
|
+
return false;
|
|
1920
|
+
}
|
|
1921
|
+
if (process.env.WRAPS_TELEMETRY_DISABLED === "1") {
|
|
1922
|
+
return false;
|
|
1923
|
+
}
|
|
1924
|
+
if (isCI()) {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
if (!this.config.isEnabled()) {
|
|
1928
|
+
return false;
|
|
1929
|
+
}
|
|
1930
|
+
return true;
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Track an event
|
|
1934
|
+
*
|
|
1935
|
+
* @param event - Event name in format "category:action" (e.g., "command:init")
|
|
1936
|
+
* @param properties - Additional event properties (no PII)
|
|
1937
|
+
*/
|
|
1938
|
+
track(event, properties) {
|
|
1939
|
+
const telemetryEvent = {
|
|
1940
|
+
event,
|
|
1941
|
+
properties: {
|
|
1942
|
+
...properties,
|
|
1943
|
+
cli_version: this.getCLIVersion(),
|
|
1944
|
+
os: process.platform,
|
|
1945
|
+
node_version: process.version,
|
|
1946
|
+
ci: isCI()
|
|
1947
|
+
},
|
|
1948
|
+
anonymousId: this.config.getAnonymousId(),
|
|
1949
|
+
...this.userId ? { userId: this.userId } : {},
|
|
1950
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1951
|
+
};
|
|
1952
|
+
if (this.debug) {
|
|
1953
|
+
console.log(
|
|
1954
|
+
"[Telemetry Debug] Event:",
|
|
1955
|
+
JSON.stringify(telemetryEvent, null, 2)
|
|
1956
|
+
);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (!this.enabled) {
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
this.eventQueue.push(telemetryEvent);
|
|
1963
|
+
if (this.flushTimer) {
|
|
1964
|
+
clearTimeout(this.flushTimer);
|
|
1965
|
+
}
|
|
1966
|
+
this.flushTimer = setTimeout(() => this.flush(), 100);
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Flush queued events to server
|
|
1970
|
+
*/
|
|
1971
|
+
async flush() {
|
|
1972
|
+
if (this.eventQueue.length === 0) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const eventsToSend = [...this.eventQueue];
|
|
1976
|
+
this.eventQueue = [];
|
|
1977
|
+
try {
|
|
1978
|
+
const controller = new AbortController();
|
|
1979
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1980
|
+
const requestBody = {
|
|
1981
|
+
events: eventsToSend,
|
|
1982
|
+
batch: true
|
|
1983
|
+
};
|
|
1984
|
+
await fetch(this.endpoint, {
|
|
1985
|
+
method: "POST",
|
|
1986
|
+
headers: {
|
|
1987
|
+
"Content-Type": "application/json"
|
|
1988
|
+
},
|
|
1989
|
+
body: JSON.stringify(requestBody),
|
|
1990
|
+
signal: controller.signal
|
|
1991
|
+
});
|
|
1992
|
+
clearTimeout(timeoutId);
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
if (this.debug) {
|
|
1995
|
+
console.error("[Telemetry Debug] Failed to send events:", error);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Flush and wait for all events to be sent
|
|
2001
|
+
* Should be called before CLI exits
|
|
2002
|
+
*/
|
|
2003
|
+
async shutdown() {
|
|
2004
|
+
if (this.flushTimer) {
|
|
2005
|
+
clearTimeout(this.flushTimer);
|
|
2006
|
+
}
|
|
2007
|
+
if (!this.userIdResolved) {
|
|
2008
|
+
await new Promise((resolve) => {
|
|
2009
|
+
const check2 = () => {
|
|
2010
|
+
if (this.userIdResolved) {
|
|
2011
|
+
return resolve();
|
|
2012
|
+
}
|
|
2013
|
+
setTimeout(check2, 10);
|
|
2014
|
+
};
|
|
2015
|
+
check2();
|
|
2016
|
+
setTimeout(resolve, 100);
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
if (this.userId) {
|
|
2020
|
+
for (const evt of this.eventQueue) {
|
|
2021
|
+
if (!evt.userId) {
|
|
2022
|
+
evt.userId = this.userId;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
await this.flush();
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Enable telemetry.
|
|
2030
|
+
* Returns null if enabled, or a string describing why an env override prevented it.
|
|
2031
|
+
*/
|
|
2032
|
+
enable() {
|
|
2033
|
+
this.config.setEnabled(true);
|
|
2034
|
+
const override = this.getEnvOverride();
|
|
2035
|
+
if (override) {
|
|
2036
|
+
this.enabled = false;
|
|
2037
|
+
return override;
|
|
2038
|
+
}
|
|
2039
|
+
this.enabled = true;
|
|
2040
|
+
return null;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Check if an environment variable is overriding the config.
|
|
2044
|
+
* Returns a human-readable reason, or null if no override.
|
|
2045
|
+
*/
|
|
2046
|
+
getEnvOverride() {
|
|
2047
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
|
|
2048
|
+
return "DO_NOT_TRACK environment variable is set";
|
|
2049
|
+
}
|
|
2050
|
+
if (process.env.WRAPS_TELEMETRY_DISABLED === "1") {
|
|
2051
|
+
return "WRAPS_TELEMETRY_DISABLED environment variable is set";
|
|
2052
|
+
}
|
|
2053
|
+
if (isCI()) {
|
|
2054
|
+
return "CI environment detected";
|
|
2055
|
+
}
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Disable telemetry
|
|
2060
|
+
*/
|
|
2061
|
+
disable() {
|
|
2062
|
+
this.config.setEnabled(false);
|
|
2063
|
+
this.enabled = false;
|
|
2064
|
+
this.eventQueue = [];
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Check if telemetry is enabled
|
|
2068
|
+
*/
|
|
2069
|
+
isEnabled() {
|
|
2070
|
+
return this.enabled;
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Get config file path
|
|
2074
|
+
*/
|
|
2075
|
+
getConfigPath() {
|
|
2076
|
+
return this.config.getConfigPath();
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Show first-run notification
|
|
2080
|
+
*/
|
|
2081
|
+
shouldShowNotification() {
|
|
2082
|
+
return this.enabled && !this.config.hasShownNotification();
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Mark notification as shown
|
|
2086
|
+
*/
|
|
2087
|
+
markNotificationShown() {
|
|
2088
|
+
this.config.markNotificationShown();
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Show promotional footer once per CLI session.
|
|
2092
|
+
* Call this after successful completion of status/list commands.
|
|
2093
|
+
* Returns true if footer was shown, false if already shown this session.
|
|
2094
|
+
*/
|
|
2095
|
+
showFooterOnce() {
|
|
2096
|
+
if (this.hasShownFooter) {
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
this.hasShownFooter = true;
|
|
2100
|
+
console.log();
|
|
2101
|
+
console.log(pc2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2102
|
+
console.log("\u{1F4CA} Wraps Platform \u2014 analytics, templates, automations");
|
|
2103
|
+
console.log(` From $10/mo \u2192 ${pc2.cyan("https://wraps.dev/platform")}`);
|
|
2104
|
+
console.log();
|
|
2105
|
+
console.log(`\u{1F4AC} ${pc2.cyan("hey@wraps.sh")}`);
|
|
2106
|
+
console.log(pc2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Get CLI version from package.json
|
|
2111
|
+
*/
|
|
2112
|
+
getCLIVersion() {
|
|
2113
|
+
try {
|
|
2114
|
+
const __filename3 = fileURLToPath2(import.meta.url);
|
|
2115
|
+
const __dirname4 = dirname(__filename3);
|
|
2116
|
+
const pkg = JSON.parse(
|
|
2117
|
+
readFileSync2(join5(__dirname4, "../package.json"), "utf-8")
|
|
2118
|
+
);
|
|
2119
|
+
return pkg.version;
|
|
2120
|
+
} catch {
|
|
2121
|
+
return "unknown";
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
telemetryInstance = null;
|
|
1934
2126
|
}
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
// src/telemetry/events.ts
|
|
2130
|
+
function trackCommand(command, metadata) {
|
|
2131
|
+
const client = getTelemetryClient();
|
|
2132
|
+
const sanitized = metadata ? { ...metadata } : {};
|
|
2133
|
+
sanitized.domain = void 0;
|
|
2134
|
+
sanitized.accountId = void 0;
|
|
2135
|
+
sanitized.email = void 0;
|
|
2136
|
+
client.track(`command:${command}`, sanitized);
|
|
1935
2137
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
2138
|
+
function trackServiceInit(service, success, metadata) {
|
|
2139
|
+
const client = getTelemetryClient();
|
|
2140
|
+
client.track("service:init", {
|
|
2141
|
+
service,
|
|
2142
|
+
success,
|
|
2143
|
+
...metadata
|
|
2144
|
+
});
|
|
1938
2145
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
}
|
|
1946
|
-
return "us-east-1";
|
|
2146
|
+
function trackServiceDeployed(service, metadata) {
|
|
2147
|
+
const client = getTelemetryClient();
|
|
2148
|
+
client.track("service:deployed", {
|
|
2149
|
+
service,
|
|
2150
|
+
...metadata
|
|
2151
|
+
});
|
|
1947
2152
|
}
|
|
1948
|
-
|
|
1949
|
-
const
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
);
|
|
1956
|
-
const identities = identitiesResponse.Identities || [];
|
|
1957
|
-
if (identities.length === 0) {
|
|
1958
|
-
return [];
|
|
1959
|
-
}
|
|
1960
|
-
const attributesResponse = await ses.send(
|
|
1961
|
-
new GetIdentityVerificationAttributesCommand({
|
|
1962
|
-
Identities: identities
|
|
1963
|
-
})
|
|
1964
|
-
);
|
|
1965
|
-
const attributes = attributesResponse.VerificationAttributes || {};
|
|
1966
|
-
return identities.map((domain) => ({
|
|
1967
|
-
domain,
|
|
1968
|
-
verified: attributes[domain]?.VerificationStatus === "Success"
|
|
1969
|
-
}));
|
|
1970
|
-
} catch {
|
|
1971
|
-
return [];
|
|
1972
|
-
}
|
|
2153
|
+
function trackError(errorCode, command, metadata) {
|
|
2154
|
+
const client = getTelemetryClient();
|
|
2155
|
+
client.track("error:occurred", {
|
|
2156
|
+
error_code: errorCode,
|
|
2157
|
+
command,
|
|
2158
|
+
...metadata
|
|
2159
|
+
});
|
|
1973
2160
|
}
|
|
1974
|
-
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
const response = await sesv22.send(new GetAccountCommand({}));
|
|
1978
|
-
return {
|
|
1979
|
-
isSandbox: !response.ProductionAccessEnabled,
|
|
1980
|
-
sendQuota: response.SendQuota ? {
|
|
1981
|
-
max24HourSend: response.SendQuota.Max24HourSend ?? 0,
|
|
1982
|
-
maxSendRate: response.SendQuota.MaxSendRate ?? 0,
|
|
1983
|
-
sentLast24Hours: response.SendQuota.SentLast24Hours ?? 0
|
|
1984
|
-
} : void 0,
|
|
1985
|
-
enforcementStatus: response.EnforcementStatus
|
|
1986
|
-
};
|
|
1987
|
-
} catch {
|
|
1988
|
-
return { isSandbox: true, sandboxUncertain: true };
|
|
1989
|
-
}
|
|
2161
|
+
function trackFeature(feature, metadata) {
|
|
2162
|
+
const client = getTelemetryClient();
|
|
2163
|
+
client.track(`feature:${feature}`, metadata || {});
|
|
1990
2164
|
}
|
|
1991
|
-
|
|
1992
|
-
const
|
|
1993
|
-
|
|
2165
|
+
function trackServiceUpgrade(service, metadata) {
|
|
2166
|
+
const client = getTelemetryClient();
|
|
2167
|
+
client.track("service:upgraded", {
|
|
2168
|
+
service,
|
|
2169
|
+
...metadata
|
|
2170
|
+
});
|
|
1994
2171
|
}
|
|
1995
|
-
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
})
|
|
2002
|
-
);
|
|
2003
|
-
const certificate = response.Certificate;
|
|
2004
|
-
if (!certificate) {
|
|
2005
|
-
return null;
|
|
2006
|
-
}
|
|
2007
|
-
const validationRecords = certificate.DomainValidationOptions?.map((option) => ({
|
|
2008
|
-
name: option.ResourceRecord?.Name || "",
|
|
2009
|
-
type: option.ResourceRecord?.Type || "",
|
|
2010
|
-
value: option.ResourceRecord?.Value || ""
|
|
2011
|
-
})) || [];
|
|
2012
|
-
return {
|
|
2013
|
-
status: certificate.Status || "UNKNOWN",
|
|
2014
|
-
domainName: certificate.DomainName || "",
|
|
2015
|
-
validationRecords
|
|
2016
|
-
};
|
|
2017
|
-
} catch (error) {
|
|
2018
|
-
console.error("Error getting ACM certificate status:", error);
|
|
2019
|
-
return null;
|
|
2020
|
-
}
|
|
2172
|
+
function trackServiceRemoved(service, metadata) {
|
|
2173
|
+
const client = getTelemetryClient();
|
|
2174
|
+
client.track("service:removed", {
|
|
2175
|
+
service,
|
|
2176
|
+
...metadata
|
|
2177
|
+
});
|
|
2021
2178
|
}
|
|
2022
|
-
var
|
|
2023
|
-
|
|
2024
|
-
"src/utils/shared/aws.ts"() {
|
|
2179
|
+
var init_events = __esm({
|
|
2180
|
+
"src/telemetry/events.ts"() {
|
|
2025
2181
|
"use strict";
|
|
2026
2182
|
init_esm_shims();
|
|
2027
|
-
|
|
2028
|
-
init_errors();
|
|
2029
|
-
SES_REGIONS = [
|
|
2030
|
-
"us-east-1",
|
|
2031
|
-
"us-east-2",
|
|
2032
|
-
"us-west-1",
|
|
2033
|
-
"us-west-2",
|
|
2034
|
-
"af-south-1",
|
|
2035
|
-
"ap-east-1",
|
|
2036
|
-
"ap-south-1",
|
|
2037
|
-
"ap-northeast-1",
|
|
2038
|
-
"ap-northeast-2",
|
|
2039
|
-
"ap-northeast-3",
|
|
2040
|
-
"ap-southeast-1",
|
|
2041
|
-
"ap-southeast-2",
|
|
2042
|
-
"ap-southeast-3",
|
|
2043
|
-
"ca-central-1",
|
|
2044
|
-
"eu-central-1",
|
|
2045
|
-
"eu-west-1",
|
|
2046
|
-
"eu-west-2",
|
|
2047
|
-
"eu-west-3",
|
|
2048
|
-
"eu-south-1",
|
|
2049
|
-
"eu-north-1",
|
|
2050
|
-
"me-south-1",
|
|
2051
|
-
"sa-east-1"
|
|
2052
|
-
];
|
|
2183
|
+
init_client();
|
|
2053
2184
|
}
|
|
2054
2185
|
});
|
|
2055
2186
|
|
|
@@ -9896,14 +10027,14 @@ init_esm_shims();
|
|
|
9896
10027
|
init_events();
|
|
9897
10028
|
init_config();
|
|
9898
10029
|
init_json_output();
|
|
9899
|
-
import * as
|
|
10030
|
+
import * as clack2 from "@clack/prompts";
|
|
9900
10031
|
import { createAuthClient } from "better-auth/client";
|
|
9901
10032
|
import {
|
|
9902
10033
|
deviceAuthorizationClient,
|
|
9903
10034
|
organizationClient
|
|
9904
10035
|
} from "better-auth/client/plugins";
|
|
9905
10036
|
import open from "open";
|
|
9906
|
-
import
|
|
10037
|
+
import pc3 from "picocolors";
|
|
9907
10038
|
function createCliAuthClient(baseURL) {
|
|
9908
10039
|
return createAuthClient({
|
|
9909
10040
|
baseURL,
|
|
@@ -9947,14 +10078,14 @@ async function login(options) {
|
|
|
9947
10078
|
if (isJsonMode()) {
|
|
9948
10079
|
jsonSuccess("auth.login", { tokenType: "api-key" });
|
|
9949
10080
|
} else {
|
|
9950
|
-
|
|
10081
|
+
clack2.log.success("API key saved.");
|
|
9951
10082
|
}
|
|
9952
10083
|
return;
|
|
9953
10084
|
}
|
|
9954
|
-
|
|
10085
|
+
clack2.intro(pc3.bold("Wraps \u203A Sign In"));
|
|
9955
10086
|
const baseURL = getAppBaseUrl();
|
|
9956
10087
|
const authClient = createCliAuthClient(baseURL);
|
|
9957
|
-
const spinner10 =
|
|
10088
|
+
const spinner10 = clack2.spinner();
|
|
9958
10089
|
const { data: codeData, error: codeError } = await authClient.device.code({
|
|
9959
10090
|
client_id: "wraps-cli"
|
|
9960
10091
|
});
|
|
@@ -9965,7 +10096,7 @@ async function login(options) {
|
|
|
9965
10096
|
method: "device"
|
|
9966
10097
|
});
|
|
9967
10098
|
trackError("DEVICE_AUTH_FAILED", "auth:login", { step: "request_code" });
|
|
9968
|
-
|
|
10099
|
+
clack2.log.error("Failed to start device authorization.");
|
|
9969
10100
|
throw new Error("Failed to start device authorization.");
|
|
9970
10101
|
}
|
|
9971
10102
|
const {
|
|
@@ -9976,11 +10107,11 @@ async function login(options) {
|
|
|
9976
10107
|
expires_in
|
|
9977
10108
|
} = codeData;
|
|
9978
10109
|
const formatted = `${user_code.slice(0, 4)}-${user_code.slice(4)}`;
|
|
9979
|
-
|
|
9980
|
-
|
|
10110
|
+
clack2.log.info(`Your code: ${pc3.bold(pc3.cyan(formatted))}`);
|
|
10111
|
+
clack2.log.info(`Visit: ${pc3.underline(`${baseURL}/device`)}`);
|
|
9981
10112
|
try {
|
|
9982
10113
|
await open(`${baseURL}/device?user_code=${user_code}`);
|
|
9983
|
-
|
|
10114
|
+
clack2.log.info("Opening browser...");
|
|
9984
10115
|
} catch {
|
|
9985
10116
|
}
|
|
9986
10117
|
spinner10.start("Waiting for approval...");
|
|
@@ -10012,14 +10143,14 @@ async function login(options) {
|
|
|
10012
10143
|
duration_ms: Date.now() - startTime,
|
|
10013
10144
|
method: "device"
|
|
10014
10145
|
});
|
|
10015
|
-
|
|
10146
|
+
clack2.log.success("Signed in successfully.");
|
|
10016
10147
|
if (organizations.length === 1) {
|
|
10017
|
-
|
|
10148
|
+
clack2.log.info(`Organization: ${pc3.cyan(organizations[0].name)}`);
|
|
10018
10149
|
} else if (organizations.length > 1) {
|
|
10019
|
-
|
|
10150
|
+
clack2.log.info(`${organizations.length} organizations available`);
|
|
10020
10151
|
} else {
|
|
10021
|
-
|
|
10022
|
-
`No organizations found. Create one at ${
|
|
10152
|
+
clack2.log.info(
|
|
10153
|
+
`No organizations found. Create one at ${pc3.underline(`${baseURL}/onboarding`)} and run ${pc3.cyan("wraps auth login")} again.`
|
|
10023
10154
|
);
|
|
10024
10155
|
}
|
|
10025
10156
|
if (isJsonMode()) {
|
|
@@ -10048,7 +10179,7 @@ async function login(options) {
|
|
|
10048
10179
|
});
|
|
10049
10180
|
trackError("ACCESS_DENIED", "auth:login", { step: "poll_token" });
|
|
10050
10181
|
spinner10.stop("Denied.");
|
|
10051
|
-
|
|
10182
|
+
clack2.log.error("Authorization was denied.");
|
|
10052
10183
|
throw new Error("Authorization was denied.");
|
|
10053
10184
|
}
|
|
10054
10185
|
if (errorCode === "expired_token") {
|
|
@@ -10063,7 +10194,7 @@ async function login(options) {
|
|
|
10063
10194
|
});
|
|
10064
10195
|
trackError("DEVICE_CODE_EXPIRED", "auth:login", { step: "poll_token" });
|
|
10065
10196
|
spinner10.stop("Expired.");
|
|
10066
|
-
|
|
10197
|
+
clack2.log.error("Device code expired. Run `wraps auth login` to try again.");
|
|
10067
10198
|
throw new Error("Device code expired.");
|
|
10068
10199
|
}
|
|
10069
10200
|
|
|
@@ -10072,11 +10203,11 @@ init_esm_shims();
|
|
|
10072
10203
|
init_events();
|
|
10073
10204
|
init_config();
|
|
10074
10205
|
init_json_output();
|
|
10075
|
-
import * as
|
|
10076
|
-
import
|
|
10206
|
+
import * as clack3 from "@clack/prompts";
|
|
10207
|
+
import pc4 from "picocolors";
|
|
10077
10208
|
async function logout() {
|
|
10078
10209
|
if (!isJsonMode()) {
|
|
10079
|
-
|
|
10210
|
+
clack3.intro(pc4.bold("Wraps \u203A Sign Out"));
|
|
10080
10211
|
}
|
|
10081
10212
|
const config2 = await readAuthConfig();
|
|
10082
10213
|
if (!config2?.auth?.token) {
|
|
@@ -10085,7 +10216,7 @@ async function logout() {
|
|
|
10085
10216
|
jsonSuccess("auth.logout", { loggedOut: false, alreadyLoggedOut: true });
|
|
10086
10217
|
return;
|
|
10087
10218
|
}
|
|
10088
|
-
|
|
10219
|
+
clack3.log.info("Not signed in.");
|
|
10089
10220
|
return;
|
|
10090
10221
|
}
|
|
10091
10222
|
await clearAuthConfig();
|
|
@@ -10094,7 +10225,7 @@ async function logout() {
|
|
|
10094
10225
|
jsonSuccess("auth.logout", { loggedOut: true });
|
|
10095
10226
|
return;
|
|
10096
10227
|
}
|
|
10097
|
-
|
|
10228
|
+
clack3.log.success("Signed out. Token removed from ~/.wraps/config.json");
|
|
10098
10229
|
}
|
|
10099
10230
|
|
|
10100
10231
|
// src/commands/auth/status.ts
|
|
@@ -10102,8 +10233,8 @@ init_esm_shims();
|
|
|
10102
10233
|
init_events();
|
|
10103
10234
|
init_config();
|
|
10104
10235
|
init_json_output();
|
|
10105
|
-
import * as
|
|
10106
|
-
import
|
|
10236
|
+
import * as clack4 from "@clack/prompts";
|
|
10237
|
+
import pc5 from "picocolors";
|
|
10107
10238
|
async function authStatus(_options = {}) {
|
|
10108
10239
|
const config2 = await readAuthConfig();
|
|
10109
10240
|
if (!config2?.auth?.token) {
|
|
@@ -10111,8 +10242,8 @@ async function authStatus(_options = {}) {
|
|
|
10111
10242
|
if (isJsonMode()) {
|
|
10112
10243
|
jsonSuccess("auth.status", { authenticated: false });
|
|
10113
10244
|
} else {
|
|
10114
|
-
|
|
10115
|
-
|
|
10245
|
+
clack4.intro(pc5.bold("Wraps \u203A Auth Status"));
|
|
10246
|
+
clack4.log.info("Not signed in. Run `wraps auth login` to authenticate.");
|
|
10116
10247
|
}
|
|
10117
10248
|
return;
|
|
10118
10249
|
}
|
|
@@ -10126,10 +10257,10 @@ async function authStatus(_options = {}) {
|
|
|
10126
10257
|
expiresAt: expiresAt || null
|
|
10127
10258
|
});
|
|
10128
10259
|
} else {
|
|
10129
|
-
|
|
10130
|
-
|
|
10260
|
+
clack4.intro(pc5.bold("Wraps \u203A Auth Status"));
|
|
10261
|
+
clack4.log.info(`Token: ${masked} (${tokenType})`);
|
|
10131
10262
|
if (expiresAt) {
|
|
10132
|
-
|
|
10263
|
+
clack4.log.info(`Expires: ${new Date(expiresAt).toLocaleDateString()}`);
|
|
10133
10264
|
}
|
|
10134
10265
|
}
|
|
10135
10266
|
trackCommand("auth:status", { success: true, authenticated: true });
|
|
@@ -17766,10 +17897,18 @@ async function createSMTPCredentials(config2) {
|
|
|
17766
17897
|
|
|
17767
17898
|
// src/infrastructure/resources/sqs.ts
|
|
17768
17899
|
init_esm_shims();
|
|
17900
|
+
init_resource_checks();
|
|
17769
17901
|
import * as aws11 from "@pulumi/aws";
|
|
17902
|
+
var SQS_TIMEOUTS = {
|
|
17903
|
+
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
17904
|
+
};
|
|
17770
17905
|
async function createSQSResources() {
|
|
17771
|
-
const
|
|
17772
|
-
|
|
17906
|
+
const dlqName = "wraps-email-events-dlq";
|
|
17907
|
+
const queueName = "wraps-email-events";
|
|
17908
|
+
const dlqUrl = await sqsQueueExists(dlqName);
|
|
17909
|
+
const queueUrl = await sqsQueueExists(queueName);
|
|
17910
|
+
const dlqConfig = {
|
|
17911
|
+
name: dlqName,
|
|
17773
17912
|
messageRetentionSeconds: 1209600,
|
|
17774
17913
|
// 14 days
|
|
17775
17914
|
tags: {
|
|
@@ -17777,9 +17916,13 @@ async function createSQSResources() {
|
|
|
17777
17916
|
Service: "email",
|
|
17778
17917
|
Description: "Dead letter queue for failed SES event processing"
|
|
17779
17918
|
}
|
|
17919
|
+
};
|
|
17920
|
+
const dlq = new aws11.sqs.Queue(dlqName, dlqConfig, {
|
|
17921
|
+
...SQS_TIMEOUTS,
|
|
17922
|
+
...dlqUrl ? { import: dlqUrl } : {}
|
|
17780
17923
|
});
|
|
17781
|
-
const
|
|
17782
|
-
name:
|
|
17924
|
+
const queueConfig = {
|
|
17925
|
+
name: queueName,
|
|
17783
17926
|
visibilityTimeoutSeconds: 300,
|
|
17784
17927
|
// Must be >= Lambda timeout (5 minutes)
|
|
17785
17928
|
messageRetentionSeconds: 345600,
|
|
@@ -17798,6 +17941,10 @@ async function createSQSResources() {
|
|
|
17798
17941
|
Service: "email",
|
|
17799
17942
|
Description: "Queue for SES email events from EventBridge"
|
|
17800
17943
|
}
|
|
17944
|
+
};
|
|
17945
|
+
const queue = new aws11.sqs.Queue(queueName, queueConfig, {
|
|
17946
|
+
...SQS_TIMEOUTS,
|
|
17947
|
+
...queueUrl ? { import: queueUrl } : {}
|
|
17801
17948
|
});
|
|
17802
17949
|
return {
|
|
17803
17950
|
queue,
|
|
@@ -18551,6 +18698,8 @@ async function connect2(options) {
|
|
|
18551
18698
|
throw new Error(`Preview failed: ${msg}`);
|
|
18552
18699
|
}
|
|
18553
18700
|
}
|
|
18701
|
+
let pulumiLog = [];
|
|
18702
|
+
const debugPulumi = process.env.WRAPS_DEBUG === "1";
|
|
18554
18703
|
let outputs;
|
|
18555
18704
|
try {
|
|
18556
18705
|
outputs = await progress.execute(
|
|
@@ -18588,8 +18737,14 @@ async function connect2(options) {
|
|
|
18588
18737
|
`wraps-${identity.accountId}-${region}`
|
|
18589
18738
|
);
|
|
18590
18739
|
await stack.setConfig("aws:region", { value: region });
|
|
18591
|
-
const upResult = await stack.up({
|
|
18592
|
-
|
|
18740
|
+
const upResult = await stack.up({
|
|
18741
|
+
onOutput: (msg) => {
|
|
18742
|
+
pulumiLog.push(msg);
|
|
18743
|
+
if (debugPulumi) {
|
|
18744
|
+
process.stderr.write(msg);
|
|
18745
|
+
}
|
|
18746
|
+
}
|
|
18747
|
+
});
|
|
18593
18748
|
const pulumiOutputs = upResult.outputs;
|
|
18594
18749
|
return {
|
|
18595
18750
|
roleArn: pulumiOutputs.roleArn?.value,
|
|
@@ -18614,9 +18769,22 @@ async function connect2(options) {
|
|
|
18614
18769
|
trackError("STACK_LOCKED", "email:connect", { step: "deploy" });
|
|
18615
18770
|
throw errors.stackLocked();
|
|
18616
18771
|
}
|
|
18772
|
+
if (pulumiLog.length > 0) {
|
|
18773
|
+
const tail = pulumiLog.join("").split("\n").slice(-60).join("\n");
|
|
18774
|
+
const redactedTail = redactSensitiveValues(tail);
|
|
18775
|
+
process.stderr.write(
|
|
18776
|
+
`
|
|
18777
|
+
${pc19.dim("\u2500\u2500\u2500 Pulumi output (last 60 lines, redacted) \u2500\u2500\u2500")}
|
|
18778
|
+
${redactedTail}
|
|
18779
|
+
${pc19.dim("\u2500\u2500\u2500 end Pulumi output \u2500\u2500\u2500")}
|
|
18780
|
+
|
|
18781
|
+
`
|
|
18782
|
+
);
|
|
18783
|
+
}
|
|
18617
18784
|
trackError("DEPLOYMENT_FAILED", "email:connect", { step: "deploy" });
|
|
18618
18785
|
throw new Error(`Pulumi deployment failed: ${msg}`);
|
|
18619
18786
|
}
|
|
18787
|
+
pulumiLog = [];
|
|
18620
18788
|
if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
|
|
18621
18789
|
const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
|
|
18622
18790
|
const hostedZone = await findHostedZone2(outputs.domain, region);
|
|
@@ -21089,6 +21257,77 @@ Enable it: ${pc24.cyan("wraps email inbound init")}
|
|
|
21089
21257
|
}
|
|
21090
21258
|
console.log();
|
|
21091
21259
|
}
|
|
21260
|
+
var NOT_VERIFIED_PATTERN = /not verified/i;
|
|
21261
|
+
function mapInboundTestSendError(error, ctx) {
|
|
21262
|
+
if (error instanceof WrapsError) {
|
|
21263
|
+
return error;
|
|
21264
|
+
}
|
|
21265
|
+
if (!(error instanceof Error)) {
|
|
21266
|
+
return new WrapsError(
|
|
21267
|
+
`Failed to send inbound test email to ${ctx.recipient}`,
|
|
21268
|
+
"INBOUND_TEST_SEND_FAILED",
|
|
21269
|
+
"An unexpected error occurred while sending the test email.\n\nCheck infrastructure status:\n wraps email status\n wraps email doctor",
|
|
21270
|
+
"https://wraps.dev/docs/guides/email/troubleshooting"
|
|
21271
|
+
);
|
|
21272
|
+
}
|
|
21273
|
+
const name = error.name;
|
|
21274
|
+
const message = error.message || "";
|
|
21275
|
+
if (name === "MailFromDomainNotVerifiedException") {
|
|
21276
|
+
return new WrapsError(
|
|
21277
|
+
`Custom MAIL FROM domain is not verified for ${ctx.domain}`,
|
|
21278
|
+
"INBOUND_TEST_MAIL_FROM_NOT_VERIFIED",
|
|
21279
|
+
`The MAIL FROM domain configured for "${ctx.domain}" is not fully verified.
|
|
21280
|
+
|
|
21281
|
+
Verify DNS records:
|
|
21282
|
+
wraps email verify
|
|
21283
|
+
|
|
21284
|
+
Or remove the custom MAIL FROM domain in the SES console and retry.`,
|
|
21285
|
+
"https://docs.aws.amazon.com/ses/latest/dg/mail-from.html"
|
|
21286
|
+
);
|
|
21287
|
+
}
|
|
21288
|
+
if (name === "MessageRejected" || name === "InvalidParameterValue" || NOT_VERIFIED_PATTERN.test(message)) {
|
|
21289
|
+
return new WrapsError(
|
|
21290
|
+
`SES rejected the inbound test send: ${message || name}`,
|
|
21291
|
+
"INBOUND_TEST_MESSAGE_REJECTED",
|
|
21292
|
+
[
|
|
21293
|
+
`Tried to send: ${ctx.source} \u2192 ${ctx.recipient} (region ${ctx.region})`,
|
|
21294
|
+
"",
|
|
21295
|
+
"Most likely causes:",
|
|
21296
|
+
` \u2022 Your SES account is in the sandbox and ${ctx.recipient} is not a verified address`,
|
|
21297
|
+
` \u2022 The sender domain "${ctx.domain}" is not verified for sending in ${ctx.region}`,
|
|
21298
|
+
` \u2022 The receiving domain "${ctx.receivingDomain}" is verified for receiving (MX) but not for sending`,
|
|
21299
|
+
"",
|
|
21300
|
+
"Check status:",
|
|
21301
|
+
" wraps email status",
|
|
21302
|
+
" wraps email doctor",
|
|
21303
|
+
"",
|
|
21304
|
+
"Exit the SES sandbox to send to any address:",
|
|
21305
|
+
" https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html"
|
|
21306
|
+
].join("\n"),
|
|
21307
|
+
"https://wraps.dev/docs/guides/email/troubleshooting"
|
|
21308
|
+
);
|
|
21309
|
+
}
|
|
21310
|
+
if (name === "AccountSendingPausedException" || name === "ConfigurationSetSendingPausedException") {
|
|
21311
|
+
return new WrapsError(
|
|
21312
|
+
"SES sending is paused for this account",
|
|
21313
|
+
"INBOUND_TEST_SENDING_PAUSED",
|
|
21314
|
+
"Your SES account or configuration set has sending paused, usually due to a high bounce or complaint rate.\n\nCheck the SES Reputation Dashboard in the AWS console and resume sending once the issue is resolved.",
|
|
21315
|
+
"https://docs.aws.amazon.com/ses/latest/dg/reputationdashboard.html"
|
|
21316
|
+
);
|
|
21317
|
+
}
|
|
21318
|
+
if (name === "AccessDeniedException" || name === "AccessDenied") {
|
|
21319
|
+
return new WrapsError(
|
|
21320
|
+
`IAM permission denied: ses:SendEmail in ${ctx.region}`,
|
|
21321
|
+
"INBOUND_TEST_PERMISSION_DENIED",
|
|
21322
|
+
`Your AWS credentials lack the "ses:SendEmail" permission in region ${ctx.region}.
|
|
21323
|
+
|
|
21324
|
+
View required SES permissions:
|
|
21325
|
+
wraps permissions --service email --json`,
|
|
21326
|
+
"https://wraps.dev/docs/guides/aws-setup/permissions"
|
|
21327
|
+
);
|
|
21328
|
+
}
|
|
21329
|
+
return error;
|
|
21330
|
+
}
|
|
21092
21331
|
async function inboundTest(options) {
|
|
21093
21332
|
if (!isJsonMode()) {
|
|
21094
21333
|
clack22.intro(pc24.bold("Inbound Email Test"));
|
|
@@ -21112,34 +21351,45 @@ Enable it: ${pc24.cyan("wraps email inbound init")}
|
|
|
21112
21351
|
const receivingDomain = inbound.receivingDomain || (inbound.subdomain ? `${inbound.subdomain}.${emailConfig.domain}` : emailConfig.domain || "");
|
|
21113
21352
|
const bucketName = inbound.bucketName || `wraps-inbound-${identity.accountId}-${region}`;
|
|
21114
21353
|
const testRecipient = `test@${receivingDomain}`;
|
|
21354
|
+
const testSource = `test@${emailConfig.domain}`;
|
|
21115
21355
|
const testSubject = `Wraps Inbound Test - ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
21116
21356
|
await progress.execute(`Sending test email to ${testRecipient}`, async () => {
|
|
21117
21357
|
const { SESClient: SESClient7, SendEmailCommand: SendEmailCommand2 } = await import("@aws-sdk/client-ses");
|
|
21118
21358
|
const ses = new SESClient7({ region });
|
|
21119
|
-
|
|
21120
|
-
|
|
21121
|
-
|
|
21122
|
-
|
|
21123
|
-
|
|
21124
|
-
|
|
21125
|
-
|
|
21126
|
-
|
|
21127
|
-
|
|
21128
|
-
|
|
21129
|
-
|
|
21130
|
-
|
|
21131
|
-
|
|
21132
|
-
|
|
21359
|
+
try {
|
|
21360
|
+
await ses.send(
|
|
21361
|
+
new SendEmailCommand2({
|
|
21362
|
+
Source: testSource,
|
|
21363
|
+
Destination: {
|
|
21364
|
+
ToAddresses: [testRecipient]
|
|
21365
|
+
},
|
|
21366
|
+
Message: {
|
|
21367
|
+
Subject: { Data: testSubject },
|
|
21368
|
+
Body: {
|
|
21369
|
+
Text: {
|
|
21370
|
+
Data: "This is a test email from Wraps CLI to verify inbound email processing."
|
|
21371
|
+
},
|
|
21372
|
+
Html: {
|
|
21373
|
+
Data: "<h1>Wraps Inbound Test</h1><p>This email was sent to verify inbound email processing is working correctly.</p>"
|
|
21374
|
+
}
|
|
21133
21375
|
}
|
|
21134
21376
|
}
|
|
21135
|
-
}
|
|
21136
|
-
|
|
21137
|
-
)
|
|
21377
|
+
})
|
|
21378
|
+
);
|
|
21379
|
+
} catch (error) {
|
|
21380
|
+
throw mapInboundTestSendError(error, {
|
|
21381
|
+
source: testSource,
|
|
21382
|
+
recipient: testRecipient,
|
|
21383
|
+
domain: emailConfig.domain || "",
|
|
21384
|
+
receivingDomain,
|
|
21385
|
+
region
|
|
21386
|
+
});
|
|
21387
|
+
}
|
|
21138
21388
|
});
|
|
21139
|
-
const spinner10 = clack22.spinner();
|
|
21140
|
-
spinner10.start("Waiting for email to be processed...");
|
|
21141
21389
|
const { S3Client: S3Client2, ListObjectsV2Command: ListObjectsV2Command2, GetObjectCommand: GetObjectCommand2 } = await import("@aws-sdk/client-s3");
|
|
21142
21390
|
const s34 = new S3Client2({ region });
|
|
21391
|
+
const spinner10 = clack22.spinner();
|
|
21392
|
+
spinner10.start("Waiting for email to be processed...");
|
|
21143
21393
|
let found = false;
|
|
21144
21394
|
const startTime = Date.now();
|
|
21145
21395
|
const timeout = 3e4;
|