@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 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 includes `temporaryPassword`. Show that password to the user directly and do not forward it into MCP payloads.
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 endpoint = `${MANAGEMENT_API_BASE}/v1/projects/${projectRef}/config/auth`;
261
- const response = await fetch(endpoint, {
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
- async function applyAuthTemplates(flags) {
431
- const { envBootstrapPath, values } = loadLocalEnv();
432
- const projectRef = resolveProjectRef(flags, values);
433
- if (!projectRef) {
434
- fail("MISSING_PROJECT_REF", "SUPABASE_PROJECT_REF is missing. Add it to .env.bootstrap.local or ensure NEXT_PUBLIC_SUPABASE_URL is present in .env.local.");
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 desiredFields = buildTemplateDesiredFields({
438
- confirmEmailEnabled: readStringFlag(flags, "confirm-email-enabled") === "true",
439
- confirmEmailSubject: readStringFlag(flags, "confirm-email-subject"),
440
- confirmEmailPath: readStringFlag(flags, "confirm-email-path"),
441
- resetPasswordEnabled: readStringFlag(flags, "reset-password-enabled") === "true",
442
- resetPasswordSubject: readStringFlag(flags, "reset-password-subject"),
443
- resetPasswordPath: readStringFlag(flags, "reset-password-path"),
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
- const current = await managementRequest({ method: "GET", projectRef, token: accessToken });
446
- const currentAuthConfig = readAuthConfigEnvelope(current);
447
- const diff = diffTemplateFields(currentAuthConfig, desiredFields);
448
- if (!diff.noChanges) {
449
- await managementRequest({ method: "PATCH", projectRef, token: accessToken, body: diff.patch });
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: "apply-auth-templates",
748
+ command: "base link-supabase-project",
454
749
  projectRef,
455
- changedKeys: diff.changedKeys,
456
- appliedKeys: diff.changedKeys,
457
- noChanges: diff.noChanges,
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(command, cwd) {
1455
+ function waitForStripeListenSecret(secretKey, localhostUrl, cwd) {
1129
1456
  return new Promise((resolve, reject) => {
1130
- const child = spawn(process.env.SHELL || "/bin/zsh", ["-lc", command], {
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: process.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
- if (!response.ok) {
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
- ensureStripeCliInstalled(process.cwd());
1643
+ const cwd = process.cwd();
1644
+ ensureStripeCliInstalled(cwd);
1307
1645
  const authMode = readStringFlag(flags, "auth-mode") === "api_key" ? "api_key" : "tty";
1308
- const command = authMode === "api_key"
1309
- ? `stripe login --api-key ${shellQuote(requireEnvValue(values, "STRIPE_SECRET_KEY", ".env.local"))}`
1310
- : "stripe login";
1311
- runShellCommand(command, process.cwd());
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 created = await stripeRequest({
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(listenCommand, cwd);
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
- for (const bucket of [buckets.public, buckets.private]) {
370
- try {
371
- await s3Client.send(new PutBucketCorsCommand({
372
- Bucket: bucket,
373
- CORSConfiguration: BUCKET_CORS_CONFIGURATION,
374
- }));
375
- }
376
- catch (error) {
377
- const message = error instanceof Error ? error.message : `Failed to apply CORS to ${bucket}.`;
378
- fail("AWS_POLICY_APPLY_FAILED", message, 1, { bucket, awsCode: getAwsCode(error), awsStatus: getAwsStatus(error) });
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
- const corsResponse = await s3Client.send(new GetBucketCorsCommand({ Bucket: bucket }));
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: corsResponse.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 > 0;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodemax/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "VibeCodeMax CLI — local provider setup for bootstrap and project configuration",
5
5
  "type": "module",
6
6
  "bin": {