@vibecodemax/cli 0.1.10 → 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 +434 -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,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"] };
|
|
435
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}.`);
|
|
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
|
});
|
|
@@ -1307,12 +1640,27 @@ function checkStripeCli() {
|
|
|
1307
1640
|
}
|
|
1308
1641
|
function loginStripeCli(flags) {
|
|
1309
1642
|
const { values } = loadLocalEnv();
|
|
1310
|
-
|
|
1643
|
+
const cwd = process.cwd();
|
|
1644
|
+
ensureStripeCliInstalled(cwd);
|
|
1311
1645
|
const authMode = readStringFlag(flags, "auth-mode") === "api_key" ? "api_key" : "tty";
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
+
}
|
|
1316
1664
|
printJson({
|
|
1317
1665
|
ok: true,
|
|
1318
1666
|
command: "payments login-stripe-cli",
|
|
@@ -1432,10 +1780,9 @@ async function setupStripeLocalhost(flags) {
|
|
|
1432
1780
|
}
|
|
1433
1781
|
ensureStripeCliInstalled(cwd);
|
|
1434
1782
|
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
1783
|
let signingSecret = "";
|
|
1437
1784
|
try {
|
|
1438
|
-
signingSecret = await waitForStripeListenSecret(
|
|
1785
|
+
signingSecret = await waitForStripeListenSecret(secretKey.trim(), localhostUrl, cwd);
|
|
1439
1786
|
}
|
|
1440
1787
|
catch (error) {
|
|
1441
1788
|
fail("STRIPE_LOCALHOST_SETUP_FAILED", error instanceof Error ? error.message : "Failed to capture Stripe localhost webhook signing secret.");
|
|
@@ -1602,6 +1949,9 @@ async function main() {
|
|
|
1602
1949
|
ok: true,
|
|
1603
1950
|
commands: [
|
|
1604
1951
|
"read-setup-state",
|
|
1952
|
+
"base check-supabase-access-token",
|
|
1953
|
+
"base create-supabase-project",
|
|
1954
|
+
"base link-supabase-project",
|
|
1605
1955
|
"admin ensure-admin",
|
|
1606
1956
|
"storage check-supabase-context",
|
|
1607
1957
|
"storage setup-supabase",
|
|
@@ -1619,7 +1969,6 @@ async function main() {
|
|
|
1619
1969
|
"configure-site-redirects",
|
|
1620
1970
|
"configure-email-password",
|
|
1621
1971
|
"enable-google-provider",
|
|
1622
|
-
"apply-auth-templates",
|
|
1623
1972
|
],
|
|
1624
1973
|
});
|
|
1625
1974
|
return;
|
|
@@ -1632,6 +1981,12 @@ async function main() {
|
|
|
1632
1981
|
}
|
|
1633
1982
|
if (command === "read-setup-state")
|
|
1634
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);
|
|
1635
1990
|
if (command === "admin" && subcommand === "ensure-admin")
|
|
1636
1991
|
return ensureAdmin(flags);
|
|
1637
1992
|
if (command === "storage" && subcommand === "check-supabase-context")
|
|
@@ -1666,8 +2021,6 @@ async function main() {
|
|
|
1666
2021
|
return configureEmailPassword(flags);
|
|
1667
2022
|
if (command === "enable-google-provider")
|
|
1668
2023
|
return enableGoogleProvider(flags);
|
|
1669
|
-
if (command === "apply-auth-templates")
|
|
1670
|
-
return applyAuthTemplates(flags);
|
|
1671
2024
|
fail("UNKNOWN_COMMAND", `Unknown command: ${command}`);
|
|
1672
2025
|
}
|
|
1673
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,
|