@vibecodemax/cli 0.1.10 → 0.1.12
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 +546 -81
- 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,427 @@ 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
|
+
function runSupabaseCommandCapture(args, dependencyManager) {
|
|
477
|
+
const runner = getSupabaseRunnerCommand(dependencyManager);
|
|
478
|
+
const result = spawnSync(runner.command, [...runner.args, ...args], {
|
|
479
|
+
cwd: process.cwd(),
|
|
480
|
+
encoding: "utf8",
|
|
481
|
+
});
|
|
482
|
+
if (result.status !== 0) {
|
|
483
|
+
fail("SUPABASE_CLI_ERROR", [result.stderr, result.stdout].find(isNonEmptyString) || `Supabase CLI ${args.join(" ")} failed.`, 1, { exitCode: result.status ?? 1 });
|
|
484
|
+
}
|
|
485
|
+
return result.stdout || "";
|
|
486
|
+
}
|
|
487
|
+
function extractMigrationVersion(value) {
|
|
488
|
+
if (!isNonEmptyString(value))
|
|
489
|
+
return "";
|
|
490
|
+
const match = value.trim().match(/^(\d{14})/);
|
|
491
|
+
return match ? match[1] : "";
|
|
492
|
+
}
|
|
493
|
+
function listLocalMigrationVersions(cwd = process.cwd()) {
|
|
494
|
+
const migrationsDir = path.join(cwd, "supabase", "migrations");
|
|
495
|
+
let entries = [];
|
|
496
|
+
try {
|
|
497
|
+
entries = fs.readdirSync(migrationsDir);
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
return [...new Set(entries.map((entry) => extractMigrationVersion(entry)).filter(Boolean))].sort();
|
|
503
|
+
}
|
|
504
|
+
function collectVersionsFromRows(rows, keyCandidates) {
|
|
505
|
+
const values = new Set();
|
|
506
|
+
for (const row of rows) {
|
|
507
|
+
if (!row || typeof row !== "object")
|
|
508
|
+
continue;
|
|
509
|
+
const record = row;
|
|
510
|
+
for (const key of keyCandidates) {
|
|
511
|
+
const version = extractMigrationVersion(record[key]);
|
|
512
|
+
if (version) {
|
|
513
|
+
values.add(version);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return Array.from(values).sort();
|
|
518
|
+
}
|
|
519
|
+
function parseMigrationListOutput(raw) {
|
|
520
|
+
const trimmed = raw.trim();
|
|
521
|
+
if (!trimmed) {
|
|
522
|
+
return { localVersions: [], remoteVersions: [] };
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
const parsed = JSON.parse(trimmed);
|
|
526
|
+
const rows = Array.isArray(parsed)
|
|
527
|
+
? parsed
|
|
528
|
+
: Array.isArray(parsed?.migrations)
|
|
529
|
+
? (parsed.migrations)
|
|
530
|
+
: Array.isArray(parsed?.data)
|
|
531
|
+
? (parsed.data)
|
|
532
|
+
: null;
|
|
533
|
+
if (!rows) {
|
|
534
|
+
fail("INVALID_MIGRATION_LIST_OUTPUT", "Supabase migration list JSON output did not contain a rows array.");
|
|
535
|
+
}
|
|
536
|
+
const localVersions = collectVersionsFromRows(rows, [
|
|
537
|
+
"local",
|
|
538
|
+
"local_version",
|
|
539
|
+
"localVersion",
|
|
540
|
+
"localMigration",
|
|
541
|
+
"local_migration",
|
|
542
|
+
]);
|
|
543
|
+
const remoteVersions = collectVersionsFromRows(rows, [
|
|
544
|
+
"remote",
|
|
545
|
+
"remote_version",
|
|
546
|
+
"remoteVersion",
|
|
547
|
+
"remoteMigration",
|
|
548
|
+
"remote_migration",
|
|
549
|
+
]);
|
|
550
|
+
if (localVersions.length === 0 && remoteVersions.length === 0) {
|
|
551
|
+
fail("INVALID_MIGRATION_LIST_OUTPUT", "Supabase migration list JSON output did not include recognizable local or remote migration version fields.");
|
|
552
|
+
}
|
|
553
|
+
return { localVersions, remoteVersions };
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
if (error instanceof SyntaxError) {
|
|
557
|
+
fail("INVALID_MIGRATION_LIST_OUTPUT", "Supabase migration list output was not valid JSON. Upgrade the local Supabase CLI if it does not support --output json for migration list.");
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async function supabaseManagementApiRequest(params) {
|
|
563
|
+
const response = await fetch(`${MANAGEMENT_API_BASE}${params.endpoint}`, {
|
|
564
|
+
method: params.method,
|
|
565
|
+
headers: {
|
|
566
|
+
Accept: "application/json",
|
|
567
|
+
"Content-Type": "application/json",
|
|
568
|
+
Authorization: `Bearer ${params.token}`,
|
|
569
|
+
},
|
|
570
|
+
body: params.body === undefined ? undefined : JSON.stringify(params.body),
|
|
571
|
+
});
|
|
572
|
+
let json = null;
|
|
573
|
+
try {
|
|
574
|
+
json = await response.json();
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
json = null;
|
|
578
|
+
}
|
|
579
|
+
if (!response.ok) {
|
|
580
|
+
const message = extractErrorMessage(json) || `Supabase returned ${response.status}`;
|
|
581
|
+
fail("MANAGEMENT_API_ERROR", message, 1, { status: response.status, endpoint: params.endpoint });
|
|
582
|
+
}
|
|
583
|
+
return json;
|
|
584
|
+
}
|
|
585
|
+
function readArrayResponse(value) {
|
|
586
|
+
if (Array.isArray(value))
|
|
587
|
+
return value;
|
|
588
|
+
const record = readObject(value);
|
|
589
|
+
for (const key of ["organizations", "projects", "api_keys", "apiKeys", "data"]) {
|
|
590
|
+
if (Array.isArray(record[key]))
|
|
591
|
+
return record[key];
|
|
592
|
+
}
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
function parseOrganizationRecord(value) {
|
|
596
|
+
const record = readObject(value);
|
|
597
|
+
const id = readIdentifier(record.id);
|
|
598
|
+
const name = readIdentifier(record.name || record.organization_name || record.slug || record.ref);
|
|
599
|
+
if (!id || !name)
|
|
600
|
+
return null;
|
|
601
|
+
return { id, name };
|
|
602
|
+
}
|
|
603
|
+
function parseProjectRecord(value) {
|
|
604
|
+
const record = readObject(value);
|
|
605
|
+
const ref = readIdentifier(record.id || record.ref || record.project_ref || record.projectRef);
|
|
606
|
+
const name = readIdentifier(record.name || record.project_name || record.projectName);
|
|
607
|
+
if (!ref || !name)
|
|
608
|
+
return null;
|
|
609
|
+
return {
|
|
610
|
+
ref,
|
|
611
|
+
name,
|
|
612
|
+
status: readIdentifier(record.status || record.project_status || record.state),
|
|
613
|
+
region: readIdentifier(record.region || record.region_code || record.aws_region || record.location),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function formatOrganizationsForPrompt(orgs) {
|
|
617
|
+
if (orgs.length === 0)
|
|
618
|
+
return "No Supabase organizations were found for this account.";
|
|
619
|
+
return orgs.map((org) => `- ${org.name} (${org.id})`).join("\n");
|
|
620
|
+
}
|
|
621
|
+
function formatProjectsForPrompt(projects) {
|
|
622
|
+
if (projects.length === 0)
|
|
623
|
+
return "No existing Supabase projects were found for this account.";
|
|
624
|
+
return projects.map((project) => {
|
|
625
|
+
const details = [project.ref, project.region].filter(Boolean).join(" • ");
|
|
626
|
+
return details ? `- ${project.name} (${details})` : `- ${project.name} (${project.ref})`;
|
|
627
|
+
}).join("\n");
|
|
628
|
+
}
|
|
629
|
+
async function listSupabaseOrganizations(token) {
|
|
630
|
+
const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: "/v1/organizations" });
|
|
631
|
+
return readArrayResponse(json)
|
|
632
|
+
.map(parseOrganizationRecord)
|
|
633
|
+
.filter((entry) => Boolean(entry));
|
|
634
|
+
}
|
|
635
|
+
async function createSupabaseOrganization(token, name) {
|
|
636
|
+
const json = await supabaseManagementApiRequest({
|
|
637
|
+
method: "POST",
|
|
638
|
+
token,
|
|
639
|
+
endpoint: "/v1/organizations",
|
|
640
|
+
body: { name },
|
|
641
|
+
});
|
|
642
|
+
const direct = parseOrganizationRecord(json);
|
|
643
|
+
if (direct)
|
|
644
|
+
return direct;
|
|
645
|
+
const nested = parseOrganizationRecord(readObject(json).organization || readObject(json).data);
|
|
646
|
+
if (nested)
|
|
647
|
+
return nested;
|
|
648
|
+
fail("INVALID_MANAGEMENT_RESPONSE", "Supabase organization creation did not return an organization identifier.");
|
|
649
|
+
}
|
|
650
|
+
async function listSupabaseProjects(token) {
|
|
651
|
+
const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: "/v1/projects" });
|
|
652
|
+
return readArrayResponse(json)
|
|
653
|
+
.map(parseProjectRecord)
|
|
654
|
+
.filter((entry) => Boolean(entry));
|
|
655
|
+
}
|
|
656
|
+
function generateDbPassword() {
|
|
657
|
+
return randomBytes(24).toString("base64url");
|
|
658
|
+
}
|
|
659
|
+
async function createSupabaseProject(token, params) {
|
|
660
|
+
const json = await supabaseManagementApiRequest({
|
|
661
|
+
method: "POST",
|
|
662
|
+
token,
|
|
663
|
+
endpoint: "/v1/projects",
|
|
664
|
+
body: {
|
|
665
|
+
organization_id: params.organizationId,
|
|
666
|
+
name: params.projectName,
|
|
667
|
+
region: params.region,
|
|
668
|
+
db_pass: params.dbPassword,
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
const direct = parseProjectRecord(json);
|
|
672
|
+
if (direct)
|
|
673
|
+
return direct;
|
|
674
|
+
const nested = parseProjectRecord(readObject(json).project || readObject(json).data);
|
|
675
|
+
if (nested)
|
|
676
|
+
return nested;
|
|
677
|
+
fail("INVALID_MANAGEMENT_RESPONSE", "Supabase project creation did not return a project ref.");
|
|
678
|
+
}
|
|
679
|
+
async function waitForSupabaseProjectReady(token, projectRef) {
|
|
680
|
+
const readyStatuses = new Set(["ACTIVE_HEALTHY", "ACTIVE", "HEALTHY", "READY"]);
|
|
681
|
+
const pendingStatuses = new Set(["INACTIVE", "CREATING", "COMING_UP", "INIT", "UNKNOWN"]);
|
|
682
|
+
for (let attempt = 0; attempt < 36; attempt += 1) {
|
|
683
|
+
const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: `/v1/projects/${projectRef}` });
|
|
684
|
+
const project = parseProjectRecord(json) || parseProjectRecord(readObject(json).project || readObject(json).data);
|
|
685
|
+
const status = readIdentifier(project?.status || readObject(json).status || readObject(json).project_status).toUpperCase();
|
|
686
|
+
if (!status || readyStatuses.has(status))
|
|
687
|
+
return;
|
|
688
|
+
if (!pendingStatuses.has(status))
|
|
689
|
+
return;
|
|
690
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
691
|
+
}
|
|
692
|
+
fail("PROJECT_NOT_READY", `Supabase project ${projectRef} did not become ready in time.`);
|
|
693
|
+
}
|
|
694
|
+
async function fetchSupabaseProjectKeys(token, projectRef) {
|
|
695
|
+
const json = await supabaseManagementApiRequest({
|
|
696
|
+
method: "GET",
|
|
697
|
+
token,
|
|
698
|
+
endpoint: `/v1/projects/${projectRef}/api-keys?reveal=true`,
|
|
699
|
+
});
|
|
700
|
+
const records = readArrayResponse(json).map(readObject);
|
|
701
|
+
const values = records
|
|
702
|
+
.map((record) => [record.api_key, record.apiKey, record.key, record.value].find(isNonEmptyString))
|
|
703
|
+
.filter((value) => isNonEmptyString(value));
|
|
704
|
+
const publishableKey = values.find((value) => value.startsWith("sb_publishable_")) || "";
|
|
705
|
+
const serviceRoleKey = values.find((value) => value.startsWith("sb_secret_")) || "";
|
|
706
|
+
if (!publishableKey || !serviceRoleKey) {
|
|
707
|
+
fail("MISSING_PROJECT_KEYS", `Supabase did not return publishable and secret API keys for project ${projectRef}.`);
|
|
435
708
|
}
|
|
709
|
+
return {
|
|
710
|
+
projectUrl: `https://${projectRef}.supabase.co`,
|
|
711
|
+
publishableKey,
|
|
712
|
+
serviceRoleKey,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, values) {
|
|
716
|
+
mergeEnvFile(envLocalPath, {
|
|
717
|
+
NEXT_PUBLIC_SUPABASE_URL: values.projectUrl,
|
|
718
|
+
SUPABASE_URL: values.projectUrl,
|
|
719
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: values.publishableKey,
|
|
720
|
+
SUPABASE_SERVICE_ROLE_KEY: values.serviceRoleKey,
|
|
721
|
+
});
|
|
722
|
+
const bootstrapValues = {
|
|
723
|
+
SUPABASE_PROJECT_REF: values.projectRef,
|
|
724
|
+
};
|
|
725
|
+
if (isNonEmptyString(values.dbPassword)) {
|
|
726
|
+
bootstrapValues.SUPABASE_DB_PASSWORD = values.dbPassword;
|
|
727
|
+
}
|
|
728
|
+
mergeEnvFile(envBootstrapPath, bootstrapValues);
|
|
729
|
+
return {
|
|
730
|
+
envKeysWritten: [
|
|
731
|
+
"NEXT_PUBLIC_SUPABASE_URL",
|
|
732
|
+
"SUPABASE_URL",
|
|
733
|
+
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
734
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
735
|
+
],
|
|
736
|
+
bootstrapKeysWritten: Object.keys(bootstrapValues),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
async function checkBaseSupabaseAccessToken() {
|
|
740
|
+
const { envBootstrapPath, values } = loadLocalEnv();
|
|
436
741
|
const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
742
|
+
const [organizations, projects] = await Promise.all([
|
|
743
|
+
listSupabaseOrganizations(accessToken),
|
|
744
|
+
listSupabaseProjects(accessToken),
|
|
745
|
+
]);
|
|
746
|
+
printJson({
|
|
747
|
+
ok: true,
|
|
748
|
+
command: "base check-supabase-access-token",
|
|
749
|
+
hasExistingProjects: projects.length > 0,
|
|
750
|
+
projectCount: projects.length,
|
|
751
|
+
projectList: formatProjectsForPrompt(projects),
|
|
752
|
+
projects: projects.map((project) => ({ ref: project.ref, name: project.name, region: project.region })),
|
|
753
|
+
hasOrganizations: organizations.length > 0,
|
|
754
|
+
organizationCount: organizations.length,
|
|
755
|
+
organizationList: formatOrganizationsForPrompt(organizations),
|
|
756
|
+
organizations: organizations,
|
|
444
757
|
});
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
758
|
+
}
|
|
759
|
+
async function createBaseSupabaseProject(flags) {
|
|
760
|
+
const { envLocalPath, envBootstrapPath, values } = loadLocalEnv();
|
|
761
|
+
const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
|
|
762
|
+
const dependencyManager = normalizeDependencyManagerFlag(flags);
|
|
763
|
+
const selection = readStringFlag(flags, "organization-selection");
|
|
764
|
+
if (!selection) {
|
|
765
|
+
fail("MISSING_ORGANIZATION_SELECTION", "--organization-selection is required.");
|
|
766
|
+
}
|
|
767
|
+
const projectName = readStringFlag(flags, "project-name");
|
|
768
|
+
if (!projectName) {
|
|
769
|
+
fail("MISSING_PROJECT_NAME", "--project-name is required.");
|
|
770
|
+
}
|
|
771
|
+
const region = readStringFlag(flags, "region");
|
|
772
|
+
if (!region) {
|
|
773
|
+
fail("MISSING_PROJECT_REGION", "--region is required.");
|
|
774
|
+
}
|
|
775
|
+
let organizationId = selection;
|
|
776
|
+
let createdOrganization = false;
|
|
777
|
+
if (selection === "create") {
|
|
778
|
+
const organizationName = readStringFlag(flags, "organization-name");
|
|
779
|
+
if (!organizationName) {
|
|
780
|
+
fail("MISSING_ORGANIZATION_NAME", "--organization-name is required when --organization-selection create is used.");
|
|
781
|
+
}
|
|
782
|
+
const organization = await createSupabaseOrganization(accessToken, organizationName);
|
|
783
|
+
organizationId = organization.id;
|
|
784
|
+
createdOrganization = true;
|
|
785
|
+
}
|
|
786
|
+
const existingDbPassword = readIdentifier(values.SUPABASE_DB_PASSWORD);
|
|
787
|
+
const dbPassword = existingDbPassword || generateDbPassword();
|
|
788
|
+
const project = await createSupabaseProject(accessToken, {
|
|
789
|
+
organizationId,
|
|
790
|
+
projectName,
|
|
791
|
+
region,
|
|
792
|
+
dbPassword,
|
|
793
|
+
});
|
|
794
|
+
await waitForSupabaseProjectReady(accessToken, project.ref);
|
|
795
|
+
runSupabaseCommand(["link", "--project-ref", project.ref], dependencyManager);
|
|
796
|
+
const keys = await fetchSupabaseProjectKeys(accessToken, project.ref);
|
|
797
|
+
const writes = writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, {
|
|
798
|
+
projectRef: project.ref,
|
|
799
|
+
projectUrl: keys.projectUrl,
|
|
800
|
+
publishableKey: keys.publishableKey,
|
|
801
|
+
serviceRoleKey: keys.serviceRoleKey,
|
|
802
|
+
dbPassword,
|
|
803
|
+
});
|
|
804
|
+
printJson({
|
|
805
|
+
ok: true,
|
|
806
|
+
command: "base create-supabase-project",
|
|
807
|
+
projectRef: project.ref,
|
|
808
|
+
projectUrl: keys.projectUrl,
|
|
809
|
+
envKeysWritten: writes.envKeysWritten,
|
|
810
|
+
bootstrapKeysWritten: writes.bootstrapKeysWritten,
|
|
811
|
+
dbPasswordGenerated: true,
|
|
812
|
+
dbPasswordReused: Boolean(existingDbPassword),
|
|
813
|
+
createdOrganization,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
async function linkBaseSupabaseProject(flags) {
|
|
817
|
+
const { envLocalPath, envBootstrapPath, values } = loadLocalEnv();
|
|
818
|
+
const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
|
|
819
|
+
const dependencyManager = normalizeDependencyManagerFlag(flags);
|
|
820
|
+
const projectRef = readStringFlag(flags, "project-ref");
|
|
821
|
+
if (!projectRef) {
|
|
822
|
+
fail("MISSING_PROJECT_REF", "--project-ref is required.");
|
|
450
823
|
}
|
|
824
|
+
runSupabaseCommand(["link", "--project-ref", projectRef], dependencyManager);
|
|
825
|
+
const keys = await fetchSupabaseProjectKeys(accessToken, projectRef);
|
|
826
|
+
const writes = writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, {
|
|
827
|
+
projectRef,
|
|
828
|
+
projectUrl: keys.projectUrl,
|
|
829
|
+
publishableKey: keys.publishableKey,
|
|
830
|
+
serviceRoleKey: keys.serviceRoleKey,
|
|
831
|
+
});
|
|
451
832
|
printJson({
|
|
452
833
|
ok: true,
|
|
453
|
-
command: "
|
|
834
|
+
command: "base link-supabase-project",
|
|
454
835
|
projectRef,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
836
|
+
projectUrl: keys.projectUrl,
|
|
837
|
+
envKeysWritten: writes.envKeysWritten,
|
|
838
|
+
bootstrapKeysWritten: writes.bootstrapKeysWritten,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
function preflightBaseMigrationState(flags) {
|
|
842
|
+
const dependencyManager = normalizeDependencyManagerFlag(flags);
|
|
843
|
+
const localVersions = listLocalMigrationVersions();
|
|
844
|
+
const raw = runSupabaseCommandCapture(["migration", "list", "--linked", "--output", "json"], dependencyManager);
|
|
845
|
+
const parsed = parseMigrationListOutput(raw);
|
|
846
|
+
const localSet = new Set(localVersions);
|
|
847
|
+
const remoteSet = new Set(parsed.remoteVersions);
|
|
848
|
+
const localOnlyVersions = localVersions.filter((version) => !remoteSet.has(version));
|
|
849
|
+
const remoteOnlyVersions = parsed.remoteVersions.filter((version) => !localSet.has(version));
|
|
850
|
+
const blocked = remoteOnlyVersions.length > 0;
|
|
851
|
+
printJson({
|
|
852
|
+
ok: true,
|
|
853
|
+
command: "base preflight-migration-state",
|
|
854
|
+
localVersions,
|
|
855
|
+
remoteVersions: parsed.remoteVersions,
|
|
856
|
+
localOnlyVersions,
|
|
857
|
+
remoteOnlyVersions,
|
|
858
|
+
blocked,
|
|
859
|
+
requiresUserDecision: blocked,
|
|
860
|
+
decisionOptions: blocked ? ["reset", "repair", "relink"] : ["continue"],
|
|
861
|
+
status: blocked ? "diverged" : "aligned_or_recoverable",
|
|
458
862
|
});
|
|
459
863
|
}
|
|
460
864
|
async function supabaseAdminRequest(params) {
|
|
@@ -662,6 +1066,24 @@ function getDependencyInstallCommand(dependencyManager, packageName) {
|
|
|
662
1066
|
return `yarn add -D ${packageName}`;
|
|
663
1067
|
return `npm install --save-dev ${packageName}`;
|
|
664
1068
|
}
|
|
1069
|
+
function buildChildProcessEnv(overrides = {}) {
|
|
1070
|
+
const env = {};
|
|
1071
|
+
for (const key of CHILD_PROCESS_ENV_KEYS) {
|
|
1072
|
+
const value = process.env[key];
|
|
1073
|
+
if (isNonEmptyString(value)) {
|
|
1074
|
+
env[key] = value;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1078
|
+
if ((key.startsWith("FAKE_") || key.startsWith("MOCK_")) && isNonEmptyString(value)) {
|
|
1079
|
+
env[key] = value;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
...env,
|
|
1084
|
+
...overrides,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
665
1087
|
function runShellCommand(command, cwd) {
|
|
666
1088
|
const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
|
|
667
1089
|
cwd,
|
|
@@ -676,6 +1098,20 @@ function runShellCommand(command, cwd) {
|
|
|
676
1098
|
}
|
|
677
1099
|
return typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
678
1100
|
}
|
|
1101
|
+
function runDirectCommand(executable, args, cwd, options) {
|
|
1102
|
+
const result = spawnSync(executable, args, {
|
|
1103
|
+
cwd,
|
|
1104
|
+
env: options.env,
|
|
1105
|
+
encoding: "utf8",
|
|
1106
|
+
});
|
|
1107
|
+
if (result.status !== 0) {
|
|
1108
|
+
const message = [result.stderr, result.stdout]
|
|
1109
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
1110
|
+
.find(Boolean) || options.fallbackMessage;
|
|
1111
|
+
fail(options.failureCode, message);
|
|
1112
|
+
}
|
|
1113
|
+
return typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
1114
|
+
}
|
|
679
1115
|
function runLinkedSupabaseCommand(command, cwd, failureCode, fallbackMessage) {
|
|
680
1116
|
const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
|
|
681
1117
|
cwd,
|
|
@@ -1125,11 +1561,17 @@ function ensureStripeLocalScripts(cwd, dependencyManager, localhostUrl) {
|
|
|
1125
1561
|
scripts,
|
|
1126
1562
|
};
|
|
1127
1563
|
}
|
|
1128
|
-
function waitForStripeListenSecret(
|
|
1564
|
+
function waitForStripeListenSecret(secretKey, localhostUrl, cwd) {
|
|
1129
1565
|
return new Promise((resolve, reject) => {
|
|
1130
|
-
const child = spawn(
|
|
1566
|
+
const child = spawn("stripe", [
|
|
1567
|
+
"listen",
|
|
1568
|
+
"--events",
|
|
1569
|
+
STRIPE_EVENTS.join(","),
|
|
1570
|
+
"--forward-to",
|
|
1571
|
+
`${localhostUrl}/api/webhooks/stripe`,
|
|
1572
|
+
], {
|
|
1131
1573
|
cwd,
|
|
1132
|
-
env:
|
|
1574
|
+
env: buildChildProcessEnv({ STRIPE_API_KEY: secretKey }),
|
|
1133
1575
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1134
1576
|
detached: true,
|
|
1135
1577
|
});
|
|
@@ -1307,12 +1749,27 @@ function checkStripeCli() {
|
|
|
1307
1749
|
}
|
|
1308
1750
|
function loginStripeCli(flags) {
|
|
1309
1751
|
const { values } = loadLocalEnv();
|
|
1310
|
-
|
|
1752
|
+
const cwd = process.cwd();
|
|
1753
|
+
ensureStripeCliInstalled(cwd);
|
|
1311
1754
|
const authMode = readStringFlag(flags, "auth-mode") === "api_key" ? "api_key" : "tty";
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1755
|
+
if (authMode === "api_key") {
|
|
1756
|
+
const secretKey = requireEnvValue(values, "STRIPE_SECRET_KEY", ".env.local");
|
|
1757
|
+
if (!isStripeSecretKey(secretKey)) {
|
|
1758
|
+
fail("INVALID_PAYMENTS_ENV", "STRIPE_SECRET_KEY is missing or malformed in .env.local.");
|
|
1759
|
+
}
|
|
1760
|
+
runDirectCommand("stripe", ["get", "/v1/account"], cwd, {
|
|
1761
|
+
env: buildChildProcessEnv({ STRIPE_API_KEY: secretKey }),
|
|
1762
|
+
failureCode: "LOCAL_COMMAND_FAILED",
|
|
1763
|
+
fallbackMessage: "Stripe CLI could not authenticate using STRIPE_SECRET_KEY.",
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
else {
|
|
1767
|
+
runDirectCommand("stripe", ["login"], cwd, {
|
|
1768
|
+
env: buildChildProcessEnv(),
|
|
1769
|
+
failureCode: "LOCAL_COMMAND_FAILED",
|
|
1770
|
+
fallbackMessage: "Stripe CLI login failed.",
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1316
1773
|
printJson({
|
|
1317
1774
|
ok: true,
|
|
1318
1775
|
command: "payments login-stripe-cli",
|
|
@@ -1432,10 +1889,9 @@ async function setupStripeLocalhost(flags) {
|
|
|
1432
1889
|
}
|
|
1433
1890
|
ensureStripeCliInstalled(cwd);
|
|
1434
1891
|
const scriptResult = ensureStripeLocalScripts(cwd, dependencyManager, localhostUrl);
|
|
1435
|
-
const listenCommand = `stripe listen --api-key ${shellQuote(secretKey.trim())} --events ${STRIPE_EVENTS.join(",")} --forward-to ${localhostUrl}/api/webhooks/stripe`;
|
|
1436
1892
|
let signingSecret = "";
|
|
1437
1893
|
try {
|
|
1438
|
-
signingSecret = await waitForStripeListenSecret(
|
|
1894
|
+
signingSecret = await waitForStripeListenSecret(secretKey.trim(), localhostUrl, cwd);
|
|
1439
1895
|
}
|
|
1440
1896
|
catch (error) {
|
|
1441
1897
|
fail("STRIPE_LOCALHOST_SETUP_FAILED", error instanceof Error ? error.message : "Failed to capture Stripe localhost webhook signing secret.");
|
|
@@ -1602,6 +2058,10 @@ async function main() {
|
|
|
1602
2058
|
ok: true,
|
|
1603
2059
|
commands: [
|
|
1604
2060
|
"read-setup-state",
|
|
2061
|
+
"base check-supabase-access-token",
|
|
2062
|
+
"base create-supabase-project",
|
|
2063
|
+
"base link-supabase-project",
|
|
2064
|
+
"base preflight-migration-state",
|
|
1605
2065
|
"admin ensure-admin",
|
|
1606
2066
|
"storage check-supabase-context",
|
|
1607
2067
|
"storage setup-supabase",
|
|
@@ -1619,7 +2079,6 @@ async function main() {
|
|
|
1619
2079
|
"configure-site-redirects",
|
|
1620
2080
|
"configure-email-password",
|
|
1621
2081
|
"enable-google-provider",
|
|
1622
|
-
"apply-auth-templates",
|
|
1623
2082
|
],
|
|
1624
2083
|
});
|
|
1625
2084
|
return;
|
|
@@ -1632,6 +2091,14 @@ async function main() {
|
|
|
1632
2091
|
}
|
|
1633
2092
|
if (command === "read-setup-state")
|
|
1634
2093
|
return readSetupState();
|
|
2094
|
+
if (command === "base" && subcommand === "check-supabase-access-token")
|
|
2095
|
+
return checkBaseSupabaseAccessToken();
|
|
2096
|
+
if (command === "base" && subcommand === "create-supabase-project")
|
|
2097
|
+
return createBaseSupabaseProject(flags);
|
|
2098
|
+
if (command === "base" && subcommand === "link-supabase-project")
|
|
2099
|
+
return linkBaseSupabaseProject(flags);
|
|
2100
|
+
if (command === "base" && subcommand === "preflight-migration-state")
|
|
2101
|
+
return preflightBaseMigrationState(flags);
|
|
1635
2102
|
if (command === "admin" && subcommand === "ensure-admin")
|
|
1636
2103
|
return ensureAdmin(flags);
|
|
1637
2104
|
if (command === "storage" && subcommand === "check-supabase-context")
|
|
@@ -1666,8 +2133,6 @@ async function main() {
|
|
|
1666
2133
|
return configureEmailPassword(flags);
|
|
1667
2134
|
if (command === "enable-google-provider")
|
|
1668
2135
|
return enableGoogleProvider(flags);
|
|
1669
|
-
if (command === "apply-auth-templates")
|
|
1670
|
-
return applyAuthTemplates(flags);
|
|
1671
2136
|
fail("UNKNOWN_COMMAND", `Unknown command: ${command}`);
|
|
1672
2137
|
}
|
|
1673
2138
|
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,
|