@wraps.dev/cli 2.18.7 → 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
CHANGED
|
@@ -66,1564 +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
|
-
};
|
|
677
|
+
console.log();
|
|
1157
678
|
}
|
|
1158
|
-
|
|
679
|
+
if (wrapsError.docsUrl) {
|
|
680
|
+
console.log(`${pc.dim("Documentation:")}`);
|
|
681
|
+
console.log(` ${pc.blue(wrapsError.docsUrl)}
|
|
682
|
+
`);
|
|
683
|
+
}
|
|
684
|
+
process.exit(1);
|
|
1159
685
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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);
|
|
695
|
+
}
|
|
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);
|
|
1167
701
|
}
|
|
1168
|
-
function
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
+
);
|
|
752
|
+
}
|
|
1171
753
|
}
|
|
1172
|
-
function
|
|
1173
|
-
|
|
1174
|
-
|
|
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"
|
|
782
|
+
);
|
|
1175
783
|
}
|
|
1176
|
-
return "aws sso login";
|
|
1177
784
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
|
1211
|
-
}
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
function hasCredentialsFile() {
|
|
1215
|
-
const credentialsPath = join5(homedir2(), ".aws", "credentials");
|
|
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]);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
const configPath = join5(homedir2(), ".aws", "config");
|
|
1233
|
-
if (existsSync4(configPath)) {
|
|
1234
|
-
const content = readFileSync2(configPath, "utf-8");
|
|
1235
|
-
const matches = content.matchAll(/\[profile ([^\]]+)\]/g);
|
|
1236
|
-
for (const match of matches) {
|
|
1237
|
-
if (!profiles.includes(match[1])) {
|
|
1238
|
-
profiles.push(match[1]);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
return profiles;
|
|
1243
|
-
}
|
|
1244
|
-
var init_aws_detection = __esm({
|
|
1245
|
-
"src/utils/shared/aws-detection.ts"() {
|
|
785
|
+
var WrapsError, errors;
|
|
786
|
+
var init_errors = __esm({
|
|
787
|
+
"src/utils/shared/errors.ts"() {
|
|
1246
788
|
"use strict";
|
|
1247
789
|
init_esm_shims();
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
errors: () => errors,
|
|
1258
|
-
handleCLIError: () => handleCLIError,
|
|
1259
|
-
isAWSError: () => isAWSError,
|
|
1260
|
-
isAWSNotFoundError: () => isAWSNotFoundError,
|
|
1261
|
-
isPulumiError: () => isPulumiError,
|
|
1262
|
-
parseAWSError: () => parseAWSError,
|
|
1263
|
-
parsePulumiError: () => parsePulumiError,
|
|
1264
|
-
sanitizeErrorMessage: () => sanitizeErrorMessage
|
|
1265
|
-
});
|
|
1266
|
-
import * as clack4 from "@clack/prompts";
|
|
1267
|
-
import pc5 from "picocolors";
|
|
1268
|
-
function isAWSError(error) {
|
|
1269
|
-
if (!(error instanceof Error)) {
|
|
1270
|
-
return false;
|
|
1271
|
-
}
|
|
1272
|
-
const awsErrorNames = [
|
|
1273
|
-
"ExpiredTokenException",
|
|
1274
|
-
"InvalidClientTokenId",
|
|
1275
|
-
"AccessDenied",
|
|
1276
|
-
"AccessDeniedException",
|
|
1277
|
-
"UnauthorizedAccess",
|
|
1278
|
-
"InvalidAccessKeyId",
|
|
1279
|
-
"SignatureDoesNotMatch",
|
|
1280
|
-
"UnrecognizedClientException",
|
|
1281
|
-
"CredentialsError",
|
|
1282
|
-
"TokenRefreshRequired",
|
|
1283
|
-
"SSOTokenExpired"
|
|
1284
|
-
];
|
|
1285
|
-
return awsErrorNames.includes(error.name) || "$metadata" in error;
|
|
1286
|
-
}
|
|
1287
|
-
function classifyDNSError(error) {
|
|
1288
|
-
if (!(error instanceof Error)) {
|
|
1289
|
-
return "unknown";
|
|
1290
|
-
}
|
|
1291
|
-
const code = error.code;
|
|
1292
|
-
if (code === "ENOTFOUND" || code === "ENODATA") {
|
|
1293
|
-
return "missing";
|
|
1294
|
-
}
|
|
1295
|
-
if (code === "ETIMEOUT" || code === "ESERVFAIL" || code === "ECONNREFUSED") {
|
|
1296
|
-
return "network";
|
|
1297
|
-
}
|
|
1298
|
-
return "unknown";
|
|
1299
|
-
}
|
|
1300
|
-
function isAWSNotFoundError(error) {
|
|
1301
|
-
if (!(error instanceof Error)) return false;
|
|
1302
|
-
const awsError = error;
|
|
1303
|
-
return error.name === "NotFoundException" || error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.name === "ResourceNotFoundException" || awsError.$metadata?.httpStatusCode === 404;
|
|
1304
|
-
}
|
|
1305
|
-
function isPulumiError(error) {
|
|
1306
|
-
if (!(error instanceof Error)) {
|
|
1307
|
-
return false;
|
|
1308
|
-
}
|
|
1309
|
-
return error.message?.includes("pulumi") || error.message?.includes("Pulumi") || error.message?.includes("resource") || error.message?.includes("creating") || error.message?.includes("AccessDenied");
|
|
1310
|
-
}
|
|
1311
|
-
function parseAWSError(error) {
|
|
1312
|
-
const errorName = error.name || "UnknownError";
|
|
1313
|
-
const actionMatch = error.message?.match(/when calling the (\w+) operation/i);
|
|
1314
|
-
const action = actionMatch?.[1];
|
|
1315
|
-
const resourceMatch = error.message?.match(/resource[:\s]+([^\s,]+)/i);
|
|
1316
|
-
const resource = resourceMatch?.[1];
|
|
1317
|
-
return { code: errorName, action, resource };
|
|
1318
|
-
}
|
|
1319
|
-
function parsePulumiError(error) {
|
|
1320
|
-
const message = error.message || "";
|
|
1321
|
-
if (message.includes("AccessDenied") || message.includes("access denied")) {
|
|
1322
|
-
const actionMatch = message.match(
|
|
1323
|
-
/(?:action|operation)[:\s]+["']?(\w+:\w+)["']?/i
|
|
1324
|
-
);
|
|
1325
|
-
if (actionMatch) {
|
|
1326
|
-
const [service] = actionMatch[1].split(":");
|
|
1327
|
-
return {
|
|
1328
|
-
code: "IAM_PERMISSION_DENIED",
|
|
1329
|
-
iamAction: actionMatch[1],
|
|
1330
|
-
service
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
if (message.includes("ses:") || message.includes("SES")) {
|
|
1334
|
-
return { code: "SES_PERMISSION_DENIED", service: "ses" };
|
|
1335
|
-
}
|
|
1336
|
-
if (message.includes("dynamodb:") || message.includes("DynamoDB")) {
|
|
1337
|
-
return { code: "DYNAMODB_PERMISSION_DENIED", service: "dynamodb" };
|
|
1338
|
-
}
|
|
1339
|
-
if (message.includes("lambda:") || message.includes("Lambda")) {
|
|
1340
|
-
return { code: "LAMBDA_PERMISSION_DENIED", service: "lambda" };
|
|
1341
|
-
}
|
|
1342
|
-
if (message.includes("events:") || message.includes("EventBridge")) {
|
|
1343
|
-
return { code: "EVENTBRIDGE_PERMISSION_DENIED", service: "events" };
|
|
1344
|
-
}
|
|
1345
|
-
if (message.includes("sqs:") || message.includes("SQS")) {
|
|
1346
|
-
return { code: "SQS_PERMISSION_DENIED", service: "sqs" };
|
|
1347
|
-
}
|
|
1348
|
-
if (message.includes("iam:") || message.includes("IAM")) {
|
|
1349
|
-
return { code: "IAM_PERMISSION_DENIED", service: "iam" };
|
|
1350
|
-
}
|
|
1351
|
-
return { code: "IAM_PERMISSION_DENIED" };
|
|
1352
|
-
}
|
|
1353
|
-
if (message.includes("AlreadyExists") || message.includes("already exists") || message.includes("already exist") || message.includes("ResourceConflictException") || message.includes("ResourceInUse") || message.includes("EntityAlreadyExists")) {
|
|
1354
|
-
const nameMatch = message.match(/error creating '([^']+)'/);
|
|
1355
|
-
const typeMatch = message.match(/\((aws:[^)]+)\)/);
|
|
1356
|
-
return {
|
|
1357
|
-
code: "RESOURCE_CONFLICT",
|
|
1358
|
-
resourceName: nameMatch?.[1],
|
|
1359
|
-
resourceType: typeMatch?.[1]
|
|
1360
|
-
};
|
|
1361
|
-
}
|
|
1362
|
-
if (message.includes("stack is currently locked")) {
|
|
1363
|
-
return { code: "STACK_LOCKED" };
|
|
1364
|
-
}
|
|
1365
|
-
return { code: "PULUMI_ERROR" };
|
|
1366
|
-
}
|
|
1367
|
-
function sanitizeErrorMessage(error) {
|
|
1368
|
-
if (!error) {
|
|
1369
|
-
return "Unknown error";
|
|
1370
|
-
}
|
|
1371
|
-
let message = error instanceof Error ? error.message : String(error);
|
|
1372
|
-
message = message.replace(/\b\d{12}\b/g, "[ACCOUNT_ID]");
|
|
1373
|
-
message = message.replace(
|
|
1374
|
-
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
1375
|
-
"[EMAIL]"
|
|
1376
|
-
);
|
|
1377
|
-
message = message.replace(
|
|
1378
|
-
/(?<!\.amazonaws\.com|\.aws\.amazon\.com)\b[a-zA-Z0-9][a-zA-Z0-9-]+\.[a-zA-Z]{2,}\b/g,
|
|
1379
|
-
(match) => {
|
|
1380
|
-
if (match.includes("amazonaws") || match.includes("aws.amazon")) {
|
|
1381
|
-
return match;
|
|
1382
|
-
}
|
|
1383
|
-
return "[DOMAIN]";
|
|
1384
|
-
}
|
|
1385
|
-
);
|
|
1386
|
-
message = message.replace(
|
|
1387
|
-
/arn:aws:[^:]+:[^:]*:\d{12}:/g,
|
|
1388
|
-
"arn:aws:[SERVICE]:[REGION]:[ACCOUNT_ID]:"
|
|
1389
|
-
);
|
|
1390
|
-
if (message.length > 500) {
|
|
1391
|
-
message = `${message.slice(0, 500)}...`;
|
|
1392
|
-
}
|
|
1393
|
-
return message;
|
|
1394
|
-
}
|
|
1395
|
-
function handleCLIError(error, command) {
|
|
1396
|
-
const cmdContext = command || "unknown";
|
|
1397
|
-
if (isJsonMode()) {
|
|
1398
|
-
let code = "UNKNOWN_ERROR";
|
|
1399
|
-
let message = "An unexpected error occurred";
|
|
1400
|
-
let suggestion;
|
|
1401
|
-
let docsUrl;
|
|
1402
|
-
if (error instanceof WrapsError) {
|
|
1403
|
-
trackError(error.code, cmdContext);
|
|
1404
|
-
code = error.code;
|
|
1405
|
-
message = error.message;
|
|
1406
|
-
suggestion = error.suggestion;
|
|
1407
|
-
docsUrl = error.docsUrl;
|
|
1408
|
-
} else if (isAWSError(error)) {
|
|
1409
|
-
const parsed = parseAWSError(error);
|
|
1410
|
-
code = `AWS_${parsed.code}`;
|
|
1411
|
-
trackError(code, cmdContext, { action: parsed.action });
|
|
1412
|
-
const wrapsErr = awsErrorToWrapsError(parsed.code, parsed.action, error);
|
|
1413
|
-
message = wrapsErr.message;
|
|
1414
|
-
suggestion = wrapsErr.suggestion;
|
|
1415
|
-
docsUrl = wrapsErr.docsUrl;
|
|
1416
|
-
} else if (isPulumiError(error)) {
|
|
1417
|
-
const parsed = parsePulumiError(error);
|
|
1418
|
-
code = `PULUMI_${parsed.code}`;
|
|
1419
|
-
trackError(code, cmdContext, {
|
|
1420
|
-
iamAction: parsed.iamAction,
|
|
1421
|
-
service: parsed.service,
|
|
1422
|
-
errorType: error?.constructor?.name
|
|
1423
|
-
});
|
|
1424
|
-
const wrapsErr = pulumiErrorToWrapsError(
|
|
1425
|
-
parsed.code,
|
|
1426
|
-
parsed.iamAction,
|
|
1427
|
-
parsed.service,
|
|
1428
|
-
parsed.resourceName,
|
|
1429
|
-
parsed.resourceType
|
|
1430
|
-
);
|
|
1431
|
-
message = wrapsErr.message;
|
|
1432
|
-
suggestion = wrapsErr.suggestion;
|
|
1433
|
-
docsUrl = wrapsErr.docsUrl;
|
|
1434
|
-
} else {
|
|
1435
|
-
trackError("UNHANDLED_ERROR", cmdContext, {
|
|
1436
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1437
|
-
message: sanitizeErrorMessage(error)
|
|
1438
|
-
});
|
|
1439
|
-
message = error instanceof Error ? error.message : String(error || message);
|
|
1440
|
-
}
|
|
1441
|
-
jsonError(cmdContext, { code, message, suggestion, docsUrl });
|
|
1442
|
-
process.exit(1);
|
|
1443
|
-
}
|
|
1444
|
-
console.error("");
|
|
1445
|
-
if (error instanceof WrapsError) {
|
|
1446
|
-
trackError(error.code, cmdContext);
|
|
1447
|
-
clack4.log.error(error.message);
|
|
1448
|
-
if (error.suggestion) {
|
|
1449
|
-
console.log(`
|
|
1450
|
-
${pc5.yellow("Suggestion:")}`);
|
|
1451
|
-
const lines = error.suggestion.split("\n");
|
|
1452
|
-
for (const line of lines) {
|
|
1453
|
-
console.log(` ${pc5.white(line)}`);
|
|
1454
|
-
}
|
|
1455
|
-
console.log();
|
|
1456
|
-
}
|
|
1457
|
-
if (error.docsUrl) {
|
|
1458
|
-
console.log(`${pc5.dim("Documentation:")}`);
|
|
1459
|
-
console.log(` ${pc5.blue(error.docsUrl)}
|
|
1460
|
-
`);
|
|
1461
|
-
}
|
|
1462
|
-
process.exit(1);
|
|
1463
|
-
}
|
|
1464
|
-
if (isAWSError(error)) {
|
|
1465
|
-
const { code, action } = parseAWSError(error);
|
|
1466
|
-
trackError(`AWS_${code}`, cmdContext, { action });
|
|
1467
|
-
const wrapsError = awsErrorToWrapsError(code, action, error);
|
|
1468
|
-
clack4.log.error(wrapsError.message);
|
|
1469
|
-
if (wrapsError.suggestion) {
|
|
1470
|
-
console.log(`
|
|
1471
|
-
${pc5.yellow("Suggestion:")}`);
|
|
1472
|
-
const lines = wrapsError.suggestion.split("\n");
|
|
1473
|
-
for (const line of lines) {
|
|
1474
|
-
console.log(` ${pc5.white(line)}`);
|
|
1475
|
-
}
|
|
1476
|
-
console.log();
|
|
1477
|
-
}
|
|
1478
|
-
if (wrapsError.docsUrl) {
|
|
1479
|
-
console.log(`${pc5.dim("Documentation:")}`);
|
|
1480
|
-
console.log(` ${pc5.blue(wrapsError.docsUrl)}
|
|
1481
|
-
`);
|
|
1482
|
-
}
|
|
1483
|
-
process.exit(1);
|
|
1484
|
-
}
|
|
1485
|
-
if (isPulumiError(error)) {
|
|
1486
|
-
const { code, iamAction, service, resourceName, resourceType } = parsePulumiError(error);
|
|
1487
|
-
trackError(`PULUMI_${code}`, cmdContext, {
|
|
1488
|
-
iamAction,
|
|
1489
|
-
service,
|
|
1490
|
-
errorType: error?.constructor?.name
|
|
1491
|
-
});
|
|
1492
|
-
const wrapsError = pulumiErrorToWrapsError(
|
|
1493
|
-
code,
|
|
1494
|
-
iamAction,
|
|
1495
|
-
service,
|
|
1496
|
-
resourceName,
|
|
1497
|
-
resourceType
|
|
1498
|
-
);
|
|
1499
|
-
clack4.log.error(wrapsError.message);
|
|
1500
|
-
if (wrapsError.suggestion) {
|
|
1501
|
-
console.log(`
|
|
1502
|
-
${pc5.yellow("Suggestion:")}`);
|
|
1503
|
-
const lines = wrapsError.suggestion.split("\n");
|
|
1504
|
-
for (const line of lines) {
|
|
1505
|
-
console.log(` ${pc5.white(line)}`);
|
|
1506
|
-
}
|
|
1507
|
-
console.log();
|
|
1508
|
-
}
|
|
1509
|
-
if (wrapsError.docsUrl) {
|
|
1510
|
-
console.log(`${pc5.dim("Documentation:")}`);
|
|
1511
|
-
console.log(` ${pc5.blue(wrapsError.docsUrl)}
|
|
1512
|
-
`);
|
|
1513
|
-
}
|
|
1514
|
-
process.exit(1);
|
|
1515
|
-
}
|
|
1516
|
-
trackError("UNHANDLED_ERROR", cmdContext, {
|
|
1517
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1518
|
-
message: sanitizeErrorMessage(error)
|
|
1519
|
-
});
|
|
1520
|
-
clack4.log.error("An unexpected error occurred");
|
|
1521
|
-
if (error instanceof Error) {
|
|
1522
|
-
console.error(pc5.dim(error.message));
|
|
1523
|
-
} else if (typeof error === "string") {
|
|
1524
|
-
console.error(error);
|
|
1525
|
-
}
|
|
1526
|
-
console.log(`
|
|
1527
|
-
${pc5.dim("If this persists, please report at:")}`);
|
|
1528
|
-
console.log(` ${pc5.blue("https://github.com/wraps-team/wraps/issues")}
|
|
1529
|
-
`);
|
|
1530
|
-
process.exit(1);
|
|
1531
|
-
}
|
|
1532
|
-
function awsErrorToWrapsError(code, action, originalError) {
|
|
1533
|
-
switch (code) {
|
|
1534
|
-
// Credential / token errors — these mean the request never reached the API
|
|
1535
|
-
case "ExpiredTokenException":
|
|
1536
|
-
case "TokenRefreshRequired":
|
|
1537
|
-
case "SSOTokenExpired":
|
|
1538
|
-
return errors.sessionTokenExpired();
|
|
1539
|
-
case "InvalidClientTokenId":
|
|
1540
|
-
case "InvalidAccessKeyId":
|
|
1541
|
-
case "SignatureDoesNotMatch":
|
|
1542
|
-
case "UnrecognizedClientException":
|
|
1543
|
-
return errors.accessKeyInvalid();
|
|
1544
|
-
// IAM permission errors — request reached AWS but was denied
|
|
1545
|
-
case "AccessDenied":
|
|
1546
|
-
case "AccessDeniedException":
|
|
1547
|
-
case "UnauthorizedAccess":
|
|
1548
|
-
return errors.iamPermissionDenied(
|
|
1549
|
-
action || "unknown",
|
|
1550
|
-
"AWS resource",
|
|
1551
|
-
"Ensure your IAM user/role has the required permissions."
|
|
1552
|
-
);
|
|
1553
|
-
// SES SendEmail errors — request reached SES but was rejected
|
|
1554
|
-
case "MessageRejected":
|
|
1555
|
-
return errors.sesMessageRejected(sanitizeErrorMessage(originalError));
|
|
1556
|
-
case "MailFromDomainNotVerifiedException":
|
|
1557
|
-
return errors.sesMailFromNotVerified(sanitizeErrorMessage(originalError));
|
|
1558
|
-
case "AccountSendingPausedException":
|
|
1559
|
-
return errors.sesAccountSendingPaused();
|
|
1560
|
-
case "ConfigurationSetSendingPausedException":
|
|
1561
|
-
return errors.sesConfigSetSendingPaused();
|
|
1562
|
-
case "ConfigurationSetDoesNotExistException":
|
|
1563
|
-
return errors.sesConfigSetMissing(sanitizeErrorMessage(originalError));
|
|
1564
|
-
// Throughput / quota errors
|
|
1565
|
-
case "Throttling":
|
|
1566
|
-
case "ThrottlingException":
|
|
1567
|
-
case "TooManyRequestsException":
|
|
1568
|
-
return errors.awsThrottled(action);
|
|
1569
|
-
case "LimitExceededException":
|
|
1570
|
-
case "ServiceQuotaExceededException":
|
|
1571
|
-
return errors.awsLimitExceeded(
|
|
1572
|
-
action,
|
|
1573
|
-
sanitizeErrorMessage(originalError)
|
|
1574
|
-
);
|
|
1575
|
-
// Anything else — surface the real error instead of lying about credentials
|
|
1576
|
-
default:
|
|
1577
|
-
return errors.awsUnknownError(
|
|
1578
|
-
code,
|
|
1579
|
-
action,
|
|
1580
|
-
sanitizeErrorMessage(originalError)
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
function pulumiErrorToWrapsError(code, iamAction, service, resourceName, resourceType) {
|
|
1585
|
-
switch (code) {
|
|
1586
|
-
case "RESOURCE_CONFLICT":
|
|
1587
|
-
return errors.resourceConflict(
|
|
1588
|
-
resourceName || "unknown resource",
|
|
1589
|
-
resourceType
|
|
1590
|
-
);
|
|
1591
|
-
case "STACK_LOCKED":
|
|
1592
|
-
return errors.stackLocked();
|
|
1593
|
-
case "SES_PERMISSION_DENIED":
|
|
1594
|
-
return errors.sesPermissionDenied(iamAction || "unknown");
|
|
1595
|
-
case "DYNAMODB_PERMISSION_DENIED":
|
|
1596
|
-
return errors.dynamoDBPermissionDenied();
|
|
1597
|
-
case "LAMBDA_PERMISSION_DENIED":
|
|
1598
|
-
return errors.lambdaPermissionDenied();
|
|
1599
|
-
case "EVENTBRIDGE_PERMISSION_DENIED":
|
|
1600
|
-
return errors.eventBridgePermissionDenied();
|
|
1601
|
-
case "SQS_PERMISSION_DENIED":
|
|
1602
|
-
return errors.sqsPermissionDenied();
|
|
1603
|
-
case "IAM_PERMISSION_DENIED":
|
|
1604
|
-
return errors.iamPermissionDenied(
|
|
1605
|
-
iamAction || "unknown",
|
|
1606
|
-
"AWS resource",
|
|
1607
|
-
service ? `Your IAM user/role needs ${service.toUpperCase()} permissions.` : "Ensure your IAM user/role has the required permissions."
|
|
1608
|
-
);
|
|
1609
|
-
default:
|
|
1610
|
-
return errors.pulumiError("Deployment failed");
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
var WrapsError, errors;
|
|
1614
|
-
var init_errors = __esm({
|
|
1615
|
-
"src/utils/shared/errors.ts"() {
|
|
1616
|
-
"use strict";
|
|
1617
|
-
init_esm_shims();
|
|
1618
|
-
init_events();
|
|
1619
|
-
init_json_output();
|
|
1620
|
-
WrapsError = class extends Error {
|
|
1621
|
-
constructor(message, code, suggestion, docsUrl) {
|
|
1622
|
-
super(message);
|
|
1623
|
-
this.code = code;
|
|
1624
|
-
this.suggestion = suggestion;
|
|
1625
|
-
this.docsUrl = docsUrl;
|
|
1626
|
-
this.name = "WrapsError";
|
|
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";
|
|
1627
799
|
}
|
|
1628
800
|
};
|
|
1629
801
|
errors = {
|
|
@@ -1938,204 +1110,1077 @@ You may need to merge your existing rules into the wraps rule set.`,
|
|
|
1938
1110
|
}
|
|
1939
1111
|
});
|
|
1940
1112
|
|
|
1941
|
-
// src/utils/shared/aws.ts
|
|
1942
|
-
var aws_exports = {};
|
|
1943
|
-
__export(aws_exports, {
|
|
1944
|
-
SES_REGIONS: () => SES_REGIONS,
|
|
1945
|
-
checkRegion: () => checkRegion,
|
|
1946
|
-
getACMCertificateStatus: () => getACMCertificateStatus,
|
|
1947
|
-
getAWSRegion: () => getAWSRegion,
|
|
1948
|
-
getSESAccountStatus: () => getSESAccountStatus,
|
|
1949
|
-
isSESSandbox: () => isSESSandbox,
|
|
1950
|
-
listSESDomains: () => listSESDomains,
|
|
1951
|
-
|
|
1952
|
-
|
|
1113
|
+
// src/utils/shared/aws.ts
|
|
1114
|
+
var aws_exports = {};
|
|
1115
|
+
__export(aws_exports, {
|
|
1116
|
+
SES_REGIONS: () => SES_REGIONS,
|
|
1117
|
+
checkRegion: () => checkRegion,
|
|
1118
|
+
getACMCertificateStatus: () => getACMCertificateStatus,
|
|
1119
|
+
getAWSRegion: () => getAWSRegion,
|
|
1120
|
+
getSESAccountStatus: () => getSESAccountStatus,
|
|
1121
|
+
isSESSandbox: () => isSESSandbox,
|
|
1122
|
+
listSESDomains: () => listSESDomains,
|
|
1123
|
+
resolveAWSCredentialsToEnv: () => resolveAWSCredentialsToEnv,
|
|
1124
|
+
validateAWSCredentials: () => validateAWSCredentials,
|
|
1125
|
+
validateAWSCredentialsWithDetails: () => validateAWSCredentialsWithDetails
|
|
1126
|
+
});
|
|
1127
|
+
import { ACMClient, DescribeCertificateCommand } from "@aws-sdk/client-acm";
|
|
1128
|
+
import {
|
|
1129
|
+
GetIdentityVerificationAttributesCommand,
|
|
1130
|
+
ListIdentitiesCommand,
|
|
1131
|
+
SESClient
|
|
1132
|
+
} from "@aws-sdk/client-ses";
|
|
1133
|
+
import { GetAccountCommand, SESv2Client } from "@aws-sdk/client-sesv2";
|
|
1134
|
+
import { GetCallerIdentityCommand as GetCallerIdentityCommand2, STSClient as STSClient2 } from "@aws-sdk/client-sts";
|
|
1135
|
+
async function validateAWSCredentials() {
|
|
1136
|
+
const result = await validateAWSCredentialsWithDetails();
|
|
1137
|
+
return result.identity;
|
|
1138
|
+
}
|
|
1139
|
+
async function validateAWSCredentialsWithDetails() {
|
|
1140
|
+
const state = await detectAWSState();
|
|
1141
|
+
const warnings = [];
|
|
1142
|
+
if (state.sso.configured && state.sso.tokenStatus?.expired) {
|
|
1143
|
+
const profile = state.sso.activeProfile?.name;
|
|
1144
|
+
throw errors.ssoSessionExpired(profile);
|
|
1145
|
+
}
|
|
1146
|
+
if (state.sso.configured && state.sso.tokenStatus?.valid && state.sso.tokenStatus.minutesRemaining !== null && state.sso.tokenStatus.minutesRemaining < 15) {
|
|
1147
|
+
const minutes = state.sso.tokenStatus.minutesRemaining;
|
|
1148
|
+
const loginCmd = getSSOLoginCommand(state.sso.activeProfile?.name);
|
|
1149
|
+
warnings.push(
|
|
1150
|
+
`SSO session expires in ${minutes} minute${minutes !== 1 ? "s" : ""}. Run "${loginCmd}" to refresh.`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
const currentProfile = getCurrentProfile();
|
|
1154
|
+
if (currentProfile && currentProfile !== "default") {
|
|
1155
|
+
const availableProfiles = getConfiguredProfiles();
|
|
1156
|
+
if (!availableProfiles.includes(currentProfile)) {
|
|
1157
|
+
throw errors.profileNotFound(currentProfile, availableProfiles);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const sts = new STSClient2({ region: "us-east-1" });
|
|
1161
|
+
try {
|
|
1162
|
+
const identity = await sts.send(new GetCallerIdentityCommand2({}));
|
|
1163
|
+
return {
|
|
1164
|
+
identity: {
|
|
1165
|
+
accountId: identity.Account,
|
|
1166
|
+
userId: identity.UserId,
|
|
1167
|
+
arn: identity.Arn
|
|
1168
|
+
},
|
|
1169
|
+
credentialSource: state.credentialSource,
|
|
1170
|
+
warnings
|
|
1171
|
+
};
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (error instanceof Error) {
|
|
1174
|
+
switch (error.name) {
|
|
1175
|
+
case "ExpiredTokenException":
|
|
1176
|
+
case "TokenRefreshRequired":
|
|
1177
|
+
throw errors.sessionTokenExpired();
|
|
1178
|
+
case "InvalidClientTokenId":
|
|
1179
|
+
case "InvalidAccessKeyId":
|
|
1180
|
+
case "SignatureDoesNotMatch":
|
|
1181
|
+
throw errors.accessKeyInvalid();
|
|
1182
|
+
case "CredentialsError":
|
|
1183
|
+
case "CredentialsProviderError":
|
|
1184
|
+
if (error.message?.includes("Could not load credentials")) {
|
|
1185
|
+
throw errors.credentialsFileMissing();
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
case "UnrecognizedClientException":
|
|
1189
|
+
throw errors.accessKeyInvalid();
|
|
1190
|
+
}
|
|
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
|
|
1953
1621
|
});
|
|
1954
|
-
import {
|
|
1955
|
-
import {
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
import { GetAccountCommand, SESv2Client } from "@aws-sdk/client-sesv2";
|
|
1961
|
-
import { GetCallerIdentityCommand as GetCallerIdentityCommand2, STSClient as STSClient2 } from "@aws-sdk/client-sts";
|
|
1962
|
-
async function validateAWSCredentials() {
|
|
1963
|
-
const result = await validateAWSCredentialsWithDetails();
|
|
1964
|
-
return result.identity;
|
|
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");
|
|
1965
1628
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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 });
|
|
1972
1636
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
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
|
+
};
|
|
1979
1855
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
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();
|
|
1986
1866
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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;
|
|
2013
1908
|
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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;
|
|
2017
2057
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
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;
|
|
2020
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);
|
|
2021
2137
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2138
|
+
function trackServiceInit(service, success, metadata) {
|
|
2139
|
+
const client = getTelemetryClient();
|
|
2140
|
+
client.track("service:init", {
|
|
2141
|
+
service,
|
|
2142
|
+
success,
|
|
2143
|
+
...metadata
|
|
2144
|
+
});
|
|
2024
2145
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
}
|
|
2032
|
-
return "us-east-1";
|
|
2146
|
+
function trackServiceDeployed(service, metadata) {
|
|
2147
|
+
const client = getTelemetryClient();
|
|
2148
|
+
client.track("service:deployed", {
|
|
2149
|
+
service,
|
|
2150
|
+
...metadata
|
|
2151
|
+
});
|
|
2033
2152
|
}
|
|
2034
|
-
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
);
|
|
2042
|
-
const identities = identitiesResponse.Identities || [];
|
|
2043
|
-
if (identities.length === 0) {
|
|
2044
|
-
return [];
|
|
2045
|
-
}
|
|
2046
|
-
const attributesResponse = await ses.send(
|
|
2047
|
-
new GetIdentityVerificationAttributesCommand({
|
|
2048
|
-
Identities: identities
|
|
2049
|
-
})
|
|
2050
|
-
);
|
|
2051
|
-
const attributes = attributesResponse.VerificationAttributes || {};
|
|
2052
|
-
return identities.map((domain) => ({
|
|
2053
|
-
domain,
|
|
2054
|
-
verified: attributes[domain]?.VerificationStatus === "Success"
|
|
2055
|
-
}));
|
|
2056
|
-
} catch {
|
|
2057
|
-
return [];
|
|
2058
|
-
}
|
|
2153
|
+
function trackError(errorCode, command, metadata) {
|
|
2154
|
+
const client = getTelemetryClient();
|
|
2155
|
+
client.track("error:occurred", {
|
|
2156
|
+
error_code: errorCode,
|
|
2157
|
+
command,
|
|
2158
|
+
...metadata
|
|
2159
|
+
});
|
|
2059
2160
|
}
|
|
2060
|
-
|
|
2061
|
-
const
|
|
2062
|
-
|
|
2063
|
-
const response = await sesv22.send(new GetAccountCommand({}));
|
|
2064
|
-
return {
|
|
2065
|
-
isSandbox: !response.ProductionAccessEnabled,
|
|
2066
|
-
sendQuota: response.SendQuota ? {
|
|
2067
|
-
max24HourSend: response.SendQuota.Max24HourSend ?? 0,
|
|
2068
|
-
maxSendRate: response.SendQuota.MaxSendRate ?? 0,
|
|
2069
|
-
sentLast24Hours: response.SendQuota.SentLast24Hours ?? 0
|
|
2070
|
-
} : void 0,
|
|
2071
|
-
enforcementStatus: response.EnforcementStatus
|
|
2072
|
-
};
|
|
2073
|
-
} catch {
|
|
2074
|
-
return { isSandbox: true, sandboxUncertain: true };
|
|
2075
|
-
}
|
|
2161
|
+
function trackFeature(feature, metadata) {
|
|
2162
|
+
const client = getTelemetryClient();
|
|
2163
|
+
client.track(`feature:${feature}`, metadata || {});
|
|
2076
2164
|
}
|
|
2077
|
-
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2165
|
+
function trackServiceUpgrade(service, metadata) {
|
|
2166
|
+
const client = getTelemetryClient();
|
|
2167
|
+
client.track("service:upgraded", {
|
|
2168
|
+
service,
|
|
2169
|
+
...metadata
|
|
2170
|
+
});
|
|
2080
2171
|
}
|
|
2081
|
-
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
})
|
|
2088
|
-
);
|
|
2089
|
-
const certificate = response.Certificate;
|
|
2090
|
-
if (!certificate) {
|
|
2091
|
-
return null;
|
|
2092
|
-
}
|
|
2093
|
-
const validationRecords = certificate.DomainValidationOptions?.map((option) => ({
|
|
2094
|
-
name: option.ResourceRecord?.Name || "",
|
|
2095
|
-
type: option.ResourceRecord?.Type || "",
|
|
2096
|
-
value: option.ResourceRecord?.Value || ""
|
|
2097
|
-
})) || [];
|
|
2098
|
-
return {
|
|
2099
|
-
status: certificate.Status || "UNKNOWN",
|
|
2100
|
-
domainName: certificate.DomainName || "",
|
|
2101
|
-
validationRecords
|
|
2102
|
-
};
|
|
2103
|
-
} catch (error) {
|
|
2104
|
-
console.error("Error getting ACM certificate status:", error);
|
|
2105
|
-
return null;
|
|
2106
|
-
}
|
|
2172
|
+
function trackServiceRemoved(service, metadata) {
|
|
2173
|
+
const client = getTelemetryClient();
|
|
2174
|
+
client.track("service:removed", {
|
|
2175
|
+
service,
|
|
2176
|
+
...metadata
|
|
2177
|
+
});
|
|
2107
2178
|
}
|
|
2108
|
-
var
|
|
2109
|
-
|
|
2110
|
-
"src/utils/shared/aws.ts"() {
|
|
2179
|
+
var init_events = __esm({
|
|
2180
|
+
"src/telemetry/events.ts"() {
|
|
2111
2181
|
"use strict";
|
|
2112
2182
|
init_esm_shims();
|
|
2113
|
-
|
|
2114
|
-
init_errors();
|
|
2115
|
-
SES_REGIONS = [
|
|
2116
|
-
"us-east-1",
|
|
2117
|
-
"us-east-2",
|
|
2118
|
-
"us-west-1",
|
|
2119
|
-
"us-west-2",
|
|
2120
|
-
"af-south-1",
|
|
2121
|
-
"ap-east-1",
|
|
2122
|
-
"ap-south-1",
|
|
2123
|
-
"ap-northeast-1",
|
|
2124
|
-
"ap-northeast-2",
|
|
2125
|
-
"ap-northeast-3",
|
|
2126
|
-
"ap-southeast-1",
|
|
2127
|
-
"ap-southeast-2",
|
|
2128
|
-
"ap-southeast-3",
|
|
2129
|
-
"ca-central-1",
|
|
2130
|
-
"eu-central-1",
|
|
2131
|
-
"eu-west-1",
|
|
2132
|
-
"eu-west-2",
|
|
2133
|
-
"eu-west-3",
|
|
2134
|
-
"eu-south-1",
|
|
2135
|
-
"eu-north-1",
|
|
2136
|
-
"me-south-1",
|
|
2137
|
-
"sa-east-1"
|
|
2138
|
-
];
|
|
2183
|
+
init_client();
|
|
2139
2184
|
}
|
|
2140
2185
|
});
|
|
2141
2186
|
|
|
@@ -9982,14 +10027,14 @@ init_esm_shims();
|
|
|
9982
10027
|
init_events();
|
|
9983
10028
|
init_config();
|
|
9984
10029
|
init_json_output();
|
|
9985
|
-
import * as
|
|
10030
|
+
import * as clack2 from "@clack/prompts";
|
|
9986
10031
|
import { createAuthClient } from "better-auth/client";
|
|
9987
10032
|
import {
|
|
9988
10033
|
deviceAuthorizationClient,
|
|
9989
10034
|
organizationClient
|
|
9990
10035
|
} from "better-auth/client/plugins";
|
|
9991
10036
|
import open from "open";
|
|
9992
|
-
import
|
|
10037
|
+
import pc3 from "picocolors";
|
|
9993
10038
|
function createCliAuthClient(baseURL) {
|
|
9994
10039
|
return createAuthClient({
|
|
9995
10040
|
baseURL,
|
|
@@ -10033,14 +10078,14 @@ async function login(options) {
|
|
|
10033
10078
|
if (isJsonMode()) {
|
|
10034
10079
|
jsonSuccess("auth.login", { tokenType: "api-key" });
|
|
10035
10080
|
} else {
|
|
10036
|
-
|
|
10081
|
+
clack2.log.success("API key saved.");
|
|
10037
10082
|
}
|
|
10038
10083
|
return;
|
|
10039
10084
|
}
|
|
10040
|
-
|
|
10085
|
+
clack2.intro(pc3.bold("Wraps \u203A Sign In"));
|
|
10041
10086
|
const baseURL = getAppBaseUrl();
|
|
10042
10087
|
const authClient = createCliAuthClient(baseURL);
|
|
10043
|
-
const spinner10 =
|
|
10088
|
+
const spinner10 = clack2.spinner();
|
|
10044
10089
|
const { data: codeData, error: codeError } = await authClient.device.code({
|
|
10045
10090
|
client_id: "wraps-cli"
|
|
10046
10091
|
});
|
|
@@ -10051,7 +10096,7 @@ async function login(options) {
|
|
|
10051
10096
|
method: "device"
|
|
10052
10097
|
});
|
|
10053
10098
|
trackError("DEVICE_AUTH_FAILED", "auth:login", { step: "request_code" });
|
|
10054
|
-
|
|
10099
|
+
clack2.log.error("Failed to start device authorization.");
|
|
10055
10100
|
throw new Error("Failed to start device authorization.");
|
|
10056
10101
|
}
|
|
10057
10102
|
const {
|
|
@@ -10062,11 +10107,11 @@ async function login(options) {
|
|
|
10062
10107
|
expires_in
|
|
10063
10108
|
} = codeData;
|
|
10064
10109
|
const formatted = `${user_code.slice(0, 4)}-${user_code.slice(4)}`;
|
|
10065
|
-
|
|
10066
|
-
|
|
10110
|
+
clack2.log.info(`Your code: ${pc3.bold(pc3.cyan(formatted))}`);
|
|
10111
|
+
clack2.log.info(`Visit: ${pc3.underline(`${baseURL}/device`)}`);
|
|
10067
10112
|
try {
|
|
10068
10113
|
await open(`${baseURL}/device?user_code=${user_code}`);
|
|
10069
|
-
|
|
10114
|
+
clack2.log.info("Opening browser...");
|
|
10070
10115
|
} catch {
|
|
10071
10116
|
}
|
|
10072
10117
|
spinner10.start("Waiting for approval...");
|
|
@@ -10098,14 +10143,14 @@ async function login(options) {
|
|
|
10098
10143
|
duration_ms: Date.now() - startTime,
|
|
10099
10144
|
method: "device"
|
|
10100
10145
|
});
|
|
10101
|
-
|
|
10146
|
+
clack2.log.success("Signed in successfully.");
|
|
10102
10147
|
if (organizations.length === 1) {
|
|
10103
|
-
|
|
10148
|
+
clack2.log.info(`Organization: ${pc3.cyan(organizations[0].name)}`);
|
|
10104
10149
|
} else if (organizations.length > 1) {
|
|
10105
|
-
|
|
10150
|
+
clack2.log.info(`${organizations.length} organizations available`);
|
|
10106
10151
|
} else {
|
|
10107
|
-
|
|
10108
|
-
`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.`
|
|
10109
10154
|
);
|
|
10110
10155
|
}
|
|
10111
10156
|
if (isJsonMode()) {
|
|
@@ -10134,7 +10179,7 @@ async function login(options) {
|
|
|
10134
10179
|
});
|
|
10135
10180
|
trackError("ACCESS_DENIED", "auth:login", { step: "poll_token" });
|
|
10136
10181
|
spinner10.stop("Denied.");
|
|
10137
|
-
|
|
10182
|
+
clack2.log.error("Authorization was denied.");
|
|
10138
10183
|
throw new Error("Authorization was denied.");
|
|
10139
10184
|
}
|
|
10140
10185
|
if (errorCode === "expired_token") {
|
|
@@ -10149,7 +10194,7 @@ async function login(options) {
|
|
|
10149
10194
|
});
|
|
10150
10195
|
trackError("DEVICE_CODE_EXPIRED", "auth:login", { step: "poll_token" });
|
|
10151
10196
|
spinner10.stop("Expired.");
|
|
10152
|
-
|
|
10197
|
+
clack2.log.error("Device code expired. Run `wraps auth login` to try again.");
|
|
10153
10198
|
throw new Error("Device code expired.");
|
|
10154
10199
|
}
|
|
10155
10200
|
|
|
@@ -10158,11 +10203,11 @@ init_esm_shims();
|
|
|
10158
10203
|
init_events();
|
|
10159
10204
|
init_config();
|
|
10160
10205
|
init_json_output();
|
|
10161
|
-
import * as
|
|
10162
|
-
import
|
|
10206
|
+
import * as clack3 from "@clack/prompts";
|
|
10207
|
+
import pc4 from "picocolors";
|
|
10163
10208
|
async function logout() {
|
|
10164
10209
|
if (!isJsonMode()) {
|
|
10165
|
-
|
|
10210
|
+
clack3.intro(pc4.bold("Wraps \u203A Sign Out"));
|
|
10166
10211
|
}
|
|
10167
10212
|
const config2 = await readAuthConfig();
|
|
10168
10213
|
if (!config2?.auth?.token) {
|
|
@@ -10171,7 +10216,7 @@ async function logout() {
|
|
|
10171
10216
|
jsonSuccess("auth.logout", { loggedOut: false, alreadyLoggedOut: true });
|
|
10172
10217
|
return;
|
|
10173
10218
|
}
|
|
10174
|
-
|
|
10219
|
+
clack3.log.info("Not signed in.");
|
|
10175
10220
|
return;
|
|
10176
10221
|
}
|
|
10177
10222
|
await clearAuthConfig();
|
|
@@ -10180,7 +10225,7 @@ async function logout() {
|
|
|
10180
10225
|
jsonSuccess("auth.logout", { loggedOut: true });
|
|
10181
10226
|
return;
|
|
10182
10227
|
}
|
|
10183
|
-
|
|
10228
|
+
clack3.log.success("Signed out. Token removed from ~/.wraps/config.json");
|
|
10184
10229
|
}
|
|
10185
10230
|
|
|
10186
10231
|
// src/commands/auth/status.ts
|
|
@@ -10188,8 +10233,8 @@ init_esm_shims();
|
|
|
10188
10233
|
init_events();
|
|
10189
10234
|
init_config();
|
|
10190
10235
|
init_json_output();
|
|
10191
|
-
import * as
|
|
10192
|
-
import
|
|
10236
|
+
import * as clack4 from "@clack/prompts";
|
|
10237
|
+
import pc5 from "picocolors";
|
|
10193
10238
|
async function authStatus(_options = {}) {
|
|
10194
10239
|
const config2 = await readAuthConfig();
|
|
10195
10240
|
if (!config2?.auth?.token) {
|
|
@@ -10197,8 +10242,8 @@ async function authStatus(_options = {}) {
|
|
|
10197
10242
|
if (isJsonMode()) {
|
|
10198
10243
|
jsonSuccess("auth.status", { authenticated: false });
|
|
10199
10244
|
} else {
|
|
10200
|
-
|
|
10201
|
-
|
|
10245
|
+
clack4.intro(pc5.bold("Wraps \u203A Auth Status"));
|
|
10246
|
+
clack4.log.info("Not signed in. Run `wraps auth login` to authenticate.");
|
|
10202
10247
|
}
|
|
10203
10248
|
return;
|
|
10204
10249
|
}
|
|
@@ -10212,10 +10257,10 @@ async function authStatus(_options = {}) {
|
|
|
10212
10257
|
expiresAt: expiresAt || null
|
|
10213
10258
|
});
|
|
10214
10259
|
} else {
|
|
10215
|
-
|
|
10216
|
-
|
|
10260
|
+
clack4.intro(pc5.bold("Wraps \u203A Auth Status"));
|
|
10261
|
+
clack4.log.info(`Token: ${masked} (${tokenType})`);
|
|
10217
10262
|
if (expiresAt) {
|
|
10218
|
-
|
|
10263
|
+
clack4.log.info(`Expires: ${new Date(expiresAt).toLocaleDateString()}`);
|
|
10219
10264
|
}
|
|
10220
10265
|
}
|
|
10221
10266
|
trackCommand("auth:status", { success: true, authenticated: true });
|
|
@@ -17852,10 +17897,18 @@ async function createSMTPCredentials(config2) {
|
|
|
17852
17897
|
|
|
17853
17898
|
// src/infrastructure/resources/sqs.ts
|
|
17854
17899
|
init_esm_shims();
|
|
17900
|
+
init_resource_checks();
|
|
17855
17901
|
import * as aws11 from "@pulumi/aws";
|
|
17902
|
+
var SQS_TIMEOUTS = {
|
|
17903
|
+
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
17904
|
+
};
|
|
17856
17905
|
async function createSQSResources() {
|
|
17857
|
-
const
|
|
17858
|
-
|
|
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,
|
|
17859
17912
|
messageRetentionSeconds: 1209600,
|
|
17860
17913
|
// 14 days
|
|
17861
17914
|
tags: {
|
|
@@ -17863,9 +17916,13 @@ async function createSQSResources() {
|
|
|
17863
17916
|
Service: "email",
|
|
17864
17917
|
Description: "Dead letter queue for failed SES event processing"
|
|
17865
17918
|
}
|
|
17919
|
+
};
|
|
17920
|
+
const dlq = new aws11.sqs.Queue(dlqName, dlqConfig, {
|
|
17921
|
+
...SQS_TIMEOUTS,
|
|
17922
|
+
...dlqUrl ? { import: dlqUrl } : {}
|
|
17866
17923
|
});
|
|
17867
|
-
const
|
|
17868
|
-
name:
|
|
17924
|
+
const queueConfig = {
|
|
17925
|
+
name: queueName,
|
|
17869
17926
|
visibilityTimeoutSeconds: 300,
|
|
17870
17927
|
// Must be >= Lambda timeout (5 minutes)
|
|
17871
17928
|
messageRetentionSeconds: 345600,
|
|
@@ -17884,6 +17941,10 @@ async function createSQSResources() {
|
|
|
17884
17941
|
Service: "email",
|
|
17885
17942
|
Description: "Queue for SES email events from EventBridge"
|
|
17886
17943
|
}
|
|
17944
|
+
};
|
|
17945
|
+
const queue = new aws11.sqs.Queue(queueName, queueConfig, {
|
|
17946
|
+
...SQS_TIMEOUTS,
|
|
17947
|
+
...queueUrl ? { import: queueUrl } : {}
|
|
17887
17948
|
});
|
|
17888
17949
|
return {
|
|
17889
17950
|
queue,
|
|
@@ -18637,6 +18698,8 @@ async function connect2(options) {
|
|
|
18637
18698
|
throw new Error(`Preview failed: ${msg}`);
|
|
18638
18699
|
}
|
|
18639
18700
|
}
|
|
18701
|
+
let pulumiLog = [];
|
|
18702
|
+
const debugPulumi = process.env.WRAPS_DEBUG === "1";
|
|
18640
18703
|
let outputs;
|
|
18641
18704
|
try {
|
|
18642
18705
|
outputs = await progress.execute(
|
|
@@ -18674,8 +18737,14 @@ async function connect2(options) {
|
|
|
18674
18737
|
`wraps-${identity.accountId}-${region}`
|
|
18675
18738
|
);
|
|
18676
18739
|
await stack.setConfig("aws:region", { value: region });
|
|
18677
|
-
const upResult = await stack.up({
|
|
18678
|
-
|
|
18740
|
+
const upResult = await stack.up({
|
|
18741
|
+
onOutput: (msg) => {
|
|
18742
|
+
pulumiLog.push(msg);
|
|
18743
|
+
if (debugPulumi) {
|
|
18744
|
+
process.stderr.write(msg);
|
|
18745
|
+
}
|
|
18746
|
+
}
|
|
18747
|
+
});
|
|
18679
18748
|
const pulumiOutputs = upResult.outputs;
|
|
18680
18749
|
return {
|
|
18681
18750
|
roleArn: pulumiOutputs.roleArn?.value,
|
|
@@ -18700,9 +18769,22 @@ async function connect2(options) {
|
|
|
18700
18769
|
trackError("STACK_LOCKED", "email:connect", { step: "deploy" });
|
|
18701
18770
|
throw errors.stackLocked();
|
|
18702
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
|
+
}
|
|
18703
18784
|
trackError("DEPLOYMENT_FAILED", "email:connect", { step: "deploy" });
|
|
18704
18785
|
throw new Error(`Pulumi deployment failed: ${msg}`);
|
|
18705
18786
|
}
|
|
18787
|
+
pulumiLog = [];
|
|
18706
18788
|
if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
|
|
18707
18789
|
const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
|
|
18708
18790
|
const hostedZone = await findHostedZone2(outputs.domain, region);
|