@timekast/factory 1.26.0 → 1.27.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.
- package/dist/commands/doctor.js +5 -4
- package/dist/commands/env.js +110 -24
- package/dist/commands/provision.js +24 -17
- package/dist/commands/ticket.js +487 -0
- package/dist/index.js +22 -0
- package/dist/lib/constants.js +133 -0
- package/dist/lib/env-file.js +239 -0
- package/dist/lib/env-wizard.js +110 -21
- package/dist/lib/initial-migration.js +1 -1
- package/dist/lib/package-json.js +66 -2
- package/dist/lib/public-env-keys.json +27 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* post-bootstrap, and that the patch channel for `src/` is EPIC 2 (agentic
|
|
14
14
|
* update, not yet available).
|
|
15
15
|
* - MISSING INITIAL MIGRATION: a derivative that ships a Drizzle schema and no
|
|
16
|
-
* generated `.sql`, so its `vercel-build` (`pnpm
|
|
17
|
-
*
|
|
16
|
+
* generated `.sql`, so its `vercel-build` (`pnpm build && pnpm db:migrate`)
|
|
17
|
+
* has nothing to apply and its first deploy never creates the tables.
|
|
18
|
+
* INFORMATIONAL — see below.
|
|
18
19
|
*
|
|
19
20
|
* No lockfile (pre-CLI repo) → delegate to the "run factory:update" advice, exit
|
|
20
21
|
* 0, never abort (same pattern as `status` / the §8 edge case).
|
|
@@ -65,8 +66,8 @@ export const A1_WARNING = 'Aviso de seguridad (A1): el boilerplate `src/` y sus
|
|
|
65
66
|
*/
|
|
66
67
|
export const MISSING_INITIAL_MIGRATION_NOTICE = `⚠ Falta la migración inicial: hay schema en \`${SCHEMA_DIR}/\` y ningún \`.sql\` en ` +
|
|
67
68
|
`\`${MIGRATIONS_DIR}/\`.\n` +
|
|
68
|
-
' El deploy corre `pnpm
|
|
69
|
-
'
|
|
69
|
+
' El deploy corre `pnpm build && pnpm db:migrate`, así que el primer deploy no crearía ' +
|
|
70
|
+
'ninguna tabla. Genera la migración con `pnpm db:generate` y commitéala antes del próximo ' +
|
|
70
71
|
'deploy.\n' +
|
|
71
72
|
' (Aviso informativo: `factory doctor` no bloquea — sale con éxito de todos modos.)';
|
|
72
73
|
/** Parse `doctor`'s argv slice into flags. Unknown flags are ignored. */
|
package/dist/commands/env.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { CLIError } from '../lib/cli-error.js';
|
|
16
|
-
import { PROVISION_OWNED, setEnvLine } from '../lib/env-file.js';
|
|
16
|
+
import { PER_ENV_MARKER, PER_ENV_REGISTRY_FILE, PROVISION_OWNED, addToPerEnvRegistry, findInlinePerEnvMarkers, findPerEnvMarkedKeys, listEnvKeys, readPerEnvRegistry, setEnvLine, } from '../lib/env-file.js';
|
|
17
17
|
import { applyDefaultOverrides, applyWizardAnswers, buildEnvReview, buildQuestionPlan, checkEnvCompleteness, generateVapidKeys, isQuestionActive, parseEnvBody, parseEnvExample, provisionEmailDefaults, } from '../lib/env-wizard.js';
|
|
18
18
|
import { defaultEnvPath, readVercelToken } from '../lib/provision-preflight.js';
|
|
19
19
|
import { readProvisionState } from '../lib/provision-state.js';
|
|
@@ -30,23 +30,40 @@ export const REDEPLOY_NOTICE = 'ℹ️ Las variables aplican en el próximo dep
|
|
|
30
30
|
export function parseEnvPushFlags(argv) {
|
|
31
31
|
return { dryRun: argv.includes('--dry-run') };
|
|
32
32
|
}
|
|
33
|
+
/** No extra exclusions — the common case (the derivative declared nothing). */
|
|
34
|
+
const NO_PER_ENV_KEYS = new Set();
|
|
33
35
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
36
|
+
* Read `.env.local` as text, or `''` when it is absent or unreadable.
|
|
37
|
+
*
|
|
38
|
+
* Fail-SAFE on purpose, and the polarity is the opposite of `readPerEnvRegistry`:
|
|
39
|
+
* this file is the source of VALUES, so an empty body means "there is nothing to
|
|
40
|
+
* push" — the harmless direction. The registry is the source of EXCLUSIONS, where
|
|
41
|
+
* an empty result would mean "push everything", so it throws instead. Do NOT
|
|
42
|
+
* copy this `catch` over there.
|
|
37
43
|
*/
|
|
38
|
-
|
|
39
|
-
const out = {};
|
|
44
|
+
function readEnvBody(filePath) {
|
|
40
45
|
if (!existsSync(filePath))
|
|
41
|
-
return
|
|
42
|
-
let text;
|
|
46
|
+
return '';
|
|
43
47
|
try {
|
|
44
|
-
|
|
48
|
+
return readFileSync(filePath, 'utf8');
|
|
45
49
|
}
|
|
46
50
|
catch {
|
|
47
|
-
return
|
|
51
|
+
return '';
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse an `.env.local` body into the product vars to push: skip comments/blanks,
|
|
56
|
+
* the provision-owned keys, the per-environment keys the derivative declared, and
|
|
57
|
+
* empty values. First-`=` split; surrounding quotes stripped (mirrors the rail
|
|
58
|
+
* reader in provision.ts).
|
|
59
|
+
*
|
|
60
|
+
* `perEnvKeys` only ever SUBTRACTS from the push — it is applied ALONGSIDE
|
|
61
|
+
* `PROVISION_OWNED` (a union), never instead of it, so a derivative cannot name a
|
|
62
|
+
* provision-owned key to opt it back in.
|
|
63
|
+
*/
|
|
64
|
+
export function parseProductVars(body, perEnvKeys = NO_PER_ENV_KEYS) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const line of body.split('\n')) {
|
|
50
67
|
const trimmed = line.trimStart();
|
|
51
68
|
if (trimmed === '' || trimmed.startsWith('#'))
|
|
52
69
|
continue;
|
|
@@ -54,7 +71,7 @@ export function readProductVars(filePath) {
|
|
|
54
71
|
if (eq === -1)
|
|
55
72
|
continue;
|
|
56
73
|
const key = line.slice(0, eq).trim();
|
|
57
|
-
if (key === '' || PROVISION_OWNED.has(key))
|
|
74
|
+
if (key === '' || PROVISION_OWNED.has(key) || perEnvKeys.has(key))
|
|
58
75
|
continue;
|
|
59
76
|
let value = line.slice(eq + 1).replace(/\s+$/, '');
|
|
60
77
|
if (value.length >= 2 &&
|
|
@@ -68,6 +85,10 @@ export function readProductVars(filePath) {
|
|
|
68
85
|
}
|
|
69
86
|
return out;
|
|
70
87
|
}
|
|
88
|
+
/** {@link parseProductVars} over the file at `filePath`. */
|
|
89
|
+
export function readProductVars(filePath, perEnvKeys = NO_PER_ENV_KEYS) {
|
|
90
|
+
return parseProductVars(readEnvBody(filePath), perEnvKeys);
|
|
91
|
+
}
|
|
71
92
|
/** Read `{ projectId, teamId }` from `.vercel/project.json` (written by provision). */
|
|
72
93
|
function readProjectLink(rootDir) {
|
|
73
94
|
const file = vercelProjectPath(rootDir);
|
|
@@ -90,13 +111,65 @@ function readProjectLink(rootDir) {
|
|
|
90
111
|
return { projectId, teamId };
|
|
91
112
|
}
|
|
92
113
|
export async function runEnvPush(flags, rootDir = process.cwd()) {
|
|
93
|
-
const
|
|
114
|
+
const body = readEnvBody(path.join(rootDir, '.env.local'));
|
|
115
|
+
// Resolve the derivative's per-environment declaration BEFORE anything can reach
|
|
116
|
+
// Vercel: a present-but-broken registry throws here and aborts the whole run
|
|
117
|
+
// (`--dry-run` included), instead of showing a plan as if nothing were protected.
|
|
118
|
+
const registry = readPerEnvRegistry(rootDir);
|
|
119
|
+
const marked = findPerEnvMarkedKeys(body);
|
|
120
|
+
const pendingAdds = marked.filter((key) => !registry.keys.includes(key));
|
|
121
|
+
// Reconciliation: a marker the registry lacks is APPENDED to it (add-only — see
|
|
122
|
+
// `addToPerEnvRegistry`). `--dry-run` reports it and writes nothing.
|
|
123
|
+
if (pendingAdds.length > 0 && !flags.dryRun) {
|
|
124
|
+
addToPerEnvRegistry(rootDir, registry, pendingAdds);
|
|
125
|
+
}
|
|
126
|
+
// Protected = registry ∪ marker ∪ PROVISION_OWNED. A union, never a replacement:
|
|
127
|
+
// the derivative's declaration can only SUBTRACT variables from the push.
|
|
128
|
+
const perEnvKeys = new Set([...registry.keys, ...marked]);
|
|
129
|
+
const productVars = parseProductVars(body, perEnvKeys);
|
|
94
130
|
const keys = Object.keys(productVars).sort();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
131
|
+
const envKeys = listEnvKeys(body);
|
|
132
|
+
const omittedProvisionOwned = envKeys.filter((key) => PROVISION_OWNED.has(key)).sort();
|
|
133
|
+
// Provision-owned wins the label, so a redundant declaration is listed ONCE.
|
|
134
|
+
const omittedPerEnv = envKeys
|
|
135
|
+
.filter((key) => perEnvKeys.has(key) && !PROVISION_OWNED.has(key))
|
|
136
|
+
.sort();
|
|
137
|
+
const declaredButAbsent = [...perEnvKeys].filter((key) => !envKeys.includes(key)).sort();
|
|
138
|
+
if (pendingAdds.length > 0) {
|
|
139
|
+
const names = pendingAdds.join(', ');
|
|
140
|
+
console.log(flags.dryRun
|
|
141
|
+
? `ℹ️ Agregaría a \`${PER_ENV_REGISTRY_FILE}\` por el marcador ` +
|
|
142
|
+
`\`${PER_ENV_MARKER}\`: ${names} (--dry-run: no escribí nada).`
|
|
143
|
+
: `✔ Agregué a \`${PER_ENV_REGISTRY_FILE}\` (marcador \`${PER_ENV_MARKER}\`): ${names}.`);
|
|
144
|
+
}
|
|
145
|
+
// 🔴 An INLINE marker ABORTS, it does not warn. Warning and pushing anyway shipped a value the
|
|
146
|
+
// code had ALREADY PROVEN corrupt: `parseProductVars` splits on the first `=` and strips no
|
|
147
|
+
// inline comment, so `KEY="x" # @per-env` uploads the literal `"x" # @per-env`. Aborting is
|
|
148
|
+
// exactly as loud as the warning — the dev sees the wrong form and fixes it — and it is the
|
|
149
|
+
// only option that also protects. Filtered to keys the registry does NOT already cover: for a
|
|
150
|
+
// protected one the diagnosis would be false (it does not go up at all), and a message that
|
|
151
|
+
// sometimes lies trains everyone to skip reading it.
|
|
152
|
+
const inlineMarked = findInlinePerEnvMarkers(body).filter((key) => !perEnvKeys.has(key));
|
|
153
|
+
if (inlineMarked.length > 0) {
|
|
154
|
+
throw new CLIError(`${inlineMarked.map((key) => `\`${key}\``).join(', ')} lleva \`${PER_ENV_MARKER}\` en la MISMA ` +
|
|
155
|
+
'línea de la asignación. Ahí no protege nada, y además el valor subiría con el comentario ' +
|
|
156
|
+
'pegado (a Vercel llegaría el texto literal, comillas incluidas).\n' +
|
|
157
|
+
` El marcador va en un comentario en la línea de ARRIBA (\`SK.md §7.2\`). Aborté el push.`);
|
|
158
|
+
}
|
|
159
|
+
for (const key of declaredButAbsent) {
|
|
160
|
+
console.log(`⚠ \`${key}\` está declarada por ambiente pero no aparece en \`.env.local\` ` +
|
|
161
|
+
'— revisa si es un typo: así no está protegiendo nada.');
|
|
99
162
|
}
|
|
163
|
+
/** Names + reason, NEVER values: the omitted vars are the secret-bearing ones. */
|
|
164
|
+
const printOmitted = () => {
|
|
165
|
+
if (omittedProvisionOwned.length === 0 && omittedPerEnv.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
console.log('\nOmitidas (no se envían):');
|
|
168
|
+
for (const key of omittedProvisionOwned)
|
|
169
|
+
console.log(` ${key} — provision-owned`);
|
|
170
|
+
for (const key of omittedPerEnv)
|
|
171
|
+
console.log(` ${key} — declarada por ambiente`);
|
|
172
|
+
};
|
|
100
173
|
const targets = ['production', 'preview'];
|
|
101
174
|
const plan = keys.map((key) => ({
|
|
102
175
|
key,
|
|
@@ -104,12 +177,23 @@ export async function runEnvPush(flags, rootDir = process.cwd()) {
|
|
|
104
177
|
target: targets,
|
|
105
178
|
}));
|
|
106
179
|
if (flags.dryRun) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
180
|
+
if (keys.length === 0) {
|
|
181
|
+
console.log('No hay variables de producto que subir en `.env.local`.');
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(`Plan (${plan.length} vars → production, preview):`);
|
|
185
|
+
for (const key of keys)
|
|
186
|
+
console.log(` ${key}`);
|
|
187
|
+
}
|
|
188
|
+
printOmitted();
|
|
110
189
|
console.log('\n(--dry-run: nada se envió a Vercel.)');
|
|
111
190
|
return;
|
|
112
191
|
}
|
|
192
|
+
if (keys.length === 0) {
|
|
193
|
+
console.log('No hay variables de producto que subir en `.env.local` ' +
|
|
194
|
+
'(todo es provision-owned, está declarado por ambiente, o está vacío).');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
113
197
|
const { projectId, teamId } = readProjectLink(rootDir);
|
|
114
198
|
const token = readVercelToken(defaultEnvPath());
|
|
115
199
|
if (!token) {
|
|
@@ -219,9 +303,11 @@ export async function runEnvWizardApply(flags, rootDir = process.cwd(), deps = {
|
|
|
219
303
|
}
|
|
220
304
|
/**
|
|
221
305
|
* `factory env wizard --review [--json]` — a secret-safe summary of the resolved
|
|
222
|
-
* `.env.local`. Non-secret values are shown; secret values are masked
|
|
223
|
-
*
|
|
224
|
-
*
|
|
306
|
+
* `.env.local`. Non-secret values are shown; secret values are masked down to their
|
|
307
|
+
* last four characters, enough to tell WHICH value is in place while rotating and
|
|
308
|
+
* useless on their own; empty ones are flagged. This is what the guided flow shows
|
|
309
|
+
* the dev at the end (the agent NEVER echoes a raw secret) — the dev opens
|
|
310
|
+
* `.env.local` to see the full values.
|
|
225
311
|
*/
|
|
226
312
|
export function runEnvWizardReview(flags, rootDir = process.cwd()) {
|
|
227
313
|
const localPath = path.join(rootDir, '.env.local');
|
|
@@ -252,7 +338,7 @@ export function runEnvWizardReview(flags, rootDir = process.cwd()) {
|
|
|
252
338
|
for (const w of warnings)
|
|
253
339
|
console.log(` ⚠ ${w}`);
|
|
254
340
|
}
|
|
255
|
-
console.log('\nAbre `.env.local` para ver los valores completos (
|
|
341
|
+
console.log('\nAbre `.env.local` para ver los valores completos (de un secreto solo se ven sus últimos 4).');
|
|
256
342
|
}
|
|
257
343
|
/**
|
|
258
344
|
* `factory env wizard --gen-vapid` — generate a VAPID keypair and write it to
|
|
@@ -1245,7 +1245,7 @@ export function writeE2eWorkflow(rootDir) {
|
|
|
1245
1245
|
* PROV-004 — the `github` step: build the git substrate, create the Vercel PROJECT,
|
|
1246
1246
|
* connect the GitHub repo (GitHub App gate), confirm `productionBranch: main`,
|
|
1247
1247
|
* write `.vercel/project.json`, and activate the E2E workflow. Migrations run in the
|
|
1248
|
-
* Vercel build (`vercel-build`: `pnpm
|
|
1248
|
+
* Vercel build (`vercel-build`: `pnpm build && pnpm db:migrate`), NOT a GitHub Actions
|
|
1249
1249
|
* workflow. HIGH-risk: every failure is recoverable via `--resume`.
|
|
1250
1250
|
*
|
|
1251
1251
|
* Order (asserted by the unit test): git substrate (rename → remote add → push
|
|
@@ -1334,7 +1334,7 @@ export async function provisionGithubVercel(rootDir, state, teamId, deps = {}) {
|
|
|
1334
1334
|
// the template is absent. It lands in the working tree and is committed below.
|
|
1335
1335
|
//
|
|
1336
1336
|
// Migrations are NOT a GitHub Actions workflow: they run in the Vercel build
|
|
1337
|
-
// (`vercel-build`: `pnpm
|
|
1337
|
+
// (`vercel-build`: `pnpm build && pnpm db:migrate`, shipped in package.json),
|
|
1338
1338
|
// so every deployment migrates ITS target's DB (production←main, preview←develop)
|
|
1339
1339
|
// before building — no migrate.yml, no Deploy Hook, no manual step.
|
|
1340
1340
|
const e2e = writeE2eWorkflow(rootDir);
|
|
@@ -1367,7 +1367,7 @@ export async function provisionGithubVercel(rootDir, state, teamId, deps = {}) {
|
|
|
1367
1367
|
* before provision activated this workflow, so it lives only in the working tree
|
|
1368
1368
|
* until committed here. (Migrations are not part of THIS commit, but they do have one:
|
|
1369
1369
|
* `ensureInitialMigration` commits the `0000` earlier in the run, with its own pathspec.
|
|
1370
|
-
* The Vercel build only APPLIES it — `vercel-build` runs `pnpm
|
|
1370
|
+
* The Vercel build only APPLIES it — `vercel-build` runs `pnpm build && pnpm db:migrate`,
|
|
1371
1371
|
* there is no migration workflow.) Idempotent — when a `--resume`
|
|
1372
1372
|
* finds it already committed, `git add` stages nothing and the commit/push is skipped.
|
|
1373
1373
|
*/
|
|
@@ -1418,7 +1418,7 @@ const INITIAL_MIGRATION_COMMIT = 'chore(db): add the initial database migration'
|
|
|
1418
1418
|
const INITIAL_MIGRATION_LABEL = 'Migración inicial de la base (0000 de Drizzle)';
|
|
1419
1419
|
/**
|
|
1420
1420
|
* The branch the step commits on and pushes: the one `vercel-build` runs
|
|
1421
|
-
* `pnpm
|
|
1421
|
+
* `pnpm build && pnpm db:migrate` from for PRODUCTION. Named, never inlined
|
|
1422
1422
|
* (CODING.md §5) — it appears in the checkout guard, in both pushes and in the
|
|
1423
1423
|
* remote confirmation, and those four have to mean the same branch.
|
|
1424
1424
|
*/
|
|
@@ -1567,7 +1567,7 @@ async function confirmMigrationOnRemote(rootDir, runGit) {
|
|
|
1567
1567
|
/**
|
|
1568
1568
|
* Generate the initial Drizzle migration, commit it with its OWN pathspec and get it to
|
|
1569
1569
|
* the remote BEFORE Vercel builds the tree (PROV-004 pushes `main`; `vercel-build` runs
|
|
1570
|
-
* `pnpm
|
|
1570
|
+
* `pnpm build && pnpm db:migrate`). Without this a fresh derivative deploys against an
|
|
1571
1571
|
* empty database.
|
|
1572
1572
|
*
|
|
1573
1573
|
* Order and its reasons:
|
|
@@ -1706,7 +1706,7 @@ export async function ensureInitialMigration(rootDir, evidence, deps = {}) {
|
|
|
1706
1706
|
return;
|
|
1707
1707
|
}
|
|
1708
1708
|
// Un `try` por push, y no uno compartido: `main` es el que le importa a producción
|
|
1709
|
-
// (`vercel-build` = `pnpm
|
|
1709
|
+
// (`vercel-build` = `pnpm build && pnpm db:migrate`) y `develop` puede fallar solo
|
|
1710
1710
|
// (non-fast-forward con un remoto divergido). Con un `try` compartido, esa falla de
|
|
1711
1711
|
// `develop` imprimía "no pude pushear la migración" y se saltaba la confirmación —
|
|
1712
1712
|
// un diagnóstico falso justo cuando `main` SÍ había llegado.
|
|
@@ -2233,7 +2233,7 @@ async function resolveDnsToken(route, envPath, headless) {
|
|
|
2233
2233
|
* PROV-006 — attach the client's DEFINITIVE domain to the Vercel project, create
|
|
2234
2234
|
* (or print, for `registrar`) its DNS record, poll Vercel until verified, and on
|
|
2235
2235
|
* success: retire the interim prod domain (with explicit confirmation), update
|
|
2236
|
-
* `EMAIL_FROM` + `
|
|
2236
|
+
* `EMAIL_FROM` + `NEXT_PUBLIC_APP_URL`, and register the domain in Resend.
|
|
2237
2237
|
*
|
|
2238
2238
|
* Standalone-safe: runnable as a PATCH over an existing provision (the `dns` step
|
|
2239
2239
|
* of PROV-005 already attached the interim domains) or inside the initial run. It
|
|
@@ -2246,7 +2246,7 @@ async function resolveDnsToken(route, envPath, headless) {
|
|
|
2246
2246
|
* - Resend DNS records (BOOT-003) → `ensureDnsRecord` create-or-PATCH (a rotated
|
|
2247
2247
|
* DKIM is PATCHed in place, never deleted); a Resend poll timeout is a WARNING
|
|
2248
2248
|
* (pending-verification, resumable), never a step failure.
|
|
2249
|
-
* - `
|
|
2249
|
+
* - `EMAIL_FROM`/`NEXT_PUBLIC_APP_URL` already correct → the upsert PATCH is omitted.
|
|
2250
2250
|
*
|
|
2251
2251
|
* On timeout the state is left `clientDomainStatus: pending-verification` (resumable);
|
|
2252
2252
|
* on success it is `live`.
|
|
@@ -2462,7 +2462,7 @@ async function deleteInterimCname(interim, envPath, fetchImpl) {
|
|
|
2462
2462
|
}
|
|
2463
2463
|
}
|
|
2464
2464
|
/**
|
|
2465
|
-
* Update `EMAIL_FROM` + `
|
|
2465
|
+
* Update `EMAIL_FROM` + `NEXT_PUBLIC_APP_URL` +
|
|
2466
2466
|
* `WEBAUTHN_RP_ID` to the client domain in the Vercel env, per-target and
|
|
2467
2467
|
* idempotently (PROV-006 §4 + BOOT-004). Lists the existing vars once and upserts
|
|
2468
2468
|
* each entry via `upsertEnvVar`, which reconciles a key that the substrate left
|
|
@@ -2483,20 +2483,27 @@ async function deleteInterimCname(interim, envPath, fetchImpl) {
|
|
|
2483
2483
|
* discipline holds).
|
|
2484
2484
|
*/
|
|
2485
2485
|
async function updateDomainEnv(projectId, token, teamId, domain, deps) {
|
|
2486
|
-
const
|
|
2486
|
+
const appOrigin = `https://${domain}`;
|
|
2487
2487
|
const plan = [
|
|
2488
2488
|
// EMAIL_FROM uses the dedicated mail subdomain (`updates.<domain>`), NOT the apex
|
|
2489
|
-
// — the apex carries the client's corporate mail.
|
|
2489
|
+
// — the apex carries the client's corporate mail.
|
|
2490
2490
|
{
|
|
2491
2491
|
key: 'EMAIL_FROM',
|
|
2492
2492
|
value: `noreply@${mailDomainFor(domain)}`,
|
|
2493
2493
|
target: ['production', 'preview'],
|
|
2494
2494
|
},
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
//
|
|
2495
|
+
// 🔴 AUTH_URL / NEXTAUTH_URL are DELIBERATELY ABSENT — do not re-add them.
|
|
2496
|
+
// `next-auth/lib/env.js` rewrites the origin of every auth request to their value,
|
|
2497
|
+
// so scoping either to `preview` pins all branch deployments' callbacks to this one
|
|
2498
|
+
// host and kills their sign-in. `trustHost` (auth.config.ts) already infers the
|
|
2499
|
+
// origin per request, which is correct on production AND on each preview.
|
|
2500
|
+
// Auth needs nothing here; the domain reaches the app through NEXT_PUBLIC_APP_URL
|
|
2501
|
+
// below, which is what `getAppUrl()` reads for email links (src/lib/env.ts).
|
|
2502
|
+
// A project that enables OAuth and must pin the callback to a fixed host sets it
|
|
2503
|
+
// manually, `production` only, knowing it forfeits preview sign-in.
|
|
2504
|
+
// Keep the public app origin pointing at the client domain once it lands
|
|
2498
2505
|
// (the substrate set it to the interim domain in buildEnvPlan; this re-points it).
|
|
2499
|
-
{ key: 'NEXT_PUBLIC_APP_URL', value:
|
|
2506
|
+
{ key: 'NEXT_PUBLIC_APP_URL', value: appOrigin, target: ['production', 'preview'] },
|
|
2500
2507
|
// WebAuthn rpID is the registrable HOST, never a URL — the bare domain (BOOT-004).
|
|
2501
2508
|
// PRODUCTION only: the interim dev domain remains the preview rpID (the dev domain
|
|
2502
2509
|
// is never retired), so the intentional split pair from buildEnvPlan is preserved.
|
|
@@ -2532,7 +2539,7 @@ function printClientDomainPlan(domain, interim, route, confirm) {
|
|
|
2532
2539
|
console.log(` … sondea GET /v9/projects/.../domains/${domain} hasta verified:true (o timeout)`);
|
|
2533
2540
|
console.log(` ${confirm ? '-' : '? (requiere --confirm o prompt)'} retira el interino ${interim} ` +
|
|
2534
2541
|
`(el dev ${domain}-dev se conserva)`);
|
|
2535
|
-
console.log(` ↻ EMAIL_FROM=noreply@${mailDomainFor(domain)} ·
|
|
2542
|
+
console.log(` ↻ EMAIL_FROM=noreply@${mailDomainFor(domain)} · NEXT_PUBLIC_APP_URL=https://${domain}`);
|
|
2536
2543
|
console.log(` ↻ WEBAUTHN_RP_ID=${domain} (solo production — las passkeys del rpID interino quedan invalidadas; el preview conserva el rpID dev)`);
|
|
2537
2544
|
console.log(` + Resend: POST /domains { name: ${mailDomainFor(domain)} } (subdominio mail, no el apex)`);
|
|
2538
2545
|
if (route === 'registrar') {
|
|
@@ -3688,7 +3695,7 @@ export async function runProvision(flags) {
|
|
|
3688
3695
|
writeProvisionState(rootDir, runState);
|
|
3689
3696
|
// 5.0. Initial migration (GAP-001) — BEFORE the steps, so the `.sql` is committed
|
|
3690
3697
|
// before the `github` step pushes `main` and Vercel builds the remote tree
|
|
3691
|
-
// (`vercel-build` = `pnpm
|
|
3698
|
+
// (`vercel-build` = `pnpm build && pnpm db:migrate`). A failed generate/install
|
|
3692
3699
|
// aborts here, with nothing pushed and nothing deployed.
|
|
3693
3700
|
await ensureInitialMigration(rootDir, greenfield);
|
|
3694
3701
|
for (const step of steps) {
|