@timekast/factory 2.2.0 → 2.3.1
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/provision.js +132 -62
- package/dist/lib/vault-api.js +4 -1
- package/package.json +1 -1
|
@@ -63,7 +63,8 @@ export const E2E_DATABASE_URL_SECRET = 'DATABASE_URL';
|
|
|
63
63
|
* `${MAIL_SUBDOMAIN}.<base>` — NEVER the apex `<base>`, which holds the org's (or a
|
|
64
64
|
* client's) corporate MX/SPF (Google Workspace, M365, …). Verifying Resend on the
|
|
65
65
|
* apex would collide with that mail; an isolated subdomain keeps both working. Same
|
|
66
|
-
* convention for the org (`updates.timekast.
|
|
66
|
+
* convention for the org (`updates.timekast.com`) and, with `--client-mail`, a client
|
|
67
|
+
* (`updates.<client>`).
|
|
67
68
|
*/
|
|
68
69
|
export const MAIL_SUBDOMAIN = 'updates';
|
|
69
70
|
/** Build the transactional-mail domain for a base domain: `updates.<base>`. */
|
|
@@ -72,10 +73,11 @@ export function mailDomainFor(base) {
|
|
|
72
73
|
}
|
|
73
74
|
/**
|
|
74
75
|
* The canonical, ORG-LEVEL sender for transactional mail on provisioned stacks
|
|
75
|
-
* (issue §4 + §5). Sends from `noreply@updates.timekast.
|
|
76
|
-
* verified in Resend, NOT the apex
|
|
77
|
-
*
|
|
78
|
-
* `updates.<client>`
|
|
76
|
+
* (issue §4 + §5). Sends from `noreply@updates.timekast.com` — the dedicated subdomain
|
|
77
|
+
* verified in Resend, NOT the apex (which is on Google Workspace). It stays the sender
|
|
78
|
+
* after the `--domain` go-live (PROV-006): the Resend account has no room for a domain
|
|
79
|
+
* per client, so the client's own `updates.<client>` is opt-in (`--client-mail`). The
|
|
80
|
+
* unit test pins this value.
|
|
79
81
|
*/
|
|
80
82
|
export const PROVISION_EMAIL_FROM = `noreply@${mailDomainFor(TIMEKAST_ZONE_NAME)}`;
|
|
81
83
|
// `provisionDomains` moved to `cloudflare-api.ts` (next to TIMEKAST_ZONE_NAME) so
|
|
@@ -250,6 +252,11 @@ export function parseProvisionFlags(argv) {
|
|
|
250
252
|
throw new CLIError('`--no-vault` y `--services=…vault…` se contradicen: uno pide omitir la bóveda y el otro ' +
|
|
251
253
|
'crearla. Quita uno de los dos.');
|
|
252
254
|
}
|
|
255
|
+
const clientMail = argv.includes('--client-mail');
|
|
256
|
+
const domain = readValueFlag(argv, 'domain');
|
|
257
|
+
if (clientMail && !domain) {
|
|
258
|
+
throw new CLIError('`--client-mail` solo aplica al go-live: pásalo junto con `--domain=<dominio del cliente>`.');
|
|
259
|
+
}
|
|
253
260
|
return {
|
|
254
261
|
services,
|
|
255
262
|
noVault,
|
|
@@ -259,7 +266,8 @@ export function parseProvisionFlags(argv) {
|
|
|
259
266
|
remint: argv.includes('--remint'),
|
|
260
267
|
slug: readValueFlag(argv, 'slug'),
|
|
261
268
|
repo: parseRepoFlag(readValueFlag(argv, 'repo')),
|
|
262
|
-
domain
|
|
269
|
+
domain,
|
|
270
|
+
clientMail,
|
|
263
271
|
dns: parseDnsRoute(readValueFlag(argv, 'dns')),
|
|
264
272
|
verifyTimeout: parseVerifyTimeout(readValueFlag(argv, 'verify-timeout')),
|
|
265
273
|
confirm: argv.includes('--confirm'),
|
|
@@ -1368,7 +1376,7 @@ const VAULT_PER_ENV_MINTED = ['AUTH_SECRET'];
|
|
|
1368
1376
|
* Which keys each destination gets depends on the services the run includes: `neon` writes the
|
|
1369
1377
|
* connection strings and the CI keys, `vercel` the runtime values, `backlog` its local creds.
|
|
1370
1378
|
*/
|
|
1371
|
-
export function buildVaultPlan(projectName, services) {
|
|
1379
|
+
export function buildVaultPlan(projectName, services, target) {
|
|
1372
1380
|
const keys = {};
|
|
1373
1381
|
const references = {};
|
|
1374
1382
|
const add = (loc, names, asRef = false) => {
|
|
@@ -1408,7 +1416,9 @@ export function buildVaultPlan(projectName, services) {
|
|
|
1408
1416
|
if (services.includes('backlog')) {
|
|
1409
1417
|
add(VAULT_LOCAL_ROOT, ['BACKLOG_API_KEY', 'BACKLOG_PROJECT_ID']);
|
|
1410
1418
|
}
|
|
1411
|
-
|
|
1419
|
+
// The syncs follow the DESTINATION, not this run's steps: a run with `vault` but without the
|
|
1420
|
+
// `railway` step still deploys to Railway, and never gets the Vercel syncs.
|
|
1421
|
+
const railway = services.includes('railway') || target === 'railway';
|
|
1412
1422
|
return {
|
|
1413
1423
|
project: projectName,
|
|
1414
1424
|
environments: VAULT_LAYOUT_ENVIRONMENTS.map((env) => env.slug),
|
|
@@ -1923,12 +1933,15 @@ const RESEND_KEY_RESUME_HINT = 'repite la corrida con `--resume` y `dns` en `--s
|
|
|
1923
1933
|
'hasta publicarla';
|
|
1924
1934
|
/**
|
|
1925
1935
|
* NO-vault: the sending domain a fresh project key is limited to — the client's mail subdomain
|
|
1926
|
-
* once
|
|
1927
|
-
*
|
|
1928
|
-
* back to the
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1936
|
+
* once a `--client-mail` go-live is recorded (Resend already knows it: that go-live registers
|
|
1937
|
+
* it), else the org domain. A `--resume` that retries the key after such a go-live must not move
|
|
1938
|
+
* the mail back to the org domain. `sharedMail` marks a go-live that kept the org sender; a
|
|
1939
|
+
* `live` state without it predates the flag, when every go-live moved the mail.
|
|
1940
|
+
*/
|
|
1941
|
+
export function noVaultMintDomain(state) {
|
|
1942
|
+
const client = state.dns?.clientDomainStatus === 'live' && state.dns.sharedMail !== true
|
|
1943
|
+
? state.dns.clientDomain
|
|
1944
|
+
: undefined;
|
|
1932
1945
|
return client ? mailDomainFor(client) : sendingDomainOf(PROVISION_EMAIL_FROM);
|
|
1933
1946
|
}
|
|
1934
1947
|
/**
|
|
@@ -4711,8 +4724,9 @@ async function resolveDnsToken(route, rail, headless, rootDir) {
|
|
|
4711
4724
|
/**
|
|
4712
4725
|
* PROV-006 — attach the client's DEFINITIVE domain to the Vercel project, create
|
|
4713
4726
|
* (or print, for `registrar`) its DNS record, poll Vercel until verified, and on
|
|
4714
|
-
* success: retire the interim prod domain (with explicit confirmation)
|
|
4715
|
-
* `
|
|
4727
|
+
* success: retire the interim prod domain (with explicit confirmation) and update
|
|
4728
|
+
* `NEXT_PUBLIC_APP_URL`. The mail stays on the org sender unless `deps.clientMail`
|
|
4729
|
+
* (`--client-mail`): then `EMAIL_FROM` moves to `updates.<domain>`, registered in Resend.
|
|
4716
4730
|
*
|
|
4717
4731
|
* Standalone-safe: runnable as a PATCH over an existing provision (the `dns` step
|
|
4718
4732
|
* of PROV-005 already attached the interim domains) or inside the initial run. It
|
|
@@ -4745,6 +4759,7 @@ export async function provisionClientDomain(rootDir, state, teamId, domain, deps
|
|
|
4745
4759
|
'Corre el paso de Vercel/GitHub primero (`--services=github,...`) y reintenta con `--resume`.');
|
|
4746
4760
|
}
|
|
4747
4761
|
const route = deps.dnsRoute ?? 'org';
|
|
4762
|
+
const clientMail = deps.clientMail === true;
|
|
4748
4763
|
const rail = deps.rail ?? defaultRail();
|
|
4749
4764
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4750
4765
|
const vercelToken = await readVercelToken(rail);
|
|
@@ -4823,27 +4838,32 @@ export async function provisionClientDomain(rootDir, state, teamId, domain, deps
|
|
|
4823
4838
|
// 5. Update EMAIL_FROM + NEXT_PUBLIC_APP_URL + the WebAuthn rpID/related origins in Vercel env
|
|
4824
4839
|
// (idempotent — §8), and in the vault's `main:/` when the repo has one (VAULT-002).
|
|
4825
4840
|
const hosts = existingProvisionDomains(state.projectName, state.dns);
|
|
4826
|
-
await updateDomainEnv(projectId, vercelToken, teamId, domain, vercelDeps, { dev: hosts.dev, interim: hosts.prod, interimRetired }, deps.vault ?? null);
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4841
|
+
await updateDomainEnv(projectId, vercelToken, teamId, domain, vercelDeps, { dev: hosts.dev, interim: hosts.prod, interimRetired }, deps.vault ?? null, clientMail);
|
|
4842
|
+
if (clientMail) {
|
|
4843
|
+
// 6. Register the client's MAIL SUBDOMAIN (`updates.<domain>`) in Resend — NOT the
|
|
4844
|
+
// apex, which holds the client's corporate MX/SPF (Google Workspace, M365, …).
|
|
4845
|
+
// Verifying Resend on a dedicated subdomain keeps the client's own mail intact.
|
|
4846
|
+
// BOOT-003: also CREATES the DKIM/SPF/MX records Resend returns (in the same
|
|
4847
|
+
// client zone as the CNAME) and polls until Resend reports `verified`.
|
|
4848
|
+
await registerResendDomain(mailDomainFor(domain), rail, deps, fetchImpl, resendDns);
|
|
4849
|
+
// 6b. VAULT-002 — the project's Resend key is limited to its sending domain, which just
|
|
4850
|
+
// changed: re-scope it to `updates.<domain>` (vault, when there is one, + deploys) and
|
|
4851
|
+
// revoke the old one once the new one reached them. Without a vault too: the deploys
|
|
4852
|
+
// carry the project's key there as well (never the rail's team key).
|
|
4853
|
+
await rescopeProjectResendKey(rootDir, state, deps.vault ?? null, mailDomainFor(domain), { projectId, token: vercelToken, teamId, deps: vercelDeps }, rail, fetchImpl, { sleep: deps.sleep, now: deps.now });
|
|
4854
|
+
}
|
|
4855
|
+
else {
|
|
4856
|
+
logSharedMail();
|
|
4857
|
+
}
|
|
4838
4858
|
// 7. Record the client domain as LIVE and persist.
|
|
4839
|
-
state.dns = {
|
|
4859
|
+
state.dns = withMailChoice({
|
|
4840
4860
|
...(state.dns ?? {
|
|
4841
4861
|
domain: provisionDomains(state.projectName).prod,
|
|
4842
4862
|
devDomain: provisionDomains(state.projectName).dev,
|
|
4843
4863
|
}),
|
|
4844
4864
|
clientDomain: domain,
|
|
4845
4865
|
clientDomainStatus: 'live',
|
|
4846
|
-
};
|
|
4866
|
+
}, clientMail);
|
|
4847
4867
|
writeProvisionState(rootDir, state);
|
|
4848
4868
|
}
|
|
4849
4869
|
/** RWAY-005 — the two records the owner creates by hand for `--dns=registrar` on Railway. */
|
|
@@ -4890,8 +4910,9 @@ async function pollRailwayDomainVerification(client, scope, dns, deps) {
|
|
|
4890
4910
|
* only PRINTS both.
|
|
4891
4911
|
* 3. Poll the domain's state in Railway (`--verify-timeout`); timeout → `pending-verification`.
|
|
4892
4912
|
* 4. Retire the interim `{slug}` from Railway with the same confirmation (`--confirm` headless).
|
|
4893
|
-
* 5. `
|
|
4894
|
-
* vault ONLY (its sync is Railway's only writer); Resend
|
|
4913
|
+
* 5. `NEXT_PUBLIC_APP_URL`, `WEBAUTHN_RP_ID`/`_RELATED_ORIGINS` (+ `EMAIL_FROM` with
|
|
4914
|
+
* `--client-mail`) → `main:/` of the vault ONLY (its sync is Railway's only writer); Resend
|
|
4915
|
+
* as on Vercel (only with `--client-mail`).
|
|
4895
4916
|
* 🔴 No request to Vercel. Requires the vault and the `railway` block (none → `--adopt`).
|
|
4896
4917
|
*/
|
|
4897
4918
|
async function provisionRailwayClientDomain(rootDir, state, domain, deps) {
|
|
@@ -4901,6 +4922,7 @@ async function provisionRailwayClientDomain(rootDir, state, domain, deps) {
|
|
|
4901
4922
|
assertRailwayDomainIds(state);
|
|
4902
4923
|
const rw = state.railway;
|
|
4903
4924
|
const route = deps.dnsRoute ?? 'org';
|
|
4925
|
+
const clientMail = deps.clientMail === true;
|
|
4904
4926
|
const rail = deps.rail ?? defaultRail();
|
|
4905
4927
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4906
4928
|
const client = railwayClientFor(rail, deps.railway);
|
|
@@ -4987,15 +5009,21 @@ async function provisionRailwayClientDomain(rootDir, state, domain, deps) {
|
|
|
4987
5009
|
dev: hosts.dev,
|
|
4988
5010
|
interim: hosts.prod,
|
|
4989
5011
|
interimRetired,
|
|
4990
|
-
});
|
|
4991
|
-
// 6. Resend — unchanged by the destination (mail subdomain + the project's key re-scoped)
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
5012
|
+
}, clientMail);
|
|
5013
|
+
// 6. Resend — unchanged by the destination (mail subdomain + the project's key re-scoped),
|
|
5014
|
+
// and only with `--client-mail`.
|
|
5015
|
+
if (clientMail) {
|
|
5016
|
+
await registerResendDomain(mailDomainFor(domain), rail, paced, fetchImpl, resendDns);
|
|
5017
|
+
await rescopeProjectResendKey(rootDir, state, vault, mailDomainFor(domain), null, rail, fetchImpl, {
|
|
5018
|
+
sleep: paced.sleep,
|
|
5019
|
+
now: paced.now,
|
|
5020
|
+
});
|
|
5021
|
+
}
|
|
5022
|
+
else {
|
|
5023
|
+
logSharedMail();
|
|
5024
|
+
}
|
|
4997
5025
|
// 7. Live.
|
|
4998
|
-
state.dns = { ...dnsBlock(), clientDomainStatus: 'live' };
|
|
5026
|
+
state.dns = withMailChoice({ ...dnsBlock(), clientDomainStatus: 'live' }, clientMail);
|
|
4999
5027
|
writeProvisionState(rootDir, state);
|
|
5000
5028
|
}
|
|
5001
5029
|
/**
|
|
@@ -5125,12 +5153,15 @@ async function deleteInterimCname(interim, rail, fetchImpl) {
|
|
|
5125
5153
|
*
|
|
5126
5154
|
* No-echo: only KEY NAMES + targets print (the values here are public, but the discipline holds).
|
|
5127
5155
|
*/
|
|
5128
|
-
export async function updateDomainEnv(projectId, token, teamId, domain, deps, hosts, vault = null) {
|
|
5156
|
+
export async function updateDomainEnv(projectId, token, teamId, domain, deps, hosts, vault = null, clientMail = false) {
|
|
5129
5157
|
const { appOrigin, emailFrom, relatedOrigins } = domainEnvValues(domain, hosts);
|
|
5130
5158
|
const plan = [
|
|
5131
|
-
// EMAIL_FROM
|
|
5132
|
-
// — the apex carries the client's corporate mail.
|
|
5133
|
-
|
|
5159
|
+
// EMAIL_FROM only with `--client-mail`: the dedicated mail subdomain (`updates.<domain>`),
|
|
5160
|
+
// NOT the apex — the apex carries the client's corporate mail. Without it the sender
|
|
5161
|
+
// already in place (the org's, or one a team set by hand) is left alone.
|
|
5162
|
+
...(clientMail
|
|
5163
|
+
? [{ key: 'EMAIL_FROM', value: emailFrom, target: ['production', 'preview'] }]
|
|
5164
|
+
: []),
|
|
5134
5165
|
// 🔴 AUTH_URL / NEXTAUTH_URL are DELIBERATELY ABSENT — do not re-add them.
|
|
5135
5166
|
// `next-auth/lib/env.js` rewrites the origin of every auth request to their value,
|
|
5136
5167
|
// so scoping either to `preview` pins all branch deployments' callbacks to this one
|
|
@@ -5169,7 +5200,7 @@ export async function updateDomainEnv(projectId, token, teamId, domain, deps, ho
|
|
|
5169
5200
|
}
|
|
5170
5201
|
}
|
|
5171
5202
|
if (vault) {
|
|
5172
|
-
await writeDomainEnvToVault(vault, domain, hosts);
|
|
5203
|
+
await writeDomainEnvToVault(vault, domain, hosts, clientMail);
|
|
5173
5204
|
}
|
|
5174
5205
|
else {
|
|
5175
5206
|
warnRpIdMoved(domain);
|
|
@@ -5191,10 +5222,10 @@ function domainEnvValues(domain, hosts) {
|
|
|
5191
5222
|
* reference for the shared ones; the syncs deliver them to production (Vercel or Railway, SYNC-001).
|
|
5192
5223
|
* `NEXT_PUBLIC_APP_URL` goes to `main:/` only: `develop:/` keeps develop's origin.
|
|
5193
5224
|
*/
|
|
5194
|
-
async function writeDomainEnvToVault(vault, domain, hosts) {
|
|
5225
|
+
async function writeDomainEnvToVault(vault, domain, hosts, clientMail) {
|
|
5195
5226
|
const { appOrigin, emailFrom, relatedOrigins } = domainEnvValues(domain, hosts);
|
|
5196
5227
|
await vaultWrite(vault, VAULT_MAIN_ROOT, {
|
|
5197
|
-
EMAIL_FROM: emailFrom,
|
|
5228
|
+
...(clientMail ? { EMAIL_FROM: emailFrom } : {}),
|
|
5198
5229
|
NEXT_PUBLIC_APP_URL: appOrigin,
|
|
5199
5230
|
WEBAUTHN_RP_ID: domain,
|
|
5200
5231
|
WEBAUTHN_RELATED_ORIGINS: relatedOrigins,
|
|
@@ -5211,9 +5242,22 @@ function warnRpIdMoved(domain) {
|
|
|
5211
5242
|
'bajo el rpID interino queda invalidada — esos usuarios deben registrar una nueva ' +
|
|
5212
5243
|
'passkey en el dominio definitivo.');
|
|
5213
5244
|
}
|
|
5245
|
+
/** The go-live without `--client-mail`: say where the mail stays and how to move it. */
|
|
5246
|
+
function logSharedMail() {
|
|
5247
|
+
console.log(` ⏭ correo: se queda en ${sendingDomainOf(PROVISION_EMAIL_FROM)} ` +
|
|
5248
|
+
'(usa `--client-mail` para enviar desde el dominio del cliente)');
|
|
5249
|
+
}
|
|
5250
|
+
/**
|
|
5251
|
+
* The go-live's mail choice in the state: `sharedMail: true` when the mail stayed on the org
|
|
5252
|
+
* sender, absent when it moved to the client ({@link noVaultMintDomain} reads it).
|
|
5253
|
+
*/
|
|
5254
|
+
function withMailChoice(dns, clientMail) {
|
|
5255
|
+
const { sharedMail: _previous, ...rest } = dns;
|
|
5256
|
+
return clientMail ? rest : { ...rest, sharedMail: true };
|
|
5257
|
+
}
|
|
5214
5258
|
/**
|
|
5215
|
-
* VAULT-002 — re-scope the project's Resend key after
|
|
5216
|
-
* sending domain, and `EMAIL_FROM` just moved to `updates.<client>`. Mints a new send-only key
|
|
5259
|
+
* VAULT-002 — re-scope the project's Resend key after a `--client-mail` go-live: the key is
|
|
5260
|
+
* limited to its sending domain, and `EMAIL_FROM` just moved to `updates.<client>`. Mints a new send-only key
|
|
5217
5261
|
* for the new domain, upserts it into `main:/` (the one copy the other environments reference;
|
|
5218
5262
|
* skipped without a vault) and into the deploys, records its id, and revokes the previous key.
|
|
5219
5263
|
*
|
|
@@ -5394,12 +5438,17 @@ function goLiveRetryHint(mailDomain) {
|
|
|
5394
5438
|
const client = mailDomain.startsWith(prefix) ? mailDomain.slice(prefix.length) : '<tu dominio>';
|
|
5395
5439
|
return `repite el go-live (\`factory provision --domain=${client}\`)`;
|
|
5396
5440
|
}
|
|
5441
|
+
/** Whether the state records a kit domain in that Railway environment (created, or found by adopt). */
|
|
5442
|
+
function hasRailwayKitDomain(state, role) {
|
|
5443
|
+
const rw = state?.railway;
|
|
5444
|
+
return Boolean(rw?.customDomains?.[role] || rw?.adoptedDomains?.some((d) => d.role === role));
|
|
5445
|
+
}
|
|
5397
5446
|
/**
|
|
5398
5447
|
* Print the resolved client-domain operation tree for `--domain --dry-run`
|
|
5399
5448
|
* (PROV-006 §8): the exact domain, the DNS route, whether the CNAME is created or
|
|
5400
5449
|
* printed, the interim-retire intent, and the env updates — mutating NOTHING.
|
|
5401
5450
|
*/
|
|
5402
|
-
function printClientDomainPlan(domain,
|
|
5451
|
+
function printClientDomainPlan(domain, hosts, route, confirm, target, clientMail = false) {
|
|
5403
5452
|
console.log(`\nPlan de \`--domain=${domain}\` (ruta DNS: ${route}):`);
|
|
5404
5453
|
if (target === 'railway') {
|
|
5405
5454
|
// RWAY-005: the CNAME's destination and the TXT are what Railway returns for the domain.
|
|
@@ -5424,17 +5473,27 @@ function printClientDomainPlan(domain, interim, route, confirm, target) {
|
|
|
5424
5473
|
}
|
|
5425
5474
|
console.log(` … sondea GET /v9/projects/.../domains/${domain} hasta verified:true (o timeout)`);
|
|
5426
5475
|
}
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5476
|
+
const { interim, dev } = hosts;
|
|
5477
|
+
console.log(interim
|
|
5478
|
+
? ` ${confirm ? '-' : '? (requiere --confirm o prompt)'} retira el interino ${interim}` +
|
|
5479
|
+
(dev ? ` (el dev ${dev} se conserva)` : '')
|
|
5480
|
+
: ' · sin dominio interino registrado: no hay nada que retirar');
|
|
5481
|
+
console.log(clientMail
|
|
5482
|
+
? ` ↻ EMAIL_FROM=noreply@${mailDomainFor(domain)} · NEXT_PUBLIC_APP_URL=https://${domain}`
|
|
5483
|
+
: ` ↻ NEXT_PUBLIC_APP_URL=https://${domain}`);
|
|
5430
5484
|
console.log(` ↻ WEBAUTHN_RP_ID=${domain} (production y preview — las passkeys del rpID interino quedan invalidadas) · ` +
|
|
5431
5485
|
`WEBAUTHN_RELATED_ORIGINS = origen de develop${confirm ? '' : ' (+ el interino si se conserva)'}`);
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5486
|
+
if (clientMail) {
|
|
5487
|
+
console.log(` + Resend: POST /domains { name: ${mailDomainFor(domain)} } (subdominio mail, no el apex)`);
|
|
5488
|
+
if (route === 'registrar') {
|
|
5489
|
+
console.log(' · (registrar) los records DNS de Resend (DKIM/SPF/MX) se crean a mano en el dashboard de Resend');
|
|
5490
|
+
}
|
|
5491
|
+
else {
|
|
5492
|
+
console.log(` + records DNS de Resend (DKIM/SPF/MX, los que devuelva) en la zona ${apexZoneOf(domain)} + sondeo hasta verified`);
|
|
5493
|
+
}
|
|
5435
5494
|
}
|
|
5436
5495
|
else {
|
|
5437
|
-
|
|
5496
|
+
logSharedMail();
|
|
5438
5497
|
}
|
|
5439
5498
|
console.log('\n(`--dry-run`) No se ejecutó ninguna llamada a API ni se escribió estado.');
|
|
5440
5499
|
}
|
|
@@ -8867,10 +8926,14 @@ export async function runProvision(flags, deps = {}) {
|
|
|
8867
8926
|
result.initialMigration = true;
|
|
8868
8927
|
}
|
|
8869
8928
|
if (hasDomain) {
|
|
8870
|
-
result.clientDomain = {
|
|
8929
|
+
result.clientDomain = {
|
|
8930
|
+
domain: flags.domain,
|
|
8931
|
+
route: flags.dns,
|
|
8932
|
+
clientMail: flags.clientMail === true,
|
|
8933
|
+
};
|
|
8871
8934
|
}
|
|
8872
8935
|
if (flags.services.includes('vault')) {
|
|
8873
|
-
result.vault = buildVaultPlan(projectName, flags.services);
|
|
8936
|
+
result.vault = buildVaultPlan(projectName, flags.services, target);
|
|
8874
8937
|
}
|
|
8875
8938
|
if (vaultDecision.skipped) {
|
|
8876
8939
|
result.vaultSkipped = vaultDecision.skipped;
|
|
@@ -8903,8 +8966,12 @@ export async function runProvision(flags, deps = {}) {
|
|
|
8903
8966
|
if (hasDomain) {
|
|
8904
8967
|
// Same rule as the real retire (DOM-001): the plan must print the interim the
|
|
8905
8968
|
// project ACTUALLY has, not one re-derived in the current org zone.
|
|
8906
|
-
const
|
|
8907
|
-
|
|
8969
|
+
const hosts = existingProvisionDomains(projectName, state?.dns);
|
|
8970
|
+
// Railway records its kit domains by environment: without one there is nothing to retire.
|
|
8971
|
+
const onRailway = target === 'railway';
|
|
8972
|
+
const interim = onRailway && !hasRailwayKitDomain(state, 'main') ? undefined : hosts.prod;
|
|
8973
|
+
const dev = onRailway && !hasRailwayKitDomain(state, 'develop') ? undefined : hosts.dev;
|
|
8974
|
+
printClientDomainPlan(flags.domain, { interim, dev }, flags.dns, flags.confirm, target, flags.clientMail === true);
|
|
8908
8975
|
}
|
|
8909
8976
|
}
|
|
8910
8977
|
return result;
|
|
@@ -8915,7 +8982,9 @@ export async function runProvision(flags, deps = {}) {
|
|
|
8915
8982
|
const preflight = await runProvisionPreflight({
|
|
8916
8983
|
services: preflightServices,
|
|
8917
8984
|
teamId: flags.team,
|
|
8918
|
-
|
|
8985
|
+
// The repo the steps target (state → `--repo` → `origin` → default), never one re-derived
|
|
8986
|
+
// from the slug: a repo named apart from its slug would fail a check the run would pass.
|
|
8987
|
+
...(repoSlug ? { repoSlug } : {}),
|
|
8919
8988
|
rootDir,
|
|
8920
8989
|
...(target ? { target } : {}),
|
|
8921
8990
|
}, { rail });
|
|
@@ -9110,6 +9179,7 @@ export async function runProvision(flags, deps = {}) {
|
|
|
9110
9179
|
const headless = !process.stdin.isTTY;
|
|
9111
9180
|
await provisionClientDomain(rootDir, runState, flags.team, flags.domain, {
|
|
9112
9181
|
dnsRoute: flags.dns,
|
|
9182
|
+
clientMail: flags.clientMail === true,
|
|
9113
9183
|
verifyTimeoutMin: flags.verifyTimeout,
|
|
9114
9184
|
preConfirmed: flags.confirm,
|
|
9115
9185
|
headless,
|
package/dist/lib/vault-api.js
CHANGED
|
@@ -946,7 +946,10 @@ export function createVaultClient(deps = {}) {
|
|
|
946
946
|
if (app !== 'railway')
|
|
947
947
|
assertDestination(app);
|
|
948
948
|
const operation = `consultar la conexión de la organización con ${app}`;
|
|
949
|
-
|
|
949
|
+
// `/available`, not the bare list: the list needs the org `Read` permission on app
|
|
950
|
+
// connections, which only admins hold; `/available` needs `Connect` — the same one a sync
|
|
951
|
+
// needs — and the built-in `member` role has it. With the list, a member got a 403.
|
|
952
|
+
const body = asObject(ok(await call(operation, 'GET', `/api/v1/app-connections/${app}/available`), operation));
|
|
950
953
|
const connections = asArray(body?.appConnections).flatMap((entry) => {
|
|
951
954
|
const connection = asObject(entry);
|
|
952
955
|
const id = asString(connection?.id);
|