@vibecodemax/cli 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -14
- package/dist/cli.js +454 -87
- package/dist/storageS3.js +40 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ npx @vibecodemax/cli admin ensure-admin --email admin@example.com
|
|
|
46
46
|
|
|
47
47
|
This command reads `SUPABASE_SERVICE_ROLE_KEY` and `SUPABASE_URL` or `NEXT_PUBLIC_SUPABASE_URL` from local env files, performs lookup/create/promote/verify locally, and prints JSON only.
|
|
48
48
|
|
|
49
|
-
If a new user is created, the CLI output
|
|
49
|
+
If a new user is created, the CLI output does not expose a password. The result indicates that password setup still needs a user-facing reset or invite flow.
|
|
50
50
|
|
|
51
51
|
### `configure-site-redirects`
|
|
52
52
|
|
|
@@ -73,18 +73,6 @@ Enables Google OAuth using credentials from `.env.bootstrap.local`. The Supabase
|
|
|
73
73
|
npx @vibecodemax/cli enable-google-provider
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
### `apply-auth-templates`
|
|
77
|
-
|
|
78
|
-
Applies custom auth email templates from local project files. The Supabase personal access token must be stored as `SUPABASE_ACCESS_TOKEN`.
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
npx @vibecodemax/cli apply-auth-templates \
|
|
82
|
-
--confirm-email-enabled true \
|
|
83
|
-
--confirm-email-path src/email/confirm-email.html \
|
|
84
|
-
--reset-password-enabled true \
|
|
85
|
-
--reset-password-path src/email/reset-password.html
|
|
86
|
-
```
|
|
87
|
-
|
|
88
76
|
## Output
|
|
89
77
|
|
|
90
78
|
All commands print structured JSON to stdout and exit with code 0 on success, non-zero on failure.
|
|
@@ -99,4 +87,4 @@ All commands print structured JSON to stdout and exit with code 0 on success, no
|
|
|
99
87
|
|
|
100
88
|
## Security
|
|
101
89
|
|
|
102
|
-
Credentials are read from local env files and sent only to the relevant provider APIs. No secret values appear in CLI output.
|
|
90
|
+
Credentials are read from local env files and sent only to the relevant provider APIs. No secret values appear in CLI output. Secret-bearing local commands avoid putting credentials on child-process argv, and `read-setup-state` requires `.vibecodemax/setup-state.json` to remain secret-free.
|
package/dist/cli.js
CHANGED
|
@@ -48,6 +48,21 @@ const LEMON_EVENTS = [
|
|
|
48
48
|
"subscription_payment_failed",
|
|
49
49
|
"subscription_payment_success",
|
|
50
50
|
];
|
|
51
|
+
const SECRET_LIKE_KEY_PATTERN = /(access[_-]?token|refresh[_-]?token|service[_-]?role|webhook[_-]?secret|api[_-]?key|secret|password)/i;
|
|
52
|
+
const SECRET_LIKE_VALUE_PATTERN = /(sb_secret_[A-Za-z0-9_-]+|sk_(?:test|live)_[A-Za-z0-9]+|whsec_[A-Za-z0-9]+|SUPABASE_SERVICE_ROLE_KEY|SUPABASE_ACCESS_TOKEN)/;
|
|
53
|
+
const CHILD_PROCESS_ENV_KEYS = [
|
|
54
|
+
"PATH",
|
|
55
|
+
"HOME",
|
|
56
|
+
"USERPROFILE",
|
|
57
|
+
"APPDATA",
|
|
58
|
+
"LOCALAPPDATA",
|
|
59
|
+
"TMPDIR",
|
|
60
|
+
"TMP",
|
|
61
|
+
"TEMP",
|
|
62
|
+
"SYSTEMROOT",
|
|
63
|
+
"COMSPEC",
|
|
64
|
+
"SHELL",
|
|
65
|
+
];
|
|
51
66
|
function printJson(value) {
|
|
52
67
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
53
68
|
}
|
|
@@ -73,7 +88,7 @@ function parseArgs(argv) {
|
|
|
73
88
|
let subcommand;
|
|
74
89
|
const flags = {};
|
|
75
90
|
let index = 0;
|
|
76
|
-
if ((command === "admin" || command === "storage" || command === "payments") && rest[0] && !rest[0].startsWith("--")) {
|
|
91
|
+
if ((command === "base" || command === "admin" || command === "storage" || command === "payments") && rest[0] && !rest[0].startsWith("--")) {
|
|
77
92
|
subcommand = rest[0];
|
|
78
93
|
index = 1;
|
|
79
94
|
}
|
|
@@ -231,12 +246,43 @@ function requireServiceRoleKey(envValues) {
|
|
|
231
246
|
return requireEnvValue(envValues, "SUPABASE_SERVICE_ROLE_KEY", ".env.local");
|
|
232
247
|
}
|
|
233
248
|
function mergeEnvFile(filePath, nextValues) {
|
|
249
|
+
for (const [key, value] of Object.entries(nextValues)) {
|
|
250
|
+
if (/[\r\n]/.test(value)) {
|
|
251
|
+
fail("INVALID_ENV_VALUE", `${key} contains a newline and cannot be written safely to ${path.basename(filePath)}.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
234
254
|
const existing = parseDotEnv(readFileIfExists(filePath));
|
|
235
255
|
const merged = { ...existing, ...nextValues };
|
|
236
256
|
const keys = Object.keys(merged).sort();
|
|
237
257
|
const content = `${keys.map((key) => `${key}=${merged[key]}`).join("\n")}\n`;
|
|
238
258
|
fs.writeFileSync(filePath, content, "utf8");
|
|
239
259
|
}
|
|
260
|
+
function detectSecretLikeEntry(value, keyPath = "setupState") {
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
263
|
+
const nested = detectSecretLikeEntry(value[index], `${keyPath}[${index}]`);
|
|
264
|
+
if (nested)
|
|
265
|
+
return nested;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
if (value && typeof value === "object") {
|
|
270
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
271
|
+
const nestedPath = `${keyPath}.${key}`;
|
|
272
|
+
if (SECRET_LIKE_KEY_PATTERN.test(key)) {
|
|
273
|
+
return nestedPath;
|
|
274
|
+
}
|
|
275
|
+
const nested = detectSecretLikeEntry(nestedValue, nestedPath);
|
|
276
|
+
if (nested)
|
|
277
|
+
return nested;
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
if (typeof value === "string" && SECRET_LIKE_VALUE_PATTERN.test(value)) {
|
|
282
|
+
return keyPath;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
240
286
|
function readSetupState() {
|
|
241
287
|
const setupStatePath = path.join(process.cwd(), SETUP_STATE_PATH);
|
|
242
288
|
const content = readFileIfExists(setupStatePath).trim();
|
|
@@ -254,11 +300,19 @@ function readSetupState() {
|
|
|
254
300
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
255
301
|
fail("INVALID_SETUP_STATE", `${SETUP_STATE_PATH} must contain a JSON object.`);
|
|
256
302
|
}
|
|
303
|
+
const secretPath = detectSecretLikeEntry(parsed);
|
|
304
|
+
if (secretPath) {
|
|
305
|
+
fail("SECRET_IN_SETUP_STATE", `${SETUP_STATE_PATH} must remain secret-free. Remove secret-like data before reading it through the CLI.`, 1, {
|
|
306
|
+
keyPath: secretPath,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
257
309
|
printJson({ ok: true, setupState: parsed });
|
|
258
310
|
}
|
|
259
|
-
async function managementRequest({ method, projectRef, token, body }) {
|
|
260
|
-
const
|
|
261
|
-
|
|
311
|
+
async function managementRequest({ method, projectRef, token, body, endpoint }) {
|
|
312
|
+
const resolvedEndpoint = endpoint
|
|
313
|
+
? `${MANAGEMENT_API_BASE}${endpoint}`
|
|
314
|
+
: `${MANAGEMENT_API_BASE}/v1/projects/${projectRef}/config/auth`;
|
|
315
|
+
const response = await fetch(resolvedEndpoint, {
|
|
262
316
|
method,
|
|
263
317
|
headers: {
|
|
264
318
|
Accept: "application/json",
|
|
@@ -294,49 +348,6 @@ function readAuthConfigEnvelope(responseJson) {
|
|
|
294
348
|
return envelope.config.auth;
|
|
295
349
|
return responseJson;
|
|
296
350
|
}
|
|
297
|
-
function normalizeTemplateText(value) {
|
|
298
|
-
if (!isNonEmptyString(value))
|
|
299
|
-
return "";
|
|
300
|
-
return value.replace(/\r\n/g, "\n").trim();
|
|
301
|
-
}
|
|
302
|
-
function buildTemplateDesiredFields(options) {
|
|
303
|
-
const desired = {};
|
|
304
|
-
if (options.confirmEmailEnabled) {
|
|
305
|
-
const confirmHtml = normalizeTemplateText(readFileIfExists(path.resolve(process.cwd(), options.confirmEmailPath || "")));
|
|
306
|
-
if (!confirmHtml) {
|
|
307
|
-
fail("MISSING_TEMPLATE_FILE", `Confirm email template content is missing. Check ${options.confirmEmailPath}.`);
|
|
308
|
-
}
|
|
309
|
-
desired.mailer_subjects_confirmation = isNonEmptyString(options.confirmEmailSubject)
|
|
310
|
-
? options.confirmEmailSubject.trim()
|
|
311
|
-
: "Confirm your signup";
|
|
312
|
-
desired.mailer_templates_confirmation_content = confirmHtml;
|
|
313
|
-
}
|
|
314
|
-
if (options.resetPasswordEnabled) {
|
|
315
|
-
const resetHtml = normalizeTemplateText(readFileIfExists(path.resolve(process.cwd(), options.resetPasswordPath || "")));
|
|
316
|
-
if (!resetHtml) {
|
|
317
|
-
fail("MISSING_TEMPLATE_FILE", `Reset password template content is missing. Check ${options.resetPasswordPath}.`);
|
|
318
|
-
}
|
|
319
|
-
desired.mailer_subjects_recovery = isNonEmptyString(options.resetPasswordSubject)
|
|
320
|
-
? options.resetPasswordSubject.trim()
|
|
321
|
-
: "Reset your password";
|
|
322
|
-
desired.mailer_templates_recovery_content = resetHtml;
|
|
323
|
-
}
|
|
324
|
-
return desired;
|
|
325
|
-
}
|
|
326
|
-
function diffTemplateFields(currentAuthConfig = {}, desiredFields) {
|
|
327
|
-
const patch = {};
|
|
328
|
-
const changedKeys = [];
|
|
329
|
-
for (const [key, desiredValue] of Object.entries(desiredFields)) {
|
|
330
|
-
const currentValue = currentAuthConfig[key];
|
|
331
|
-
const normalizedCurrent = typeof currentValue === "string" ? normalizeTemplateText(currentValue) : currentValue;
|
|
332
|
-
const normalizedDesired = typeof desiredValue === "string" ? normalizeTemplateText(desiredValue) : desiredValue;
|
|
333
|
-
if (normalizedCurrent !== normalizedDesired) {
|
|
334
|
-
patch[key] = desiredValue;
|
|
335
|
-
changedKeys.push(key);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return { patch, changedKeys, noChanges: changedKeys.length === 0 };
|
|
339
|
-
}
|
|
340
351
|
async function configureSiteRedirects(flags) {
|
|
341
352
|
const { envLocalPath, envBootstrapPath, values } = loadLocalEnv();
|
|
342
353
|
const projectRef = resolveProjectRef(flags, values);
|
|
@@ -427,34 +438,318 @@ async function enableGoogleProvider(flags) {
|
|
|
427
438
|
applied: ["external_google_enabled", "external_google_client_id", "external_google_secret"],
|
|
428
439
|
});
|
|
429
440
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
441
|
+
function readObject(value) {
|
|
442
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
443
|
+
}
|
|
444
|
+
function readIdentifier(value) {
|
|
445
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
446
|
+
return String(value);
|
|
447
|
+
return isNonEmptyString(value) ? value.trim() : "";
|
|
448
|
+
}
|
|
449
|
+
function normalizeDependencyManagerFlag(flags) {
|
|
450
|
+
const dependencyManager = readStringFlag(flags, "dependency-manager");
|
|
451
|
+
if (dependencyManager === "pnpm")
|
|
452
|
+
return "pnpm";
|
|
453
|
+
if (dependencyManager === "yarn")
|
|
454
|
+
return "yarn";
|
|
455
|
+
return "npm";
|
|
456
|
+
}
|
|
457
|
+
function getSupabaseRunnerCommand(dependencyManager) {
|
|
458
|
+
if (dependencyManager === "pnpm") {
|
|
459
|
+
return { command: "pnpm", args: ["exec", "supabase"] };
|
|
460
|
+
}
|
|
461
|
+
if (dependencyManager === "yarn") {
|
|
462
|
+
return { command: "yarn", args: ["supabase"] };
|
|
463
|
+
}
|
|
464
|
+
return { command: "npx", args: ["supabase"] };
|
|
465
|
+
}
|
|
466
|
+
function runSupabaseCommand(args, dependencyManager) {
|
|
467
|
+
const runner = getSupabaseRunnerCommand(dependencyManager);
|
|
468
|
+
const result = spawnSync(runner.command, [...runner.args, ...args], {
|
|
469
|
+
cwd: process.cwd(),
|
|
470
|
+
encoding: "utf8",
|
|
471
|
+
});
|
|
472
|
+
if (result.status !== 0) {
|
|
473
|
+
fail("SUPABASE_CLI_ERROR", [result.stderr, result.stdout].find(isNonEmptyString) || `Supabase CLI ${args.join(" ")} failed.`, 1, { exitCode: result.status ?? 1 });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function supabaseManagementApiRequest(params) {
|
|
477
|
+
const response = await fetch(`${MANAGEMENT_API_BASE}${params.endpoint}`, {
|
|
478
|
+
method: params.method,
|
|
479
|
+
headers: {
|
|
480
|
+
Accept: "application/json",
|
|
481
|
+
"Content-Type": "application/json",
|
|
482
|
+
Authorization: `Bearer ${params.token}`,
|
|
483
|
+
},
|
|
484
|
+
body: params.body === undefined ? undefined : JSON.stringify(params.body),
|
|
485
|
+
});
|
|
486
|
+
let json = null;
|
|
487
|
+
try {
|
|
488
|
+
json = await response.json();
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
json = null;
|
|
492
|
+
}
|
|
493
|
+
if (!response.ok) {
|
|
494
|
+
const message = extractErrorMessage(json) || `Supabase returned ${response.status}`;
|
|
495
|
+
fail("MANAGEMENT_API_ERROR", message, 1, { status: response.status, endpoint: params.endpoint });
|
|
496
|
+
}
|
|
497
|
+
return json;
|
|
498
|
+
}
|
|
499
|
+
function readArrayResponse(value) {
|
|
500
|
+
if (Array.isArray(value))
|
|
501
|
+
return value;
|
|
502
|
+
const record = readObject(value);
|
|
503
|
+
for (const key of ["organizations", "projects", "api_keys", "apiKeys", "data"]) {
|
|
504
|
+
if (Array.isArray(record[key]))
|
|
505
|
+
return record[key];
|
|
506
|
+
}
|
|
507
|
+
return [];
|
|
508
|
+
}
|
|
509
|
+
function parseOrganizationRecord(value) {
|
|
510
|
+
const record = readObject(value);
|
|
511
|
+
const id = readIdentifier(record.id);
|
|
512
|
+
const name = readIdentifier(record.name || record.organization_name || record.slug || record.ref);
|
|
513
|
+
if (!id || !name)
|
|
514
|
+
return null;
|
|
515
|
+
return { id, name };
|
|
516
|
+
}
|
|
517
|
+
function parseProjectRecord(value) {
|
|
518
|
+
const record = readObject(value);
|
|
519
|
+
const ref = readIdentifier(record.id || record.ref || record.project_ref || record.projectRef);
|
|
520
|
+
const name = readIdentifier(record.name || record.project_name || record.projectName);
|
|
521
|
+
if (!ref || !name)
|
|
522
|
+
return null;
|
|
523
|
+
return {
|
|
524
|
+
ref,
|
|
525
|
+
name,
|
|
526
|
+
status: readIdentifier(record.status || record.project_status || record.state),
|
|
527
|
+
region: readIdentifier(record.region || record.region_code || record.aws_region || record.location),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function formatOrganizationsForPrompt(orgs) {
|
|
531
|
+
if (orgs.length === 0)
|
|
532
|
+
return "No Supabase organizations were found for this account.";
|
|
533
|
+
return orgs.map((org) => `- ${org.name} (${org.id})`).join("\n");
|
|
534
|
+
}
|
|
535
|
+
function formatProjectsForPrompt(projects) {
|
|
536
|
+
if (projects.length === 0)
|
|
537
|
+
return "No existing Supabase projects were found for this account.";
|
|
538
|
+
return projects.map((project) => {
|
|
539
|
+
const details = [project.ref, project.region].filter(Boolean).join(" • ");
|
|
540
|
+
return details ? `- ${project.name} (${details})` : `- ${project.name} (${project.ref})`;
|
|
541
|
+
}).join("\n");
|
|
542
|
+
}
|
|
543
|
+
async function listSupabaseOrganizations(token) {
|
|
544
|
+
const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: "/v1/organizations" });
|
|
545
|
+
return readArrayResponse(json)
|
|
546
|
+
.map(parseOrganizationRecord)
|
|
547
|
+
.filter((entry) => Boolean(entry));
|
|
548
|
+
}
|
|
549
|
+
async function createSupabaseOrganization(token, name) {
|
|
550
|
+
const json = await supabaseManagementApiRequest({
|
|
551
|
+
method: "POST",
|
|
552
|
+
token,
|
|
553
|
+
endpoint: "/v1/organizations",
|
|
554
|
+
body: { name },
|
|
555
|
+
});
|
|
556
|
+
const direct = parseOrganizationRecord(json);
|
|
557
|
+
if (direct)
|
|
558
|
+
return direct;
|
|
559
|
+
const nested = parseOrganizationRecord(readObject(json).organization || readObject(json).data);
|
|
560
|
+
if (nested)
|
|
561
|
+
return nested;
|
|
562
|
+
fail("INVALID_MANAGEMENT_RESPONSE", "Supabase organization creation did not return an organization identifier.");
|
|
563
|
+
}
|
|
564
|
+
async function listSupabaseProjects(token) {
|
|
565
|
+
const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: "/v1/projects" });
|
|
566
|
+
return readArrayResponse(json)
|
|
567
|
+
.map(parseProjectRecord)
|
|
568
|
+
.filter((entry) => Boolean(entry));
|
|
569
|
+
}
|
|
570
|
+
function generateDbPassword() {
|
|
571
|
+
return randomBytes(24).toString("base64url");
|
|
572
|
+
}
|
|
573
|
+
async function createSupabaseProject(token, params) {
|
|
574
|
+
const json = await supabaseManagementApiRequest({
|
|
575
|
+
method: "POST",
|
|
576
|
+
token,
|
|
577
|
+
endpoint: "/v1/projects",
|
|
578
|
+
body: {
|
|
579
|
+
organization_id: params.organizationId,
|
|
580
|
+
name: params.projectName,
|
|
581
|
+
region: params.region,
|
|
582
|
+
db_pass: params.dbPassword,
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
const direct = parseProjectRecord(json);
|
|
586
|
+
if (direct)
|
|
587
|
+
return direct;
|
|
588
|
+
const nested = parseProjectRecord(readObject(json).project || readObject(json).data);
|
|
589
|
+
if (nested)
|
|
590
|
+
return nested;
|
|
591
|
+
fail("INVALID_MANAGEMENT_RESPONSE", "Supabase project creation did not return a project ref.");
|
|
592
|
+
}
|
|
593
|
+
async function waitForSupabaseProjectReady(token, projectRef) {
|
|
594
|
+
const readyStatuses = new Set(["ACTIVE_HEALTHY", "ACTIVE", "HEALTHY", "READY"]);
|
|
595
|
+
const pendingStatuses = new Set(["INACTIVE", "CREATING", "COMING_UP", "INIT", "UNKNOWN"]);
|
|
596
|
+
for (let attempt = 0; attempt < 36; attempt += 1) {
|
|
597
|
+
const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: `/v1/projects/${projectRef}` });
|
|
598
|
+
const project = parseProjectRecord(json) || parseProjectRecord(readObject(json).project || readObject(json).data);
|
|
599
|
+
const status = readIdentifier(project?.status || readObject(json).status || readObject(json).project_status).toUpperCase();
|
|
600
|
+
if (!status || readyStatuses.has(status))
|
|
601
|
+
return;
|
|
602
|
+
if (!pendingStatuses.has(status))
|
|
603
|
+
return;
|
|
604
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
605
|
+
}
|
|
606
|
+
fail("PROJECT_NOT_READY", `Supabase project ${projectRef} did not become ready in time.`);
|
|
607
|
+
}
|
|
608
|
+
async function fetchSupabaseProjectKeys(token, projectRef) {
|
|
609
|
+
const json = await supabaseManagementApiRequest({
|
|
610
|
+
method: "GET",
|
|
611
|
+
token,
|
|
612
|
+
endpoint: `/v1/projects/${projectRef}/api-keys?reveal=true`,
|
|
613
|
+
});
|
|
614
|
+
const records = readArrayResponse(json).map(readObject);
|
|
615
|
+
const values = records
|
|
616
|
+
.map((record) => [record.api_key, record.apiKey, record.key, record.value].find(isNonEmptyString))
|
|
617
|
+
.filter((value) => isNonEmptyString(value));
|
|
618
|
+
const publishableKey = values.find((value) => value.startsWith("sb_publishable_")) || "";
|
|
619
|
+
const serviceRoleKey = values.find((value) => value.startsWith("sb_secret_")) || "";
|
|
620
|
+
if (!publishableKey || !serviceRoleKey) {
|
|
621
|
+
fail("MISSING_PROJECT_KEYS", `Supabase did not return publishable and secret API keys for project ${projectRef}.`);
|
|
435
622
|
}
|
|
623
|
+
return {
|
|
624
|
+
projectUrl: `https://${projectRef}.supabase.co`,
|
|
625
|
+
publishableKey,
|
|
626
|
+
serviceRoleKey,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, values) {
|
|
630
|
+
mergeEnvFile(envLocalPath, {
|
|
631
|
+
NEXT_PUBLIC_SUPABASE_URL: values.projectUrl,
|
|
632
|
+
SUPABASE_URL: values.projectUrl,
|
|
633
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: values.publishableKey,
|
|
634
|
+
SUPABASE_SERVICE_ROLE_KEY: values.serviceRoleKey,
|
|
635
|
+
});
|
|
636
|
+
const bootstrapValues = {
|
|
637
|
+
SUPABASE_PROJECT_REF: values.projectRef,
|
|
638
|
+
};
|
|
639
|
+
if (isNonEmptyString(values.dbPassword)) {
|
|
640
|
+
bootstrapValues.SUPABASE_DB_PASSWORD = values.dbPassword;
|
|
641
|
+
}
|
|
642
|
+
mergeEnvFile(envBootstrapPath, bootstrapValues);
|
|
643
|
+
return {
|
|
644
|
+
envKeysWritten: [
|
|
645
|
+
"NEXT_PUBLIC_SUPABASE_URL",
|
|
646
|
+
"SUPABASE_URL",
|
|
647
|
+
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
648
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
649
|
+
],
|
|
650
|
+
bootstrapKeysWritten: Object.keys(bootstrapValues),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
async function checkBaseSupabaseAccessToken() {
|
|
654
|
+
const { envBootstrapPath, values } = loadLocalEnv();
|
|
436
655
|
const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
656
|
+
const [organizations, projects] = await Promise.all([
|
|
657
|
+
listSupabaseOrganizations(accessToken),
|
|
658
|
+
listSupabaseProjects(accessToken),
|
|
659
|
+
]);
|
|
660
|
+
printJson({
|
|
661
|
+
ok: true,
|
|
662
|
+
command: "base check-supabase-access-token",
|
|
663
|
+
hasExistingProjects: projects.length > 0,
|
|
664
|
+
projectCount: projects.length,
|
|
665
|
+
projectList: formatProjectsForPrompt(projects),
|
|
666
|
+
projects: projects.map((project) => ({ ref: project.ref, name: project.name, region: project.region })),
|
|
667
|
+
hasOrganizations: organizations.length > 0,
|
|
668
|
+
organizationCount: organizations.length,
|
|
669
|
+
organizationList: formatOrganizationsForPrompt(organizations),
|
|
670
|
+
organizations: organizations,
|
|
444
671
|
});
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
672
|
+
}
|
|
673
|
+
async function createBaseSupabaseProject(flags) {
|
|
674
|
+
const { envLocalPath, envBootstrapPath, values } = loadLocalEnv();
|
|
675
|
+
const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
|
|
676
|
+
const dependencyManager = normalizeDependencyManagerFlag(flags);
|
|
677
|
+
const selection = readStringFlag(flags, "organization-selection");
|
|
678
|
+
if (!selection) {
|
|
679
|
+
fail("MISSING_ORGANIZATION_SELECTION", "--organization-selection is required.");
|
|
680
|
+
}
|
|
681
|
+
const projectName = readStringFlag(flags, "project-name");
|
|
682
|
+
if (!projectName) {
|
|
683
|
+
fail("MISSING_PROJECT_NAME", "--project-name is required.");
|
|
684
|
+
}
|
|
685
|
+
const region = readStringFlag(flags, "region");
|
|
686
|
+
if (!region) {
|
|
687
|
+
fail("MISSING_PROJECT_REGION", "--region is required.");
|
|
688
|
+
}
|
|
689
|
+
let organizationId = selection;
|
|
690
|
+
let createdOrganization = false;
|
|
691
|
+
if (selection === "create") {
|
|
692
|
+
const organizationName = readStringFlag(flags, "organization-name");
|
|
693
|
+
if (!organizationName) {
|
|
694
|
+
fail("MISSING_ORGANIZATION_NAME", "--organization-name is required when --organization-selection create is used.");
|
|
695
|
+
}
|
|
696
|
+
const organization = await createSupabaseOrganization(accessToken, organizationName);
|
|
697
|
+
organizationId = organization.id;
|
|
698
|
+
createdOrganization = true;
|
|
699
|
+
}
|
|
700
|
+
const existingDbPassword = readIdentifier(values.SUPABASE_DB_PASSWORD);
|
|
701
|
+
const dbPassword = existingDbPassword || generateDbPassword();
|
|
702
|
+
const project = await createSupabaseProject(accessToken, {
|
|
703
|
+
organizationId,
|
|
704
|
+
projectName,
|
|
705
|
+
region,
|
|
706
|
+
dbPassword,
|
|
707
|
+
});
|
|
708
|
+
await waitForSupabaseProjectReady(accessToken, project.ref);
|
|
709
|
+
runSupabaseCommand(["link", "--project-ref", project.ref], dependencyManager);
|
|
710
|
+
const keys = await fetchSupabaseProjectKeys(accessToken, project.ref);
|
|
711
|
+
const writes = writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, {
|
|
712
|
+
projectRef: project.ref,
|
|
713
|
+
projectUrl: keys.projectUrl,
|
|
714
|
+
publishableKey: keys.publishableKey,
|
|
715
|
+
serviceRoleKey: keys.serviceRoleKey,
|
|
716
|
+
dbPassword,
|
|
717
|
+
});
|
|
718
|
+
printJson({
|
|
719
|
+
ok: true,
|
|
720
|
+
command: "base create-supabase-project",
|
|
721
|
+
projectRef: project.ref,
|
|
722
|
+
projectUrl: keys.projectUrl,
|
|
723
|
+
envKeysWritten: writes.envKeysWritten,
|
|
724
|
+
bootstrapKeysWritten: writes.bootstrapKeysWritten,
|
|
725
|
+
dbPasswordGenerated: true,
|
|
726
|
+
dbPasswordReused: Boolean(existingDbPassword),
|
|
727
|
+
createdOrganization,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
async function linkBaseSupabaseProject(flags) {
|
|
731
|
+
const { envLocalPath, envBootstrapPath, values } = loadLocalEnv();
|
|
732
|
+
const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
|
|
733
|
+
const dependencyManager = normalizeDependencyManagerFlag(flags);
|
|
734
|
+
const projectRef = readStringFlag(flags, "project-ref");
|
|
735
|
+
if (!projectRef) {
|
|
736
|
+
fail("MISSING_PROJECT_REF", "--project-ref is required.");
|
|
450
737
|
}
|
|
738
|
+
runSupabaseCommand(["link", "--project-ref", projectRef], dependencyManager);
|
|
739
|
+
const keys = await fetchSupabaseProjectKeys(accessToken, projectRef);
|
|
740
|
+
const writes = writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, {
|
|
741
|
+
projectRef,
|
|
742
|
+
projectUrl: keys.projectUrl,
|
|
743
|
+
publishableKey: keys.publishableKey,
|
|
744
|
+
serviceRoleKey: keys.serviceRoleKey,
|
|
745
|
+
});
|
|
451
746
|
printJson({
|
|
452
747
|
ok: true,
|
|
453
|
-
command: "
|
|
748
|
+
command: "base link-supabase-project",
|
|
454
749
|
projectRef,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
750
|
+
projectUrl: keys.projectUrl,
|
|
751
|
+
envKeysWritten: writes.envKeysWritten,
|
|
752
|
+
bootstrapKeysWritten: writes.bootstrapKeysWritten,
|
|
458
753
|
});
|
|
459
754
|
}
|
|
460
755
|
async function supabaseAdminRequest(params) {
|
|
@@ -662,6 +957,24 @@ function getDependencyInstallCommand(dependencyManager, packageName) {
|
|
|
662
957
|
return `yarn add -D ${packageName}`;
|
|
663
958
|
return `npm install --save-dev ${packageName}`;
|
|
664
959
|
}
|
|
960
|
+
function buildChildProcessEnv(overrides = {}) {
|
|
961
|
+
const env = {};
|
|
962
|
+
for (const key of CHILD_PROCESS_ENV_KEYS) {
|
|
963
|
+
const value = process.env[key];
|
|
964
|
+
if (isNonEmptyString(value)) {
|
|
965
|
+
env[key] = value;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
969
|
+
if ((key.startsWith("FAKE_") || key.startsWith("MOCK_")) && isNonEmptyString(value)) {
|
|
970
|
+
env[key] = value;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
...env,
|
|
975
|
+
...overrides,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
665
978
|
function runShellCommand(command, cwd) {
|
|
666
979
|
const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
|
|
667
980
|
cwd,
|
|
@@ -676,6 +989,20 @@ function runShellCommand(command, cwd) {
|
|
|
676
989
|
}
|
|
677
990
|
return typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
678
991
|
}
|
|
992
|
+
function runDirectCommand(executable, args, cwd, options) {
|
|
993
|
+
const result = spawnSync(executable, args, {
|
|
994
|
+
cwd,
|
|
995
|
+
env: options.env,
|
|
996
|
+
encoding: "utf8",
|
|
997
|
+
});
|
|
998
|
+
if (result.status !== 0) {
|
|
999
|
+
const message = [result.stderr, result.stdout]
|
|
1000
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
1001
|
+
.find(Boolean) || options.fallbackMessage;
|
|
1002
|
+
fail(options.failureCode, message);
|
|
1003
|
+
}
|
|
1004
|
+
return typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
1005
|
+
}
|
|
679
1006
|
function runLinkedSupabaseCommand(command, cwd, failureCode, fallbackMessage) {
|
|
680
1007
|
const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
|
|
681
1008
|
cwd,
|
|
@@ -1125,11 +1452,17 @@ function ensureStripeLocalScripts(cwd, dependencyManager, localhostUrl) {
|
|
|
1125
1452
|
scripts,
|
|
1126
1453
|
};
|
|
1127
1454
|
}
|
|
1128
|
-
function waitForStripeListenSecret(
|
|
1455
|
+
function waitForStripeListenSecret(secretKey, localhostUrl, cwd) {
|
|
1129
1456
|
return new Promise((resolve, reject) => {
|
|
1130
|
-
const child = spawn(
|
|
1457
|
+
const child = spawn("stripe", [
|
|
1458
|
+
"listen",
|
|
1459
|
+
"--events",
|
|
1460
|
+
STRIPE_EVENTS.join(","),
|
|
1461
|
+
"--forward-to",
|
|
1462
|
+
`${localhostUrl}/api/webhooks/stripe`,
|
|
1463
|
+
], {
|
|
1131
1464
|
cwd,
|
|
1132
|
-
env:
|
|
1465
|
+
env: buildChildProcessEnv({ STRIPE_API_KEY: secretKey }),
|
|
1133
1466
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1134
1467
|
detached: true,
|
|
1135
1468
|
});
|
|
@@ -1192,6 +1525,14 @@ function formEncode(value, prefix = "") {
|
|
|
1192
1525
|
return entries;
|
|
1193
1526
|
}
|
|
1194
1527
|
async function stripeRequest(params) {
|
|
1528
|
+
const { response, json } = await stripeRequestWithResponse(params);
|
|
1529
|
+
if (!response.ok) {
|
|
1530
|
+
const message = extractErrorMessage(json?.error) || extractErrorMessage(json) || `Stripe returned ${response.status}`;
|
|
1531
|
+
fail("STRIPE_API_ERROR", message, 1, { status: response.status });
|
|
1532
|
+
}
|
|
1533
|
+
return json;
|
|
1534
|
+
}
|
|
1535
|
+
async function stripeRequestWithResponse(params) {
|
|
1195
1536
|
const url = new URL(`https://api.stripe.com${params.path}`);
|
|
1196
1537
|
if (params.query) {
|
|
1197
1538
|
for (const [key, value] of formEncode(params.query)) {
|
|
@@ -1212,11 +1553,7 @@ async function stripeRequest(params) {
|
|
|
1212
1553
|
body,
|
|
1213
1554
|
});
|
|
1214
1555
|
const json = await response.json().catch(() => null);
|
|
1215
|
-
|
|
1216
|
-
const message = extractErrorMessage(json?.error) || extractErrorMessage(json) || `Stripe returned ${response.status}`;
|
|
1217
|
-
fail("STRIPE_API_ERROR", message, 1, { status: response.status });
|
|
1218
|
-
}
|
|
1219
|
-
return json;
|
|
1556
|
+
return { response, json };
|
|
1220
1557
|
}
|
|
1221
1558
|
async function lemonRequest(params) {
|
|
1222
1559
|
const url = new URL(`https://api.lemonsqueezy.com/v1${params.path}`);
|
|
@@ -1303,12 +1640,27 @@ function checkStripeCli() {
|
|
|
1303
1640
|
}
|
|
1304
1641
|
function loginStripeCli(flags) {
|
|
1305
1642
|
const { values } = loadLocalEnv();
|
|
1306
|
-
|
|
1643
|
+
const cwd = process.cwd();
|
|
1644
|
+
ensureStripeCliInstalled(cwd);
|
|
1307
1645
|
const authMode = readStringFlag(flags, "auth-mode") === "api_key" ? "api_key" : "tty";
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1646
|
+
if (authMode === "api_key") {
|
|
1647
|
+
const secretKey = requireEnvValue(values, "STRIPE_SECRET_KEY", ".env.local");
|
|
1648
|
+
if (!isStripeSecretKey(secretKey)) {
|
|
1649
|
+
fail("INVALID_PAYMENTS_ENV", "STRIPE_SECRET_KEY is missing or malformed in .env.local.");
|
|
1650
|
+
}
|
|
1651
|
+
runDirectCommand("stripe", ["get", "/v1/account"], cwd, {
|
|
1652
|
+
env: buildChildProcessEnv({ STRIPE_API_KEY: secretKey }),
|
|
1653
|
+
failureCode: "LOCAL_COMMAND_FAILED",
|
|
1654
|
+
fallbackMessage: "Stripe CLI could not authenticate using STRIPE_SECRET_KEY.",
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
runDirectCommand("stripe", ["login"], cwd, {
|
|
1659
|
+
env: buildChildProcessEnv(),
|
|
1660
|
+
failureCode: "LOCAL_COMMAND_FAILED",
|
|
1661
|
+
fallbackMessage: "Stripe CLI login failed.",
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1312
1664
|
printJson({
|
|
1313
1665
|
ok: true,
|
|
1314
1666
|
command: "payments login-stripe-cli",
|
|
@@ -1379,7 +1731,7 @@ async function createStripeWebhook(flags) {
|
|
|
1379
1731
|
});
|
|
1380
1732
|
return;
|
|
1381
1733
|
}
|
|
1382
|
-
const
|
|
1734
|
+
const createdResponse = await stripeRequestWithResponse({
|
|
1383
1735
|
secretKey,
|
|
1384
1736
|
method: "POST",
|
|
1385
1737
|
path: "/v1/webhook_endpoints",
|
|
@@ -1389,6 +1741,16 @@ async function createStripeWebhook(flags) {
|
|
|
1389
1741
|
description: "VibeCodeMax payments webhook",
|
|
1390
1742
|
},
|
|
1391
1743
|
});
|
|
1744
|
+
if (!createdResponse.response.ok) {
|
|
1745
|
+
const errorMessage = extractErrorMessage(createdResponse.json?.error)
|
|
1746
|
+
|| extractErrorMessage(createdResponse.json)
|
|
1747
|
+
|| `Stripe returned ${createdResponse.response.status}`;
|
|
1748
|
+
if (/activate|activation|business details|submit.*business/i.test(errorMessage)) {
|
|
1749
|
+
fail("STRIPE_ACCOUNT_NOT_ACTIVATED", "Your Stripe account is not activated. Submit your business details in the Stripe dashboard before setting up live webhooks.", 1, { status: createdResponse.response.status });
|
|
1750
|
+
}
|
|
1751
|
+
fail("STRIPE_WEBHOOK_CREATE_FAILED", errorMessage, 1, { status: createdResponse.response.status });
|
|
1752
|
+
}
|
|
1753
|
+
const created = createdResponse.json;
|
|
1392
1754
|
if (!isStripeWebhookSecret(created.secret)) {
|
|
1393
1755
|
fail("STRIPE_WEBHOOK_CREATE_FAILED", "Stripe did not return a webhook signing secret.");
|
|
1394
1756
|
}
|
|
@@ -1418,10 +1780,9 @@ async function setupStripeLocalhost(flags) {
|
|
|
1418
1780
|
}
|
|
1419
1781
|
ensureStripeCliInstalled(cwd);
|
|
1420
1782
|
const scriptResult = ensureStripeLocalScripts(cwd, dependencyManager, localhostUrl);
|
|
1421
|
-
const listenCommand = `stripe listen --api-key ${shellQuote(secretKey.trim())} --events ${STRIPE_EVENTS.join(",")} --forward-to ${localhostUrl}/api/webhooks/stripe`;
|
|
1422
1783
|
let signingSecret = "";
|
|
1423
1784
|
try {
|
|
1424
|
-
signingSecret = await waitForStripeListenSecret(
|
|
1785
|
+
signingSecret = await waitForStripeListenSecret(secretKey.trim(), localhostUrl, cwd);
|
|
1425
1786
|
}
|
|
1426
1787
|
catch (error) {
|
|
1427
1788
|
fail("STRIPE_LOCALHOST_SETUP_FAILED", error instanceof Error ? error.message : "Failed to capture Stripe localhost webhook signing secret.");
|
|
@@ -1588,6 +1949,9 @@ async function main() {
|
|
|
1588
1949
|
ok: true,
|
|
1589
1950
|
commands: [
|
|
1590
1951
|
"read-setup-state",
|
|
1952
|
+
"base check-supabase-access-token",
|
|
1953
|
+
"base create-supabase-project",
|
|
1954
|
+
"base link-supabase-project",
|
|
1591
1955
|
"admin ensure-admin",
|
|
1592
1956
|
"storage check-supabase-context",
|
|
1593
1957
|
"storage setup-supabase",
|
|
@@ -1605,7 +1969,6 @@ async function main() {
|
|
|
1605
1969
|
"configure-site-redirects",
|
|
1606
1970
|
"configure-email-password",
|
|
1607
1971
|
"enable-google-provider",
|
|
1608
|
-
"apply-auth-templates",
|
|
1609
1972
|
],
|
|
1610
1973
|
});
|
|
1611
1974
|
return;
|
|
@@ -1618,6 +1981,12 @@ async function main() {
|
|
|
1618
1981
|
}
|
|
1619
1982
|
if (command === "read-setup-state")
|
|
1620
1983
|
return readSetupState();
|
|
1984
|
+
if (command === "base" && subcommand === "check-supabase-access-token")
|
|
1985
|
+
return checkBaseSupabaseAccessToken();
|
|
1986
|
+
if (command === "base" && subcommand === "create-supabase-project")
|
|
1987
|
+
return createBaseSupabaseProject(flags);
|
|
1988
|
+
if (command === "base" && subcommand === "link-supabase-project")
|
|
1989
|
+
return linkBaseSupabaseProject(flags);
|
|
1621
1990
|
if (command === "admin" && subcommand === "ensure-admin")
|
|
1622
1991
|
return ensureAdmin(flags);
|
|
1623
1992
|
if (command === "storage" && subcommand === "check-supabase-context")
|
|
@@ -1652,8 +2021,6 @@ async function main() {
|
|
|
1652
2021
|
return configureEmailPassword(flags);
|
|
1653
2022
|
if (command === "enable-google-provider")
|
|
1654
2023
|
return enableGoogleProvider(flags);
|
|
1655
|
-
if (command === "apply-auth-templates")
|
|
1656
|
-
return applyAuthTemplates(flags);
|
|
1657
2024
|
fail("UNKNOWN_COMMAND", `Unknown command: ${command}`);
|
|
1658
2025
|
}
|
|
1659
2026
|
main().catch((error) => {
|
package/dist/storageS3.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as crypto from "node:crypto";
|
|
4
|
-
import { S3Client, HeadBucketCommand, CreateBucketCommand, PutPublicAccessBlockCommand, PutBucketEncryptionCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketCorsCommand, GetBucketLocationCommand, GetPublicAccessBlockCommand, GetBucketEncryptionCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketCorsCommand, } from "@aws-sdk/client-s3";
|
|
4
|
+
import { S3Client, HeadBucketCommand, CreateBucketCommand, PutPublicAccessBlockCommand, PutBucketEncryptionCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketCorsCommand, DeleteBucketCorsCommand, GetBucketLocationCommand, GetPublicAccessBlockCommand, GetBucketEncryptionCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketCorsCommand, } from "@aws-sdk/client-s3";
|
|
5
5
|
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
|
|
6
6
|
const SETUP_CONFIG_PATH = path.join(".vibecodemax", "setup-config.json");
|
|
7
7
|
const DEFAULT_BUCKETS = {
|
|
@@ -98,6 +98,11 @@ function readStringFlag(flags, key) {
|
|
|
98
98
|
return typeof value === "string" ? value.trim() : "";
|
|
99
99
|
}
|
|
100
100
|
function mergeEnvFile(filePath, nextValues) {
|
|
101
|
+
for (const [key, value] of Object.entries(nextValues)) {
|
|
102
|
+
if (/[\r\n]/.test(value)) {
|
|
103
|
+
fail("INVALID_ENV_VALUE", `${key} contains a newline and cannot be written safely to ${path.basename(filePath)}.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
101
106
|
const existing = parseDotEnv(readFileIfExists(filePath));
|
|
102
107
|
const merged = { ...existing, ...nextValues };
|
|
103
108
|
const keys = Object.keys(merged).sort();
|
|
@@ -287,6 +292,11 @@ function isRegionMismatchError(error) {
|
|
|
287
292
|
const status = getAwsStatus(error);
|
|
288
293
|
return status === 301 || code === "PermanentRedirect" || code === "AuthorizationHeaderMalformed" || code === "IncorrectEndpoint" || code === "IllegalLocationConstraintException";
|
|
289
294
|
}
|
|
295
|
+
function isMissingCorsConfiguration(error) {
|
|
296
|
+
const code = getAwsCode(error);
|
|
297
|
+
const status = getAwsStatus(error);
|
|
298
|
+
return code === "NoSuchCORSConfiguration" || status === 404;
|
|
299
|
+
}
|
|
290
300
|
async function ensureSingleBucket(s3Client, bucket, region) {
|
|
291
301
|
let created = false;
|
|
292
302
|
try {
|
|
@@ -366,16 +376,23 @@ async function applyBucketSafety(region, credentials, buckets) {
|
|
|
366
376
|
}
|
|
367
377
|
async function applyBucketCors(region, credentials, buckets) {
|
|
368
378
|
const { s3Client } = createAwsClients(region, credentials);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
+
try {
|
|
380
|
+
await s3Client.send(new PutBucketCorsCommand({
|
|
381
|
+
Bucket: buckets.public,
|
|
382
|
+
CORSConfiguration: BUCKET_CORS_CONFIGURATION,
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
const message = error instanceof Error ? error.message : `Failed to apply CORS to ${buckets.public}.`;
|
|
387
|
+
fail("AWS_POLICY_APPLY_FAILED", message, 1, { bucket: buckets.public, awsCode: getAwsCode(error), awsStatus: getAwsStatus(error) });
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
await s3Client.send(new DeleteBucketCorsCommand({ Bucket: buckets.private }));
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
if (!isMissingCorsConfiguration(error)) {
|
|
394
|
+
const message = error instanceof Error ? error.message : `Failed to remove CORS from ${buckets.private}.`;
|
|
395
|
+
fail("AWS_POLICY_APPLY_FAILED", message, 1, { bucket: buckets.private, awsCode: getAwsCode(error), awsStatus: getAwsStatus(error) });
|
|
379
396
|
}
|
|
380
397
|
}
|
|
381
398
|
}
|
|
@@ -438,7 +455,16 @@ async function getBucketEvidence(s3Client, bucket, regionHint) {
|
|
|
438
455
|
const encryptionResponse = await s3Client.send(new GetBucketEncryptionCommand({ Bucket: bucket }));
|
|
439
456
|
const ownershipResponse = await s3Client.send(new GetBucketOwnershipControlsCommand({ Bucket: bucket }));
|
|
440
457
|
const policyResponse = await s3Client.send(new GetBucketPolicyCommand({ Bucket: bucket }));
|
|
441
|
-
|
|
458
|
+
let corsRules = [];
|
|
459
|
+
try {
|
|
460
|
+
const corsResponse = await s3Client.send(new GetBucketCorsCommand({ Bucket: bucket }));
|
|
461
|
+
corsRules = corsResponse.CORSRules || [];
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
if (!isMissingCorsConfiguration(error)) {
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
442
468
|
const policyText = typeof policyResponse.Policy === "string" ? policyResponse.Policy : "";
|
|
443
469
|
return {
|
|
444
470
|
bucketRegion,
|
|
@@ -447,7 +473,7 @@ async function getBucketEvidence(s3Client, bucket, regionHint) {
|
|
|
447
473
|
ownershipControls: ownershipResponse.OwnershipControls?.Rules?.[0]?.ObjectOwnership || null,
|
|
448
474
|
policyApplied: Boolean(policyText),
|
|
449
475
|
hasPublicReadPolicy: policyText.includes("\"Action\":\"s3:GetObject\"") || policyText.includes("\"Action\":[\"s3:GetObject\"]"),
|
|
450
|
-
corsRules
|
|
476
|
+
corsRules,
|
|
451
477
|
};
|
|
452
478
|
}
|
|
453
479
|
async function verifyTwoBuckets(region, credentials, buckets) {
|
|
@@ -473,7 +499,7 @@ async function verifyTwoBuckets(region, credentials, buckets) {
|
|
|
473
499
|
Boolean(privateEvidence.publicAccessBlock?.RestrictPublicBuckets) &&
|
|
474
500
|
privateEvidence.policyApplied === true &&
|
|
475
501
|
privateEvidence.hasPublicReadPolicy === false &&
|
|
476
|
-
Array.isArray(privateEvidence.corsRules) && privateEvidence.corsRules.length
|
|
502
|
+
Array.isArray(privateEvidence.corsRules) && privateEvidence.corsRules.length === 0;
|
|
477
503
|
if (!publicOk || !privateOk) {
|
|
478
504
|
fail("AWS_VERIFY_FAILED", "AWS S3 bucket configuration verification failed.", 1, {
|
|
479
505
|
publicBucket: buckets.public,
|