@timekast/factory 1.15.0 → 1.16.0

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.
@@ -28,7 +28,7 @@ import { CLIError } from '../lib/cli-error.js';
28
28
  import { apexZoneOf, CloudflareApiError, CLOUDFLARE_API_BASE, ensureCnameRecord, findCnameRecord, resolveZoneId, resolveZoneIdForName, CNAME_VERCEL_TARGET, TIMEKAST_ZONE_NAME, provisionDomains, } from '../lib/cloudflare-api.js';
29
29
  import { createDevelopBranch, createNeonProject, createProjectApiKey, deleteNeonApiKeysByName, deleteNeonProject, getConnectionUri, listNeonBranches, listNeonProjects, NeonApiError, NEON_API_BASE, NEON_DEVELOP_BRANCH, } from '../lib/neon-api.js';
30
30
  import { defaultEnvPath, readVercelToken, runProvisionPreflight, } from '../lib/provision-preflight.js';
31
- import { addVercelDomain, buildEnvPlan, createVercelProject, generateAuthSecret, getLinkedRepoId, getVercelDomainStatus, GitHubAppNotInstalledError, linkGitRepo, listVercelProjects, patchProjectSettings, pushEnvPlan, removeVercelDomain, triggerDeployment, upsertEnvVar, listEnvVars, VercelApiError, writeVercelProjectJson, } from '../lib/vercel-env.js';
31
+ import { addVercelDomain, buildEnvPlan, createVercelProject, generateAuthSecret, generateMfaEncryptionKey, getLinkedRepoId, getVercelDomainStatus, GitHubAppNotInstalledError, linkGitRepo, listVercelProjects, patchProjectSettings, pushEnvPlan, removeVercelDomain, triggerDeployment, upsertEnvVar, listEnvVars, VercelApiError, writeVercelProjectJson, } from '../lib/vercel-env.js';
32
32
  import { writeEnvLocal } from '../lib/env-file.js';
33
33
  import { adoptState, ADOPT_UNKNOWN, clearProvisionState, clearState, hasProvisionState, isServiceComplete, PROVISION_DIR, PROVISION_STATE_FILE, readDestroyTarget, readProvisionState, writeProvisionState, } from '../lib/provision-state.js';
34
34
  /**
@@ -652,11 +652,21 @@ export async function provisionVercel(rootDir, state, teamId, deps = {}) {
652
652
  writeVercelProjectJson(rootDir, { projectId, orgId: teamId, teamId }, { force: deps.force });
653
653
  // Mint an INDEPENDENT AUTH_SECRET per environment (issue §4 — never shared).
654
654
  // POOLED strings → the Vercel runtime DATABASE_URL (serverless/concurrent).
655
+ // WebAuthn rpID = the EXACT host per env (provision's interim domains). A passkey
656
+ // dies if the rpID changes, so a later custom-domain swap (--domain / PROV-006)
657
+ // must also re-point WEBAUTHN_RP_ID — tracked as a follow-up. MFA_ENCRYPTION_KEY
658
+ // is minted independently per env (base64url) so a derivative with MFA enabled
659
+ // never boots broken (P-1).
660
+ const rpDomains = provisionDomains(state.projectName);
655
661
  const plan = buildEnvPlan({
656
662
  mainDatabaseUrl: neonSecrets.mainPooled,
657
663
  developDatabaseUrl: neonSecrets.developPooled,
658
664
  authSecretProd: generateAuthSecret(),
659
665
  authSecretPreview: generateAuthSecret(),
666
+ mfaEncryptionKeyProd: generateMfaEncryptionKey(),
667
+ mfaEncryptionKeyPreview: generateMfaEncryptionKey(),
668
+ webauthnRpIdProd: rpDomains.prod,
669
+ webauthnRpIdPreview: rpDomains.dev,
660
670
  shared: readSharedVercelEnv(envPath),
661
671
  });
662
672
  let result;
@@ -25,6 +25,11 @@ export const PROVISION_OWNED = new Set([
25
25
  'DATABASE_URL_MAIN',
26
26
  'AUTH_SECRET',
27
27
  'NEXT_PUBLIC_APP_ENV',
28
+ // Provision mints these per-env too (P-1): the MFA key (base64url) + the
29
+ // WebAuthn rpID (the exact host). A shared day-2 push would break passkeys
30
+ // (rpID must match the env's host) or the at-rest TOTP crypto.
31
+ 'MFA_ENCRYPTION_KEY',
32
+ 'WEBAUTHN_RP_ID',
28
33
  ]);
29
34
  /**
30
35
  * Replace the value of `KEY=...` in an env-file body, preserving every other line.
@@ -369,6 +369,17 @@ export async function upsertEnvVar(projectId, token, teamId, plan, existing, dep
369
369
  export function generateAuthSecret() {
370
370
  return randomBytes(32).toString('hex');
371
371
  }
372
+ /**
373
+ * Generate a fresh `MFA_ENCRYPTION_KEY` — the AES-256 key the runtime uses to
374
+ * encrypt TOTP secrets at rest (`src/lib/auth/totp-crypto.ts`). It MUST be
375
+ * base64url of EXACTLY 32 bytes — NOT hex like `AUTH_SECRET` (the runtime rejects
376
+ * a wrong-length/format key). Minted independently per environment so prod and
377
+ * preview never share it. Without this var a derivative with MFA enabled boots
378
+ * broken (P-1).
379
+ */
380
+ export function generateMfaEncryptionKey() {
381
+ return randomBytes(32).toString('base64url');
382
+ }
372
383
  /** Absolute path to `.vercel/project.json` under a repo root. */
373
384
  export function vercelProjectPath(rootDir) {
374
385
  return path.join(rootDir, VERCEL_DIR, VERCEL_PROJECT_FILE);
@@ -447,6 +458,15 @@ export function buildEnvPlan(input) {
447
458
  // Auth secret — distinct value per environment (never shared).
448
459
  { key: 'AUTH_SECRET', value: input.authSecretProd, target: ['production'] },
449
460
  { key: 'AUTH_SECRET', value: input.authSecretPreview, target: ['preview'] },
461
+ // MFA encryption key — distinct per env (base64url 32 bytes). Without it a
462
+ // derivative with MFA enabled boots broken; minted independently so prod and
463
+ // preview never share it (P-1).
464
+ { key: 'MFA_ENCRYPTION_KEY', value: input.mfaEncryptionKeyProd, target: ['production'] },
465
+ { key: 'MFA_ENCRYPTION_KEY', value: input.mfaEncryptionKeyPreview, target: ['preview'] },
466
+ // WebAuthn relying-party id — the EXACT host per env (a passkey dies if the
467
+ // rpID changes), so passkeys work without manual Vercel setup (P-1 / PASSKEY-002 §8).
468
+ { key: 'WEBAUTHN_RP_ID', value: input.webauthnRpIdProd, target: ['production'] },
469
+ { key: 'WEBAUTHN_RP_ID', value: input.webauthnRpIdPreview, target: ['preview'] },
450
470
  // Deployment identity — drives the env-aware PWA manifest (see .env.example).
451
471
  { key: 'NEXT_PUBLIC_APP_ENV', value: 'production', target: ['production'] },
452
472
  { key: 'NEXT_PUBLIC_APP_ENV', value: 'preview', target: ['preview'] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timekast/factory",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Public, thin CLI to bootstrap and maintain TimeKast Factory derived projects.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",