@timekast/factory 2.2.0 → 2.3.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/provision.js +109 -55
- 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'),
|
|
@@ -1923,12 +1931,15 @@ const RESEND_KEY_RESUME_HINT = 'repite la corrida con `--resume` y `dns` en `--s
|
|
|
1923
1931
|
'hasta publicarla';
|
|
1924
1932
|
/**
|
|
1925
1933
|
* 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
|
-
|
|
1934
|
+
* once a `--client-mail` go-live is recorded (Resend already knows it: that go-live registers
|
|
1935
|
+
* it), else the org domain. A `--resume` that retries the key after such a go-live must not move
|
|
1936
|
+
* the mail back to the org domain. `sharedMail` marks a go-live that kept the org sender; a
|
|
1937
|
+
* `live` state without it predates the flag, when every go-live moved the mail.
|
|
1938
|
+
*/
|
|
1939
|
+
export function noVaultMintDomain(state) {
|
|
1940
|
+
const client = state.dns?.clientDomainStatus === 'live' && state.dns.sharedMail !== true
|
|
1941
|
+
? state.dns.clientDomain
|
|
1942
|
+
: undefined;
|
|
1932
1943
|
return client ? mailDomainFor(client) : sendingDomainOf(PROVISION_EMAIL_FROM);
|
|
1933
1944
|
}
|
|
1934
1945
|
/**
|
|
@@ -4711,8 +4722,9 @@ async function resolveDnsToken(route, rail, headless, rootDir) {
|
|
|
4711
4722
|
/**
|
|
4712
4723
|
* PROV-006 — attach the client's DEFINITIVE domain to the Vercel project, create
|
|
4713
4724
|
* (or print, for `registrar`) its DNS record, poll Vercel until verified, and on
|
|
4714
|
-
* success: retire the interim prod domain (with explicit confirmation)
|
|
4715
|
-
* `
|
|
4725
|
+
* success: retire the interim prod domain (with explicit confirmation) and update
|
|
4726
|
+
* `NEXT_PUBLIC_APP_URL`. The mail stays on the org sender unless `deps.clientMail`
|
|
4727
|
+
* (`--client-mail`): then `EMAIL_FROM` moves to `updates.<domain>`, registered in Resend.
|
|
4716
4728
|
*
|
|
4717
4729
|
* Standalone-safe: runnable as a PATCH over an existing provision (the `dns` step
|
|
4718
4730
|
* of PROV-005 already attached the interim domains) or inside the initial run. It
|
|
@@ -4745,6 +4757,7 @@ export async function provisionClientDomain(rootDir, state, teamId, domain, deps
|
|
|
4745
4757
|
'Corre el paso de Vercel/GitHub primero (`--services=github,...`) y reintenta con `--resume`.');
|
|
4746
4758
|
}
|
|
4747
4759
|
const route = deps.dnsRoute ?? 'org';
|
|
4760
|
+
const clientMail = deps.clientMail === true;
|
|
4748
4761
|
const rail = deps.rail ?? defaultRail();
|
|
4749
4762
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4750
4763
|
const vercelToken = await readVercelToken(rail);
|
|
@@ -4823,27 +4836,32 @@ export async function provisionClientDomain(rootDir, state, teamId, domain, deps
|
|
|
4823
4836
|
// 5. Update EMAIL_FROM + NEXT_PUBLIC_APP_URL + the WebAuthn rpID/related origins in Vercel env
|
|
4824
4837
|
// (idempotent — §8), and in the vault's `main:/` when the repo has one (VAULT-002).
|
|
4825
4838
|
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
|
-
|
|
4839
|
+
await updateDomainEnv(projectId, vercelToken, teamId, domain, vercelDeps, { dev: hosts.dev, interim: hosts.prod, interimRetired }, deps.vault ?? null, clientMail);
|
|
4840
|
+
if (clientMail) {
|
|
4841
|
+
// 6. Register the client's MAIL SUBDOMAIN (`updates.<domain>`) in Resend — NOT the
|
|
4842
|
+
// apex, which holds the client's corporate MX/SPF (Google Workspace, M365, …).
|
|
4843
|
+
// Verifying Resend on a dedicated subdomain keeps the client's own mail intact.
|
|
4844
|
+
// BOOT-003: also CREATES the DKIM/SPF/MX records Resend returns (in the same
|
|
4845
|
+
// client zone as the CNAME) and polls until Resend reports `verified`.
|
|
4846
|
+
await registerResendDomain(mailDomainFor(domain), rail, deps, fetchImpl, resendDns);
|
|
4847
|
+
// 6b. VAULT-002 — the project's Resend key is limited to its sending domain, which just
|
|
4848
|
+
// changed: re-scope it to `updates.<domain>` (vault, when there is one, + deploys) and
|
|
4849
|
+
// revoke the old one once the new one reached them. Without a vault too: the deploys
|
|
4850
|
+
// carry the project's key there as well (never the rail's team key).
|
|
4851
|
+
await rescopeProjectResendKey(rootDir, state, deps.vault ?? null, mailDomainFor(domain), { projectId, token: vercelToken, teamId, deps: vercelDeps }, rail, fetchImpl, { sleep: deps.sleep, now: deps.now });
|
|
4852
|
+
}
|
|
4853
|
+
else {
|
|
4854
|
+
logSharedMail();
|
|
4855
|
+
}
|
|
4838
4856
|
// 7. Record the client domain as LIVE and persist.
|
|
4839
|
-
state.dns = {
|
|
4857
|
+
state.dns = withMailChoice({
|
|
4840
4858
|
...(state.dns ?? {
|
|
4841
4859
|
domain: provisionDomains(state.projectName).prod,
|
|
4842
4860
|
devDomain: provisionDomains(state.projectName).dev,
|
|
4843
4861
|
}),
|
|
4844
4862
|
clientDomain: domain,
|
|
4845
4863
|
clientDomainStatus: 'live',
|
|
4846
|
-
};
|
|
4864
|
+
}, clientMail);
|
|
4847
4865
|
writeProvisionState(rootDir, state);
|
|
4848
4866
|
}
|
|
4849
4867
|
/** RWAY-005 — the two records the owner creates by hand for `--dns=registrar` on Railway. */
|
|
@@ -4890,8 +4908,9 @@ async function pollRailwayDomainVerification(client, scope, dns, deps) {
|
|
|
4890
4908
|
* only PRINTS both.
|
|
4891
4909
|
* 3. Poll the domain's state in Railway (`--verify-timeout`); timeout → `pending-verification`.
|
|
4892
4910
|
* 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
|
|
4911
|
+
* 5. `NEXT_PUBLIC_APP_URL`, `WEBAUTHN_RP_ID`/`_RELATED_ORIGINS` (+ `EMAIL_FROM` with
|
|
4912
|
+
* `--client-mail`) → `main:/` of the vault ONLY (its sync is Railway's only writer); Resend
|
|
4913
|
+
* as on Vercel (only with `--client-mail`).
|
|
4895
4914
|
* 🔴 No request to Vercel. Requires the vault and the `railway` block (none → `--adopt`).
|
|
4896
4915
|
*/
|
|
4897
4916
|
async function provisionRailwayClientDomain(rootDir, state, domain, deps) {
|
|
@@ -4901,6 +4920,7 @@ async function provisionRailwayClientDomain(rootDir, state, domain, deps) {
|
|
|
4901
4920
|
assertRailwayDomainIds(state);
|
|
4902
4921
|
const rw = state.railway;
|
|
4903
4922
|
const route = deps.dnsRoute ?? 'org';
|
|
4923
|
+
const clientMail = deps.clientMail === true;
|
|
4904
4924
|
const rail = deps.rail ?? defaultRail();
|
|
4905
4925
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4906
4926
|
const client = railwayClientFor(rail, deps.railway);
|
|
@@ -4987,15 +5007,21 @@ async function provisionRailwayClientDomain(rootDir, state, domain, deps) {
|
|
|
4987
5007
|
dev: hosts.dev,
|
|
4988
5008
|
interim: hosts.prod,
|
|
4989
5009
|
interimRetired,
|
|
4990
|
-
});
|
|
4991
|
-
// 6. Resend — unchanged by the destination (mail subdomain + the project's key re-scoped)
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
5010
|
+
}, clientMail);
|
|
5011
|
+
// 6. Resend — unchanged by the destination (mail subdomain + the project's key re-scoped),
|
|
5012
|
+
// and only with `--client-mail`.
|
|
5013
|
+
if (clientMail) {
|
|
5014
|
+
await registerResendDomain(mailDomainFor(domain), rail, paced, fetchImpl, resendDns);
|
|
5015
|
+
await rescopeProjectResendKey(rootDir, state, vault, mailDomainFor(domain), null, rail, fetchImpl, {
|
|
5016
|
+
sleep: paced.sleep,
|
|
5017
|
+
now: paced.now,
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
else {
|
|
5021
|
+
logSharedMail();
|
|
5022
|
+
}
|
|
4997
5023
|
// 7. Live.
|
|
4998
|
-
state.dns = { ...dnsBlock(), clientDomainStatus: 'live' };
|
|
5024
|
+
state.dns = withMailChoice({ ...dnsBlock(), clientDomainStatus: 'live' }, clientMail);
|
|
4999
5025
|
writeProvisionState(rootDir, state);
|
|
5000
5026
|
}
|
|
5001
5027
|
/**
|
|
@@ -5125,12 +5151,15 @@ async function deleteInterimCname(interim, rail, fetchImpl) {
|
|
|
5125
5151
|
*
|
|
5126
5152
|
* No-echo: only KEY NAMES + targets print (the values here are public, but the discipline holds).
|
|
5127
5153
|
*/
|
|
5128
|
-
export async function updateDomainEnv(projectId, token, teamId, domain, deps, hosts, vault = null) {
|
|
5154
|
+
export async function updateDomainEnv(projectId, token, teamId, domain, deps, hosts, vault = null, clientMail = false) {
|
|
5129
5155
|
const { appOrigin, emailFrom, relatedOrigins } = domainEnvValues(domain, hosts);
|
|
5130
5156
|
const plan = [
|
|
5131
|
-
// EMAIL_FROM
|
|
5132
|
-
// — the apex carries the client's corporate mail.
|
|
5133
|
-
|
|
5157
|
+
// EMAIL_FROM only with `--client-mail`: the dedicated mail subdomain (`updates.<domain>`),
|
|
5158
|
+
// NOT the apex — the apex carries the client's corporate mail. Without it the sender
|
|
5159
|
+
// already in place (the org's, or one a team set by hand) is left alone.
|
|
5160
|
+
...(clientMail
|
|
5161
|
+
? [{ key: 'EMAIL_FROM', value: emailFrom, target: ['production', 'preview'] }]
|
|
5162
|
+
: []),
|
|
5134
5163
|
// 🔴 AUTH_URL / NEXTAUTH_URL are DELIBERATELY ABSENT — do not re-add them.
|
|
5135
5164
|
// `next-auth/lib/env.js` rewrites the origin of every auth request to their value,
|
|
5136
5165
|
// so scoping either to `preview` pins all branch deployments' callbacks to this one
|
|
@@ -5169,7 +5198,7 @@ export async function updateDomainEnv(projectId, token, teamId, domain, deps, ho
|
|
|
5169
5198
|
}
|
|
5170
5199
|
}
|
|
5171
5200
|
if (vault) {
|
|
5172
|
-
await writeDomainEnvToVault(vault, domain, hosts);
|
|
5201
|
+
await writeDomainEnvToVault(vault, domain, hosts, clientMail);
|
|
5173
5202
|
}
|
|
5174
5203
|
else {
|
|
5175
5204
|
warnRpIdMoved(domain);
|
|
@@ -5191,10 +5220,10 @@ function domainEnvValues(domain, hosts) {
|
|
|
5191
5220
|
* reference for the shared ones; the syncs deliver them to production (Vercel or Railway, SYNC-001).
|
|
5192
5221
|
* `NEXT_PUBLIC_APP_URL` goes to `main:/` only: `develop:/` keeps develop's origin.
|
|
5193
5222
|
*/
|
|
5194
|
-
async function writeDomainEnvToVault(vault, domain, hosts) {
|
|
5223
|
+
async function writeDomainEnvToVault(vault, domain, hosts, clientMail) {
|
|
5195
5224
|
const { appOrigin, emailFrom, relatedOrigins } = domainEnvValues(domain, hosts);
|
|
5196
5225
|
await vaultWrite(vault, VAULT_MAIN_ROOT, {
|
|
5197
|
-
EMAIL_FROM: emailFrom,
|
|
5226
|
+
...(clientMail ? { EMAIL_FROM: emailFrom } : {}),
|
|
5198
5227
|
NEXT_PUBLIC_APP_URL: appOrigin,
|
|
5199
5228
|
WEBAUTHN_RP_ID: domain,
|
|
5200
5229
|
WEBAUTHN_RELATED_ORIGINS: relatedOrigins,
|
|
@@ -5211,9 +5240,22 @@ function warnRpIdMoved(domain) {
|
|
|
5211
5240
|
'bajo el rpID interino queda invalidada — esos usuarios deben registrar una nueva ' +
|
|
5212
5241
|
'passkey en el dominio definitivo.');
|
|
5213
5242
|
}
|
|
5243
|
+
/** The go-live without `--client-mail`: say where the mail stays and how to move it. */
|
|
5244
|
+
function logSharedMail() {
|
|
5245
|
+
console.log(` ⏭ correo: se queda en ${sendingDomainOf(PROVISION_EMAIL_FROM)} ` +
|
|
5246
|
+
'(usa `--client-mail` para enviar desde el dominio del cliente)');
|
|
5247
|
+
}
|
|
5248
|
+
/**
|
|
5249
|
+
* The go-live's mail choice in the state: `sharedMail: true` when the mail stayed on the org
|
|
5250
|
+
* sender, absent when it moved to the client ({@link noVaultMintDomain} reads it).
|
|
5251
|
+
*/
|
|
5252
|
+
function withMailChoice(dns, clientMail) {
|
|
5253
|
+
const { sharedMail: _previous, ...rest } = dns;
|
|
5254
|
+
return clientMail ? rest : { ...rest, sharedMail: true };
|
|
5255
|
+
}
|
|
5214
5256
|
/**
|
|
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
|
|
5257
|
+
* VAULT-002 — re-scope the project's Resend key after a `--client-mail` go-live: the key is
|
|
5258
|
+
* limited to its sending domain, and `EMAIL_FROM` just moved to `updates.<client>`. Mints a new send-only key
|
|
5217
5259
|
* for the new domain, upserts it into `main:/` (the one copy the other environments reference;
|
|
5218
5260
|
* skipped without a vault) and into the deploys, records its id, and revokes the previous key.
|
|
5219
5261
|
*
|
|
@@ -5399,7 +5441,7 @@ function goLiveRetryHint(mailDomain) {
|
|
|
5399
5441
|
* (PROV-006 §8): the exact domain, the DNS route, whether the CNAME is created or
|
|
5400
5442
|
* printed, the interim-retire intent, and the env updates — mutating NOTHING.
|
|
5401
5443
|
*/
|
|
5402
|
-
function printClientDomainPlan(domain, interim, route, confirm, target) {
|
|
5444
|
+
function printClientDomainPlan(domain, interim, route, confirm, target, clientMail = false) {
|
|
5403
5445
|
console.log(`\nPlan de \`--domain=${domain}\` (ruta DNS: ${route}):`);
|
|
5404
5446
|
if (target === 'railway') {
|
|
5405
5447
|
// RWAY-005: the CNAME's destination and the TXT are what Railway returns for the domain.
|
|
@@ -5426,15 +5468,22 @@ function printClientDomainPlan(domain, interim, route, confirm, target) {
|
|
|
5426
5468
|
}
|
|
5427
5469
|
console.log(` ${confirm ? '-' : '? (requiere --confirm o prompt)'} retira el interino ${interim} ` +
|
|
5428
5470
|
`(el dev ${domain}-dev se conserva)`);
|
|
5429
|
-
console.log(
|
|
5471
|
+
console.log(clientMail
|
|
5472
|
+
? ` ↻ EMAIL_FROM=noreply@${mailDomainFor(domain)} · NEXT_PUBLIC_APP_URL=https://${domain}`
|
|
5473
|
+
: ` ↻ NEXT_PUBLIC_APP_URL=https://${domain}`);
|
|
5430
5474
|
console.log(` ↻ WEBAUTHN_RP_ID=${domain} (production y preview — las passkeys del rpID interino quedan invalidadas) · ` +
|
|
5431
5475
|
`WEBAUTHN_RELATED_ORIGINS = origen de develop${confirm ? '' : ' (+ el interino si se conserva)'}`);
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5476
|
+
if (clientMail) {
|
|
5477
|
+
console.log(` + Resend: POST /domains { name: ${mailDomainFor(domain)} } (subdominio mail, no el apex)`);
|
|
5478
|
+
if (route === 'registrar') {
|
|
5479
|
+
console.log(' · (registrar) los records DNS de Resend (DKIM/SPF/MX) se crean a mano en el dashboard de Resend');
|
|
5480
|
+
}
|
|
5481
|
+
else {
|
|
5482
|
+
console.log(` + records DNS de Resend (DKIM/SPF/MX, los que devuelva) en la zona ${apexZoneOf(domain)} + sondeo hasta verified`);
|
|
5483
|
+
}
|
|
5435
5484
|
}
|
|
5436
5485
|
else {
|
|
5437
|
-
|
|
5486
|
+
logSharedMail();
|
|
5438
5487
|
}
|
|
5439
5488
|
console.log('\n(`--dry-run`) No se ejecutó ninguna llamada a API ni se escribió estado.');
|
|
5440
5489
|
}
|
|
@@ -8867,7 +8916,11 @@ export async function runProvision(flags, deps = {}) {
|
|
|
8867
8916
|
result.initialMigration = true;
|
|
8868
8917
|
}
|
|
8869
8918
|
if (hasDomain) {
|
|
8870
|
-
result.clientDomain = {
|
|
8919
|
+
result.clientDomain = {
|
|
8920
|
+
domain: flags.domain,
|
|
8921
|
+
route: flags.dns,
|
|
8922
|
+
clientMail: flags.clientMail === true,
|
|
8923
|
+
};
|
|
8871
8924
|
}
|
|
8872
8925
|
if (flags.services.includes('vault')) {
|
|
8873
8926
|
result.vault = buildVaultPlan(projectName, flags.services);
|
|
@@ -8904,7 +8957,7 @@ export async function runProvision(flags, deps = {}) {
|
|
|
8904
8957
|
// Same rule as the real retire (DOM-001): the plan must print the interim the
|
|
8905
8958
|
// project ACTUALLY has, not one re-derived in the current org zone.
|
|
8906
8959
|
const interim = existingProvisionDomains(projectName, state?.dns).prod;
|
|
8907
|
-
printClientDomainPlan(flags.domain, interim, flags.dns, flags.confirm, target);
|
|
8960
|
+
printClientDomainPlan(flags.domain, interim, flags.dns, flags.confirm, target, flags.clientMail === true);
|
|
8908
8961
|
}
|
|
8909
8962
|
}
|
|
8910
8963
|
return result;
|
|
@@ -9110,6 +9163,7 @@ export async function runProvision(flags, deps = {}) {
|
|
|
9110
9163
|
const headless = !process.stdin.isTTY;
|
|
9111
9164
|
await provisionClientDomain(rootDir, runState, flags.team, flags.domain, {
|
|
9112
9165
|
dnsRoute: flags.dns,
|
|
9166
|
+
clientMail: flags.clientMail === true,
|
|
9113
9167
|
verifyTimeoutMin: flags.verifyTimeout,
|
|
9114
9168
|
preConfirmed: flags.confirm,
|
|
9115
9169
|
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);
|