@wtfalch/foundry 0.71.0 → 0.72.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/index.d.ts +1 -1
- package/dist/modules/sign-in.js +81 -9
- package/dist/modules/types.d.ts +2 -0
- package/dist/org.d.ts +18 -1
- package/dist/org.js +40 -0
- package/dist/plan/boule.d.ts +15 -0
- package/dist/plan/boule.js +13 -0
- package/dist/plan/broker-registrar-token.d.ts +69 -0
- package/dist/plan/broker-registrar-token.js +215 -0
- package/dist/plan/broker-registrar.d.ts +2 -0
- package/dist/plan/broker-registrar.js +5 -1
- package/dist/plan/diff.d.ts +13 -0
- package/dist/plan/diff.js +26 -5
- package/dist/plan/executor.d.ts +66 -0
- package/dist/plan/executor.js +136 -8
- package/dist/plan/health-check-settings.d.ts +13 -0
- package/dist/plan/health-check-settings.js +17 -0
- package/dist/plan/own-diff.d.ts +32 -0
- package/dist/plan/own-diff.js +67 -10
- package/dist/plan/own-hostnames.d.ts +8 -0
- package/dist/plan/own-hostnames.js +14 -0
- package/dist/plan/own-services.d.ts +7 -0
- package/dist/plan/own-services.js +2 -0
- package/dist/plan/plan-apply.d.ts +15 -0
- package/dist/plan/plan-apply.js +21 -0
- package/dist/plan/platform-diff.d.ts +9 -5
- package/dist/plan/platform-diff.js +16 -10
- package/dist/plan/platform-providers.d.ts +24 -2
- package/dist/plan/platform-providers.js +155 -12
- package/dist/plan/signin-mail.d.ts +18 -5
- package/dist/plan/signin-mail.js +14 -6
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +72 -46
- package/dist/plan/zitadel-machine-user.d.ts +14 -1
- package/dist/plan/zitadel-machine-user.js +96 -13
- package/package.json +3 -3
|
@@ -15,7 +15,7 @@ import { desktopCallbackUri, unionRedirectUris } from './desktop-callback.js';
|
|
|
15
15
|
import { placedOnPlatform } from './destinations.js';
|
|
16
16
|
import { ACCESS_POLICY_NAME, LOG_DIR, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, setOidcAppLoginBaseUri, } from './executor.js';
|
|
17
17
|
import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
|
|
18
|
-
import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, tcpProbe, } from './health-check-settings.js';
|
|
18
|
+
import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, rollingUpdateSettings, tcpProbe, } from './health-check-settings.js';
|
|
19
19
|
import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
20
20
|
import { LIMITS, limited } from './own-limits.js';
|
|
21
21
|
import { postLogoutAppliedValue, postLogoutUrisFor } from './post-logout-uris.js';
|
|
@@ -24,6 +24,7 @@ import { buildStageCopyHostPorts } from './stage-copy-host.js';
|
|
|
24
24
|
import { stageGateKey, stageHostname, syncStageGateMembers, } from './stage-guards.js';
|
|
25
25
|
export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
26
26
|
import { BROKER_DB_NAME, BROKER_DB_USER, ensureBrokerMcpEnv } from './broker-mcp.js';
|
|
27
|
+
import { REGISTRAR_TOKEN_SCOPE_KEY, REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY, RegistrarTokenError, ensureBrokerRegistrarToken, } from './broker-registrar-token.js';
|
|
27
28
|
import { withTimeout } from './call-timeout.js';
|
|
28
29
|
import { SIGN_IN_BIND_KEY_ENV, SIGN_IN_BIND_KEY_MIN_LENGTH, SIGN_IN_BIND_KEY_SCOPE_KEY, } from './cookie-domain.js';
|
|
29
30
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
@@ -436,6 +437,9 @@ export class FoundryPlatformStepExecutor {
|
|
|
436
437
|
const inviteBlocked = ownerInviteBlockedReason(ownerCompany, Boolean(this.auth.ownerInviteMail));
|
|
437
438
|
if (inviteBlocked)
|
|
438
439
|
blocked.authOwnerInvite = inviteBlocked;
|
|
440
|
+
const registrarBlocked = await registrarTokenBlockedReason(this.auth, recordedIds);
|
|
441
|
+
if (registrarBlocked)
|
|
442
|
+
blocked.authRegistrar = registrarBlocked;
|
|
439
443
|
}
|
|
440
444
|
if (platform.mail.enabled) {
|
|
441
445
|
const mailBlocked = await mailBlockedReason(platform, this.mail, recordedIds);
|
|
@@ -911,13 +915,22 @@ export class FoundryPlatformStepExecutor {
|
|
|
911
915
|
const loginClientDrifted = await authLoginClientDrifted(platform, this.auth, recordedIds);
|
|
912
916
|
// The broker lacks the MCP database and secret a shared org's entry needs (auth ADR 0002).
|
|
913
917
|
const mcpDrifted = recordedIds[PLATFORM_AUTH_MCP_SCOPE_KEY] !== '1' && (await this.mcpWanted());
|
|
918
|
+
// The broker has no registrar token yet (`ZITADEL_REGISTRAR_TOKEN`).
|
|
919
|
+
// A token minted and never held is blocked (`blockedSteps`), not drift: no apply could change it.
|
|
920
|
+
const registrarDrifted = this.auth.registrarTokenWritable === true &&
|
|
921
|
+
!recordedIds[REGISTRAR_TOKEN_SCOPE_KEY] &&
|
|
922
|
+
!(await registrarTokenBlockedReason(this.auth, recordedIds));
|
|
914
923
|
if (loginClientDrifted ||
|
|
915
924
|
mcpDrifted ||
|
|
925
|
+
registrarDrifted ||
|
|
916
926
|
(await this.signInLoginDrifted(this.auth, recordedIds))) {
|
|
917
927
|
result.auth = true;
|
|
918
928
|
}
|
|
919
929
|
if (await authAppsNotRunning(this.auth.coolify, recordedIds))
|
|
920
930
|
result.authApps = true;
|
|
931
|
+
// An app made before the rolling update still has its fixed container name: the step clears it.
|
|
932
|
+
if (await authAppsKeepInternalName(this.auth.coolify, recordedIds))
|
|
933
|
+
result.auth = true;
|
|
921
934
|
}
|
|
922
935
|
// #380: once the runner app is recorded, re-check its live env against
|
|
923
936
|
// every setting `ensureStarted()` requires -- the gap that left the
|
|
@@ -1561,7 +1574,25 @@ const PLATFORM_AUTH_ADMIN_USER_NAME = 'foundry-admin';
|
|
|
1561
1574
|
const PLATFORM_AUTH_RESERVED_USERNAME = 'admin';
|
|
1562
1575
|
/** #216 (building on #209/#215): the narrower `IAM_LOGIN_CLIENT` machine user for the login app and the broker. */
|
|
1563
1576
|
const PLATFORM_AUTH_LOGIN_CLIENT_USER_NAME = 'foundry-login-client';
|
|
1564
|
-
const PLATFORM_AUTH_ORG_NAME = 'foundry';
|
|
1577
|
+
export const PLATFORM_AUTH_ORG_NAME = 'foundry';
|
|
1578
|
+
/**
|
|
1579
|
+
* The instance's first organisation (the default one, whose name the login shows as its wordmark).
|
|
1580
|
+
* A company's own instance passes the recorded id (the platform passes none and keeps the name `foundry`);
|
|
1581
|
+
* found by that id when there is one, because a company's own instance has it renamed to the
|
|
1582
|
+
* company's name (`applySigninMail`), and a lookup by name would then make a second `foundry` org. The
|
|
1583
|
+
* name is the way in only before the id is recorded. A recorded id ZITADEL no longer has is a failure,
|
|
1584
|
+
* never a reason to make another org.
|
|
1585
|
+
*/
|
|
1586
|
+
async function platformAuthFirstOrg(zitadel, recordedId) {
|
|
1587
|
+
if (recordedId) {
|
|
1588
|
+
// The name is no way back in once the id is recorded: the org was renamed, and a lookup by name would make another.
|
|
1589
|
+
if (typeof zitadel.getOrg !== 'function') {
|
|
1590
|
+
throw new Error("the instance's first organisation is recorded, but the host's ZITADEL port cannot read it by id; it is not looked up by name");
|
|
1591
|
+
}
|
|
1592
|
+
return await zitadel.getOrg(recordedId);
|
|
1593
|
+
}
|
|
1594
|
+
return (await zitadel.findOrCreateOrg(PLATFORM_AUTH_ORG_NAME)).org;
|
|
1595
|
+
}
|
|
1565
1596
|
/**
|
|
1566
1597
|
* #283: the ref the new instance's IAM_OWNER machine token (a PAT) is
|
|
1567
1598
|
* delivered under, once. A host reads it back to build `zitadelForInstance`
|
|
@@ -1628,6 +1659,27 @@ function loginClientRecordValue(userId, patId, token) {
|
|
|
1628
1659
|
};
|
|
1629
1660
|
return JSON.stringify(record);
|
|
1630
1661
|
}
|
|
1662
|
+
/**
|
|
1663
|
+
* The broker's registrar token was minted and never held: nothing can deliver it and a second mint is refused
|
|
1664
|
+
* (platform-step rule 1), so the plan reports it and plans nothing for it. Undefined when not in that state.
|
|
1665
|
+
*/
|
|
1666
|
+
async function registrarTokenBlockedReason(auth, recordedIds) {
|
|
1667
|
+
const userId = recordedIds[REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY];
|
|
1668
|
+
if (auth.registrarTokenWritable !== true || !userId || recordedIds[REGISTRAR_TOKEN_SCOPE_KEY]) {
|
|
1669
|
+
return undefined;
|
|
1670
|
+
}
|
|
1671
|
+
let held;
|
|
1672
|
+
try {
|
|
1673
|
+
held = await auth.heldKeys?.resolve(platformTarget().refs.registrarToken);
|
|
1674
|
+
}
|
|
1675
|
+
catch {
|
|
1676
|
+
// Not read: nothing is claimed, and the step itself reports the state.
|
|
1677
|
+
return undefined;
|
|
1678
|
+
}
|
|
1679
|
+
if (held)
|
|
1680
|
+
return undefined;
|
|
1681
|
+
return `the broker's registrar token of ZITADEL machine user ${userId} was minted but never confirmed held; remove the recorded key ${REGISTRAR_TOKEN_UNDELIVERED_SCOPE_KEY} to make a new one`;
|
|
1682
|
+
}
|
|
1631
1683
|
/**
|
|
1632
1684
|
* #216 round 4: whether the login-client PAT recorded for this platform
|
|
1633
1685
|
* (under {@link PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY}) is still, right
|
|
@@ -2037,7 +2089,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2037
2089
|
const gitBranchPatchFor = (app) => authRefChanged && !app.justCreated ? { git_branch: gitRef } : {};
|
|
2038
2090
|
// The Dockerfile's own HEALTHCHECK is the live check (`IMAGE_HEALTHCHECK_SETTINGS`).
|
|
2039
2091
|
await deps.coolify.updateApplication(loginAppId, {
|
|
2040
|
-
|
|
2092
|
+
...rollingUpdateSettings(target.internal.authLogin),
|
|
2041
2093
|
...IMAGE_HEALTHCHECK_SETTINGS,
|
|
2042
2094
|
...gitBranchPatchFor(loginApp),
|
|
2043
2095
|
});
|
|
@@ -2071,7 +2123,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2071
2123
|
: null;
|
|
2072
2124
|
if (signInApp && signInNames) {
|
|
2073
2125
|
await deps.coolify.updateApplication(signInApp.appId, {
|
|
2074
|
-
|
|
2126
|
+
...rollingUpdateSettings(signInNames.internal),
|
|
2075
2127
|
...IMAGE_HEALTHCHECK_SETTINGS,
|
|
2076
2128
|
...gitBranchPatchFor(signInApp),
|
|
2077
2129
|
});
|
|
@@ -2097,7 +2149,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2097
2149
|
const brokerAppId = brokerApp.appId;
|
|
2098
2150
|
await record(PLATFORM_AUTH_BROKER_SCOPE_KEY, brokerAppId);
|
|
2099
2151
|
await deps.coolify.updateApplication(brokerAppId, {
|
|
2100
|
-
|
|
2152
|
+
...rollingUpdateSettings(target.internal.authBroker),
|
|
2101
2153
|
...IMAGE_HEALTHCHECK_SETTINGS,
|
|
2102
2154
|
...gitBranchPatchFor(brokerApp),
|
|
2103
2155
|
});
|
|
@@ -2201,6 +2253,8 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2201
2253
|
loginEnvs.push({ key: 'LETTERMINT_API_TOKEN', value: heldMailKey });
|
|
2202
2254
|
if (input.smtpSender)
|
|
2203
2255
|
loginEnvs.push({ key: 'LOGIN_MAIL_FROM', value: input.smtpSender });
|
|
2256
|
+
if (input.passkeyRpId)
|
|
2257
|
+
loginEnvs.push({ key: LOGIN_PASSKEY_RP_ID_ENV, value: input.passkeyRpId });
|
|
2204
2258
|
// `AUTH_APPS` and `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` are not pushed here
|
|
2205
2259
|
// -- this function's own doc comment, "Known gaps". `LETTERMINT_API_TOKEN`
|
|
2206
2260
|
// is, once held (#210): pushing it costs nothing even while the broker
|
|
@@ -2242,6 +2296,12 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2242
2296
|
await deps.coolify.bulkUpsertEnvs(zitadelAppId, zitadelEnvs);
|
|
2243
2297
|
await deps.coolify.bulkUpsertEnvs(loginAppId, loginEnvs);
|
|
2244
2298
|
await deps.coolify.bulkUpsertEnvs(brokerAppId, brokerEnvs);
|
|
2299
|
+
// A domain removed (or `usesDomain` off) leaves no RP ID to set: a value from an earlier apply is removed,
|
|
2300
|
+
// and the apps deploy again, so a passkey is never bound to a domain the company no longer has.
|
|
2301
|
+
const staleRpIdLogin = input.passkeyRpId
|
|
2302
|
+
? false
|
|
2303
|
+
: await removeLoginEnv(deps.coolify, loginAppId, LOGIN_PASSKEY_RP_ID_ENV);
|
|
2304
|
+
let staleRpIdSignIn = false;
|
|
2245
2305
|
// The in-app login: the login's env, the base path as a build argument, and the host table.
|
|
2246
2306
|
let signInEnvsChanged = false;
|
|
2247
2307
|
// The session bind key is minted once, on the in-app login app only: a key already live (32 characters or
|
|
@@ -2265,6 +2325,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2265
2325
|
];
|
|
2266
2326
|
signInEnvsChanged = await envsDiffer(deps.coolify, signInApp.appId, signInEnvs);
|
|
2267
2327
|
await deps.coolify.bulkUpsertEnvs(signInApp.appId, signInEnvs);
|
|
2328
|
+
staleRpIdSignIn = input.passkeyRpId
|
|
2329
|
+
? false
|
|
2330
|
+
: await removeLoginEnv(deps.coolify, signInApp.appId, LOGIN_PASSKEY_RP_ID_ENV);
|
|
2268
2331
|
}
|
|
2269
2332
|
// Deploy an app that was just created, is not running (a throw after the
|
|
2270
2333
|
// create left it never deployed, or it stopped), or whose envs changed.
|
|
@@ -2290,10 +2353,12 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2290
2353
|
// died between the write and the deploy leaves the marker unset, and the next run sees no env diff.
|
|
2291
2354
|
const loginEnvUnconfirmed = Boolean(heldMailKey &&
|
|
2292
2355
|
input.smtpSender &&
|
|
2293
|
-
recorded[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] !==
|
|
2356
|
+
recorded[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] !==
|
|
2357
|
+
authLoginEnvMarker(input.smtpSender, input.passkeyRpId));
|
|
2294
2358
|
if (loginApp.justCreated ||
|
|
2295
2359
|
!loginApp.running ||
|
|
2296
2360
|
loginEnvsChanged ||
|
|
2361
|
+
staleRpIdLogin ||
|
|
2297
2362
|
loginEnvUnconfirmed ||
|
|
2298
2363
|
authRefChanged) {
|
|
2299
2364
|
await deps.coolify.deploy(loginAppId);
|
|
@@ -2305,6 +2370,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2305
2370
|
(signInApp.justCreated ||
|
|
2306
2371
|
!signInApp.running ||
|
|
2307
2372
|
signInEnvsChanged ||
|
|
2373
|
+
staleRpIdSignIn ||
|
|
2308
2374
|
bindKeyWritten ||
|
|
2309
2375
|
bindKeyUnconfirmed ||
|
|
2310
2376
|
loginEnvUnconfirmed ||
|
|
@@ -2316,7 +2382,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2316
2382
|
// The mail env is live on the login apps: written above and deployed with them. Without the held
|
|
2317
2383
|
// key the marker stays unset, so the step is planned again once the key is held.
|
|
2318
2384
|
if (heldMailKey && input.smtpSender) {
|
|
2319
|
-
await record(PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, authLoginEnvMarker(input.smtpSender));
|
|
2385
|
+
await record(PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, authLoginEnvMarker(input.smtpSender, input.passkeyRpId));
|
|
2320
2386
|
}
|
|
2321
2387
|
// #211: the broker's own `server.ts` refuses to start without
|
|
2322
2388
|
// `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `LETTERMINT_API_TOKEN` --
|
|
@@ -2501,7 +2567,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2501
2567
|
// gets here right after the deploy.
|
|
2502
2568
|
await waitForInstance(`https://${hostname}/.well-known/openid-configuration`, runtime, sleep, label);
|
|
2503
2569
|
const zitadel = instanceZitadel();
|
|
2504
|
-
const
|
|
2570
|
+
const org = await platformAuthFirstOrg(zitadel, target.kind === 'org' ? recorded[PLATFORM_ZITADEL_ORG_SCOPE_KEY] : undefined);
|
|
2505
2571
|
// #216 round 5: recorded here too (mirroring `provisionPlatformArchon`),
|
|
2506
2572
|
// so `authLoginClientDrifted` (below) never needs its own ZITADEL call
|
|
2507
2573
|
// to resolve the org -- see that function's own doc comment.
|
|
@@ -2642,7 +2708,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2642
2708
|
throw new Error(`${label}: the login-client token of ZITADEL machine user ${loginClientLost} was minted but never confirmed delivered; remove the recorded key ${PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY} to mint a new one`);
|
|
2643
2709
|
}
|
|
2644
2710
|
const loginClientZitadel = instanceZitadel();
|
|
2645
|
-
const
|
|
2711
|
+
const loginClientOrg = await platformAuthFirstOrg(loginClientZitadel, target.kind === 'org' ? freshRecordedIds[PLATFORM_ZITADEL_ORG_SCOPE_KEY] : undefined);
|
|
2646
2712
|
// #216 round 5: backfills the org id for a platform whose admin block
|
|
2647
2713
|
// (above) ran before this recording existed and has not run since --
|
|
2648
2714
|
// see `authLoginClientDrifted`'s own doc comment.
|
|
@@ -2788,12 +2854,17 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2788
2854
|
(deps.signinMailWritable !== true ||
|
|
2789
2855
|
typeof mailZitadel.ensureEmailOtpMessageText !== 'function' ||
|
|
2790
2856
|
typeof mailZitadel.ensureLabelPolicy !== 'function' ||
|
|
2857
|
+
typeof mailZitadel.ensureOrgName !== 'function' ||
|
|
2791
2858
|
typeof mailZitadel.ensureEmailOtpGenerator !== 'function')) {
|
|
2792
2859
|
console.info(`${label}: the host cannot write message texts, so the sign-in mail keeps ZITADEL's default text`);
|
|
2793
2860
|
}
|
|
2794
2861
|
else if (input.mailBranding) {
|
|
2862
|
+
const firstOrgId = scope.recordedIds()[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
|
|
2863
|
+
if (!firstOrgId) {
|
|
2864
|
+
throw new Error(`${label}: the instance's first organisation is not recorded`);
|
|
2865
|
+
}
|
|
2795
2866
|
try {
|
|
2796
|
-
await applySigninMail(mailZitadel, input.mailBranding.companyName);
|
|
2867
|
+
await applySigninMail(mailZitadel, input.mailBranding.companyName, firstOrgId);
|
|
2797
2868
|
}
|
|
2798
2869
|
catch (error) {
|
|
2799
2870
|
const cause = redactSecrets(error instanceof Error ? error.message : String(error), [
|
|
@@ -2854,6 +2925,41 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2854
2925
|
}
|
|
2855
2926
|
}
|
|
2856
2927
|
}
|
|
2928
|
+
// The broker's registrar token: made once, held, written to the broker's env, adopted when set by hand.
|
|
2929
|
+
// Last, so a stop here never holds up the login env, the mail or the invite above. It is reported, not
|
|
2930
|
+
// thrown: the plan names the step again while the token is due, or blocked while it was minted and not held.
|
|
2931
|
+
if (input.registrarOrgIds) {
|
|
2932
|
+
try {
|
|
2933
|
+
const firstOrgId = scope.recordedIds()[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
|
|
2934
|
+
if (!firstOrgId) {
|
|
2935
|
+
throw new RegistrarTokenError(`${label}: the instance's first organisation is not recorded`);
|
|
2936
|
+
}
|
|
2937
|
+
await ensureBrokerRegistrarToken({
|
|
2938
|
+
label,
|
|
2939
|
+
zitadel: instanceZitadel(),
|
|
2940
|
+
coolify: deps.coolify,
|
|
2941
|
+
brokerAppId,
|
|
2942
|
+
homeOrgId: firstOrgId,
|
|
2943
|
+
orgIds: input.registrarOrgIds,
|
|
2944
|
+
heldKeys: deps.heldKeys,
|
|
2945
|
+
ref: target.refs.registrarToken,
|
|
2946
|
+
recorded: () => scope.recordedIds(),
|
|
2947
|
+
record,
|
|
2948
|
+
persist: async () => {
|
|
2949
|
+
await scope.persist();
|
|
2950
|
+
},
|
|
2951
|
+
secret: (value) => runtime.onSecret?.(value),
|
|
2952
|
+
sleep,
|
|
2953
|
+
log: (line) => console.info(line),
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
catch (error) {
|
|
2957
|
+
// Only our own text, which names ids; any other cause can carry a token.
|
|
2958
|
+
console.warn(error instanceof RegistrarTokenError
|
|
2959
|
+
? error.message
|
|
2960
|
+
: `${label}: the broker's registrar token is not in place yet; the next apply tries again`);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2857
2963
|
return { resourceId: zitadelAppId };
|
|
2858
2964
|
}
|
|
2859
2965
|
/** The platform's sign-in project: the shared `platform` project, its id recorded under the sign-in's own scope key. */
|
|
@@ -2886,6 +2992,8 @@ async function provisionPlatformAuth(context, deps, options = {}) {
|
|
|
2886
2992
|
// In-app sign-in: the second login app copies the login's host table (the host's port writes that).
|
|
2887
2993
|
...(signInLogin ? { signInLogin: { bindings: null } } : {}),
|
|
2888
2994
|
mcp,
|
|
2995
|
+
// The shared companies' organisations are granted by each company's registrar step.
|
|
2996
|
+
...(deps.registrarTokenWritable === true ? { registrarOrgIds: [] } : {}),
|
|
2889
2997
|
});
|
|
2890
2998
|
}
|
|
2891
2999
|
/**
|
|
@@ -6161,6 +6269,30 @@ async function authAppsNotRunning(coolify, recordedIds) {
|
|
|
6161
6269
|
}
|
|
6162
6270
|
return false;
|
|
6163
6271
|
}
|
|
6272
|
+
/**
|
|
6273
|
+
* Whether the login app, the in-app login app or the broker still has a fixed container name
|
|
6274
|
+
* (`custom_internal_name`), which stops Coolify's rolling update. `rollingUpdateSettings` clears it on every
|
|
6275
|
+
* run of the step, so this plans the step again for an app made before. A Coolify read error counts as not drifted.
|
|
6276
|
+
*/
|
|
6277
|
+
async function authAppsKeepInternalName(coolify, recordedIds) {
|
|
6278
|
+
for (const scopeKey of [
|
|
6279
|
+
PLATFORM_AUTH_LOGIN_SCOPE_KEY,
|
|
6280
|
+
SIGN_IN_LOGIN_APP_SCOPE_KEY,
|
|
6281
|
+
PLATFORM_AUTH_BROKER_SCOPE_KEY,
|
|
6282
|
+
]) {
|
|
6283
|
+
const appId = recordedIds[scopeKey];
|
|
6284
|
+
if (!appId)
|
|
6285
|
+
continue;
|
|
6286
|
+
try {
|
|
6287
|
+
if ((await coolify.getApplication(appId)).custom_internal_name)
|
|
6288
|
+
return true;
|
|
6289
|
+
}
|
|
6290
|
+
catch {
|
|
6291
|
+
// Nothing logged: the error can carry Coolify's response.
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
return false;
|
|
6295
|
+
}
|
|
6164
6296
|
/**
|
|
6165
6297
|
* Archon's live release webhook secret is missing, or is not the one its last deploy recorded
|
|
6166
6298
|
* (`PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY`): the release hook step wrote it after Archon's
|
|
@@ -7516,6 +7648,17 @@ export function brokerDeployRecordValue(brokerAppId, authAppsRaw) {
|
|
|
7516
7648
|
};
|
|
7517
7649
|
return JSON.stringify(record);
|
|
7518
7650
|
}
|
|
7651
|
+
const LOGIN_PASSKEY_RP_ID_ENV = 'LOGIN_PASSKEY_RP_ID';
|
|
7652
|
+
/** Removes one env from an app when it is live. True when it was removed; false with nothing live or no delete on the port. */
|
|
7653
|
+
async function removeLoginEnv(coolify, appId, key) {
|
|
7654
|
+
if (typeof coolify.deleteEnv !== 'function')
|
|
7655
|
+
return false;
|
|
7656
|
+
const row = (await coolify.listEnvs(appId)).find((e) => e.key === key && !e.is_preview);
|
|
7657
|
+
if (!row?.uuid)
|
|
7658
|
+
return false;
|
|
7659
|
+
await coolify.deleteEnv(appId, row.uuid);
|
|
7660
|
+
return true;
|
|
7661
|
+
}
|
|
7519
7662
|
/** Whether any desired env is missing from the app, or set to another value. */
|
|
7520
7663
|
export async function envsDiffer(coolify, appId, desired) {
|
|
7521
7664
|
const live = new Map((await coolify.listEnvs(appId)).filter((e) => !e.is_preview).map((e) => [e.key, e.value]));
|
|
@@ -8069,7 +8212,7 @@ const EDGE_REPO = 'wtfalch/edge';
|
|
|
8069
8212
|
const PLATFORM_EDGE_PROJECT = 'platform';
|
|
8070
8213
|
const PLATFORM_EDGE_DB_NAME = 'platform-edge-db';
|
|
8071
8214
|
const EDGE_APP_NAME = 'edge';
|
|
8072
|
-
/** The app's
|
|
8215
|
+
/** The app's network alias (#1054: not a `custom_internal_name`, which turns off rolling updates). */
|
|
8073
8216
|
const EDGE_RUNNER_INTERNAL_NAME = 'platform-edge';
|
|
8074
8217
|
/** The runner's port (its Dockerfile: `PORT=3000`, `EXPOSE 3000`). */
|
|
8075
8218
|
const EDGE_PORT = '3000';
|
|
@@ -8274,7 +8417,7 @@ async function provisionPlatformEdge(context, deps) {
|
|
|
8274
8417
|
// port the `waitForNewDeployment` call below already probes internally.
|
|
8275
8418
|
await deps.coolify.updateApplication(appId, {
|
|
8276
8419
|
dockerfile_target_build: 'runner',
|
|
8277
|
-
|
|
8420
|
+
...rollingUpdateSettings(EDGE_RUNNER_INTERNAL_NAME),
|
|
8278
8421
|
git_branch: gitRef,
|
|
8279
8422
|
...healthCheckSettings('/api/health', Number(EDGE_PORT)),
|
|
8280
8423
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The sign-in code mail of a company that runs its own sign-in: the company's name in the sender and
|
|
3
3
|
* the text, the platform's design tokens for the look. ZITADEL's email-code message is built from the
|
|
4
4
|
* instance's custom text and label policy; both are written through `PlatformZitadelMailBranding`
|
|
5
|
-
* (`@wtfalch/providers`
|
|
5
|
+
* (`@wtfalch/providers`), each only when it differs. The code itself (six digits, five minutes)
|
|
6
6
|
* is the instance's email-OTP generator.
|
|
7
7
|
*
|
|
8
8
|
* The look: no company declares a design foundry can read (an `OrgTheme` lives in cms and reaches
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (`SIGNIN_MAIL_LABEL_POLICY`). No logo: ZITADEL takes it as a multipart upload the client does not make.
|
|
11
11
|
*/
|
|
12
12
|
/** Raise it when the text or the look changes, so an applied company plans its sign-in step again. */
|
|
13
|
-
export declare const AUTH_SIGNIN_MAIL_VERSION = "
|
|
13
|
+
export declare const AUTH_SIGNIN_MAIL_VERSION = "3";
|
|
14
14
|
/** `signinMailMarker` of the company name and version last written, recorded after both writes. */
|
|
15
15
|
export declare const PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY = "auth_signin_mail_applied";
|
|
16
16
|
/** The code the mail carries: six digits, valid five minutes (`123-456` in the mail, see `signinMailText`). */
|
|
@@ -18,8 +18,16 @@ export declare const SIGNIN_MAIL_CODE: {
|
|
|
18
18
|
readonly length: 6;
|
|
19
19
|
readonly expirySeconds: 300;
|
|
20
20
|
};
|
|
21
|
-
/** The
|
|
21
|
+
/** The four ZITADEL calls the sign-in mail needs. */
|
|
22
22
|
export interface PlatformZitadelMailBranding {
|
|
23
|
+
/**
|
|
24
|
+
* Renames an organisation, only when its name differs (`PUT /management/v1/orgs/me` as that
|
|
25
|
+
* organisation). The login shows the instance's default organisation's name as its wordmark when the
|
|
26
|
+
* host is bound to no company, so a company's own instance names it after the company.
|
|
27
|
+
*/
|
|
28
|
+
ensureOrgName(orgId: string, name: string): Promise<{
|
|
29
|
+
changed: boolean;
|
|
30
|
+
}>;
|
|
23
31
|
ensureEmailOtpMessageText(language: string, text: {
|
|
24
32
|
title: string;
|
|
25
33
|
preHeader: string;
|
|
@@ -65,6 +73,11 @@ export declare const SIGNIN_MAIL_LANGUAGE = "en";
|
|
|
65
73
|
* holds the code, split in the middle with Go's `slice` (`123-456`), because ZITADEL renders the
|
|
66
74
|
* greeting as the large heading, so the code is the first thing the reader sees. No greeting by
|
|
67
75
|
* name: the login fills names from the email address, so it would read "Wtfalch Wtfalch".
|
|
76
|
+
*
|
|
77
|
+
* Paragraphs: ZITADEL v4.17.3 puts `{{.Text}}` in one `<div>` of its built-in mail, where a line
|
|
78
|
+
* break collapses to a space. It escapes the field (`html/template`), then un-escapes the whole mail
|
|
79
|
+
* before sending it over SMTP (`internal/notification/types/user_email.go:43`,
|
|
80
|
+
* `html.UnescapeString`), so `<p>` tags in the text arrive as tags. There is no mail template API.
|
|
68
81
|
*/
|
|
69
82
|
export declare function signinMailText(companyName: string): {
|
|
70
83
|
title: string;
|
|
@@ -91,7 +104,7 @@ export declare const SIGNIN_MAIL_LABEL_POLICY: {
|
|
|
91
104
|
readonly warnColorDark: "#FF9C8C";
|
|
92
105
|
readonly disableWatermark: true;
|
|
93
106
|
};
|
|
94
|
-
/**
|
|
95
|
-
export declare function applySigninMail(zitadel: PlatformZitadelMailBranding, companyName: string): Promise<{
|
|
107
|
+
/** Names the default organisation after the company, then writes the text, the look and the code, each only when it differs. Throws the client's fixed-text error. */
|
|
108
|
+
export declare function applySigninMail(zitadel: PlatformZitadelMailBranding, companyName: string, defaultOrgId: string): Promise<{
|
|
96
109
|
changed: boolean;
|
|
97
110
|
}>;
|
package/dist/plan/signin-mail.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The sign-in code mail of a company that runs its own sign-in: the company's name in the sender and
|
|
3
3
|
* the text, the platform's design tokens for the look. ZITADEL's email-code message is built from the
|
|
4
4
|
* instance's custom text and label policy; both are written through `PlatformZitadelMailBranding`
|
|
5
|
-
* (`@wtfalch/providers`
|
|
5
|
+
* (`@wtfalch/providers`), each only when it differs. The code itself (six digits, five minutes)
|
|
6
6
|
* is the instance's email-OTP generator.
|
|
7
7
|
*
|
|
8
8
|
* The look: no company declares a design foundry can read (an `OrgTheme` lives in cms and reaches
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (`SIGNIN_MAIL_LABEL_POLICY`). No logo: ZITADEL takes it as a multipart upload the client does not make.
|
|
11
11
|
*/
|
|
12
12
|
/** Raise it when the text or the look changes, so an applied company plans its sign-in step again. */
|
|
13
|
-
export const AUTH_SIGNIN_MAIL_VERSION = '
|
|
13
|
+
export const AUTH_SIGNIN_MAIL_VERSION = '3';
|
|
14
14
|
/** `signinMailMarker` of the company name and version last written, recorded after both writes. */
|
|
15
15
|
export const PLATFORM_AUTH_SIGNIN_MAIL_SCOPE_KEY = 'auth_signin_mail_applied';
|
|
16
16
|
/** The code the mail carries: six digits, valid five minutes (`123-456` in the mail, see `signinMailText`). */
|
|
@@ -37,6 +37,11 @@ export const SIGNIN_MAIL_LANGUAGE = 'en';
|
|
|
37
37
|
* holds the code, split in the middle with Go's `slice` (`123-456`), because ZITADEL renders the
|
|
38
38
|
* greeting as the large heading, so the code is the first thing the reader sees. No greeting by
|
|
39
39
|
* name: the login fills names from the email address, so it would read "Wtfalch Wtfalch".
|
|
40
|
+
*
|
|
41
|
+
* Paragraphs: ZITADEL v4.17.3 puts `{{.Text}}` in one `<div>` of its built-in mail, where a line
|
|
42
|
+
* break collapses to a space. It escapes the field (`html/template`), then un-escapes the whole mail
|
|
43
|
+
* before sending it over SMTP (`internal/notification/types/user_email.go:43`,
|
|
44
|
+
* `html.UnescapeString`), so `<p>` tags in the text arrive as tags. There is no mail template API.
|
|
40
45
|
*/
|
|
41
46
|
export function signinMailText(companyName) {
|
|
42
47
|
const name = signinMailName(companyName);
|
|
@@ -45,7 +50,7 @@ export function signinMailText(companyName) {
|
|
|
45
50
|
preHeader: `Your ${name} sign-in code`,
|
|
46
51
|
subject: `Your ${name} sign-in code`,
|
|
47
52
|
greeting: '{{slice .OTP 0 3}}-{{slice .OTP 3}}',
|
|
48
|
-
text:
|
|
53
|
+
text: `<p style="margin:0 0 12px 0">Enter this code to sign in. It works for five minutes.</p><p style="margin:0">Or use the button to sign in on this device.</p>`,
|
|
49
54
|
buttonText: 'Sign in',
|
|
50
55
|
footerText: name,
|
|
51
56
|
};
|
|
@@ -66,10 +71,13 @@ export const SIGNIN_MAIL_LABEL_POLICY = {
|
|
|
66
71
|
warnColorDark: '#FF9C8C',
|
|
67
72
|
disableWatermark: true,
|
|
68
73
|
};
|
|
69
|
-
/**
|
|
70
|
-
export async function applySigninMail(zitadel, companyName) {
|
|
74
|
+
/** Names the default organisation after the company, then writes the text, the look and the code, each only when it differs. Throws the client's fixed-text error. */
|
|
75
|
+
export async function applySigninMail(zitadel, companyName, defaultOrgId) {
|
|
76
|
+
const name = await zitadel.ensureOrgName(defaultOrgId, signinMailName(companyName));
|
|
71
77
|
const text = await zitadel.ensureEmailOtpMessageText(SIGNIN_MAIL_LANGUAGE, signinMailText(companyName));
|
|
72
78
|
const look = await zitadel.ensureLabelPolicy({ ...SIGNIN_MAIL_LABEL_POLICY });
|
|
73
79
|
const code = await zitadel.ensureEmailOtpGenerator({ ...SIGNIN_MAIL_CODE });
|
|
74
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
changed: name.changed || text.changed || look.changed || code.changed,
|
|
82
|
+
};
|
|
75
83
|
}
|
|
@@ -20,4 +20,4 @@ export interface ZitadelLoginPolicyOptions {
|
|
|
20
20
|
* policy (POST), so a later change to the default cannot turn registration
|
|
21
21
|
* back on. A custom policy keeps its own factors and IdPs (PUT).
|
|
22
22
|
*/
|
|
23
|
-
export declare function createZitadelLoginPolicy(options: ZitadelLoginPolicyOptions): Pick<ZitadelPort, 'closeSelfRegistration' | 'enforceSecondFactor'
|
|
23
|
+
export declare function createZitadelLoginPolicy(options: ZitadelLoginPolicyOptions): Required<Pick<ZitadelPort, 'closeSelfRegistration' | 'enforceSecondFactor' | 'setCustomersLoginPolicy'>>;
|
|
@@ -29,7 +29,22 @@ const SECOND_FACTOR_SETTINGS = {
|
|
|
29
29
|
passwordlessType: 'PASSWORDLESS_TYPE_ALLOWED',
|
|
30
30
|
mfaInitSkipLifetime: '0s',
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* What {@link ZitadelPort.setCustomersLoginPolicy} sets: a superset of what any customer app needs, so one
|
|
34
|
+
* policy serves every app of the customers org. `allowRegister` is not here: `customers.signUp` owns it.
|
|
35
|
+
*/
|
|
36
|
+
const CUSTOMERS_POLICY_SETTINGS = {
|
|
37
|
+
forceMfa: false,
|
|
38
|
+
allowExternalIdp: false,
|
|
39
|
+
allowUsernamePassword: true,
|
|
40
|
+
passwordlessType: 'PASSWORDLESS_TYPE_ALLOWED',
|
|
41
|
+
};
|
|
32
42
|
const SECOND_FACTORS = ['SECOND_FACTOR_TYPE_OTP', 'SECOND_FACTOR_TYPE_U2F'];
|
|
43
|
+
/**
|
|
44
|
+
* The customers policy also keeps the emailed code as a second factor, and never removes it: the email-code
|
|
45
|
+
* session check may need it. The login app narrows the factors it offers an app.
|
|
46
|
+
*/
|
|
47
|
+
const CUSTOMERS_SECOND_FACTORS = [...SECOND_FACTORS, 'SECOND_FACTOR_TYPE_OTP_EMAIL'];
|
|
33
48
|
const MULTI_FACTORS = ['MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION'];
|
|
34
49
|
/** ZITADEL leaves false booleans and zero durations out of a read. */
|
|
35
50
|
const isZeroDuration = (value) => value === undefined || (typeof value === 'string' && /^0+(\.0+)?s$/.test(value));
|
|
@@ -90,56 +105,67 @@ export function createZitadelLoginPolicy(options) {
|
|
|
90
105
|
}
|
|
91
106
|
return settings;
|
|
92
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* Makes the org's login policy hold `want` and exactly these factor lists, writing nothing when it already
|
|
110
|
+
* does. Any setting not in `want` is copied back as it is. An org on the instance default gets its own
|
|
111
|
+
* policy (POST); a custom one is changed by PUT, then by adding the missing factors before removing the
|
|
112
|
+
* extra ones, so the org is never left with no second factor it needs.
|
|
113
|
+
*/
|
|
114
|
+
async function conform(orgId, want, wantSecond, wantMulti) {
|
|
115
|
+
const data = await call('GET', orgId, undefined);
|
|
116
|
+
const policy = data.policy ?? {};
|
|
117
|
+
const isDefault = data.isDefault === true || policy.isDefault === true;
|
|
118
|
+
const secondFactors = stringList(policy.secondFactors);
|
|
119
|
+
const multiFactors = stringList(policy.multiFactors);
|
|
120
|
+
const settingsDiffer = Object.entries(want).some(([key, value]) => typeof value === 'boolean'
|
|
121
|
+
? (policy[key] === true) !== value
|
|
122
|
+
: value === '0s'
|
|
123
|
+
? !isZeroDuration(policy[key])
|
|
124
|
+
: policy[key] !== value);
|
|
125
|
+
const factorsDiffer = !sameSet(secondFactors, wantSecond) || !sameSet(multiFactors, wantMulti);
|
|
126
|
+
if (!isDefault && !settingsDiffer && !factorsDiffer)
|
|
127
|
+
return { changed: false };
|
|
128
|
+
const settings = { ...settingsOf(policy), ...want };
|
|
129
|
+
if (isDefault) {
|
|
130
|
+
// The org's own policy, written whole: the factor lists go in the same body.
|
|
131
|
+
await call('POST', orgId, {
|
|
132
|
+
...settings,
|
|
133
|
+
secondFactors: wantSecond,
|
|
134
|
+
multiFactors: wantMulti,
|
|
135
|
+
idps: (policy.idps ?? []).map((idp) => ({
|
|
136
|
+
idpId: idp.idpId,
|
|
137
|
+
ownerType: 'IDP_OWNER_TYPE_SYSTEM',
|
|
138
|
+
})),
|
|
139
|
+
});
|
|
140
|
+
return { changed: true };
|
|
141
|
+
}
|
|
142
|
+
if (settingsDiffer)
|
|
143
|
+
await call('PUT', orgId, settings);
|
|
144
|
+
const factorPath = '/management/v1/policies/login';
|
|
145
|
+
for (const type of wantSecond.filter((t) => !secondFactors.includes(t))) {
|
|
146
|
+
await call('POST', orgId, { type }, `${factorPath}/second_factors`);
|
|
147
|
+
}
|
|
148
|
+
for (const type of wantMulti.filter((t) => !multiFactors.includes(t))) {
|
|
149
|
+
await call('POST', orgId, { type }, `${factorPath}/multi_factors`);
|
|
150
|
+
}
|
|
151
|
+
for (const type of secondFactors.filter((t) => !wantSecond.includes(t))) {
|
|
152
|
+
await call('DELETE', orgId, undefined, `${factorPath}/second_factors/${encodeURIComponent(type)}`);
|
|
153
|
+
}
|
|
154
|
+
for (const type of multiFactors.filter((t) => !wantMulti.includes(t))) {
|
|
155
|
+
await call('DELETE', orgId, undefined, `${factorPath}/multi_factors/${encodeURIComponent(type)}`);
|
|
156
|
+
}
|
|
157
|
+
return { changed: true };
|
|
158
|
+
}
|
|
93
159
|
return {
|
|
94
160
|
async enforceSecondFactor(orgId) {
|
|
95
161
|
if (!orgId)
|
|
96
162
|
throw new Error('enforceSecondFactor: orgId is required');
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const settingsDiffer = (policy.forceMfa === true) !== want.forceMfa ||
|
|
104
|
-
(policy.allowRegister === true) !== want.allowRegister ||
|
|
105
|
-
(policy.allowExternalIdp === true) !== want.allowExternalIdp ||
|
|
106
|
-
(policy.allowUsernamePassword === true) !== want.allowUsernamePassword ||
|
|
107
|
-
policy.passwordlessType !== want.passwordlessType ||
|
|
108
|
-
!isZeroDuration(policy.mfaInitSkipLifetime);
|
|
109
|
-
const factorsDiffer = !sameSet(secondFactors, SECOND_FACTORS) || !sameSet(multiFactors, MULTI_FACTORS);
|
|
110
|
-
if (!isDefault && !settingsDiffer && !factorsDiffer)
|
|
111
|
-
return { changed: false };
|
|
112
|
-
const settings = { ...settingsOf(policy), ...want };
|
|
113
|
-
if (isDefault) {
|
|
114
|
-
// The org's own policy, written whole: the factor lists go in the same body.
|
|
115
|
-
await call('POST', orgId, {
|
|
116
|
-
...settings,
|
|
117
|
-
secondFactors: SECOND_FACTORS,
|
|
118
|
-
multiFactors: MULTI_FACTORS,
|
|
119
|
-
idps: (policy.idps ?? []).map((idp) => ({
|
|
120
|
-
idpId: idp.idpId,
|
|
121
|
-
ownerType: 'IDP_OWNER_TYPE_SYSTEM',
|
|
122
|
-
})),
|
|
123
|
-
});
|
|
124
|
-
return { changed: true };
|
|
125
|
-
}
|
|
126
|
-
if (settingsDiffer)
|
|
127
|
-
await call('PUT', orgId, settings);
|
|
128
|
-
// Add before remove, so the org is never left with no second factor.
|
|
129
|
-
const factorPath = '/management/v1/policies/login';
|
|
130
|
-
for (const type of SECOND_FACTORS.filter((t) => !secondFactors.includes(t))) {
|
|
131
|
-
await call('POST', orgId, { type }, `${factorPath}/second_factors`);
|
|
132
|
-
}
|
|
133
|
-
for (const type of MULTI_FACTORS.filter((t) => !multiFactors.includes(t))) {
|
|
134
|
-
await call('POST', orgId, { type }, `${factorPath}/multi_factors`);
|
|
135
|
-
}
|
|
136
|
-
for (const type of secondFactors.filter((t) => !SECOND_FACTORS.includes(t))) {
|
|
137
|
-
await call('DELETE', orgId, undefined, `${factorPath}/second_factors/${encodeURIComponent(type)}`);
|
|
138
|
-
}
|
|
139
|
-
for (const type of multiFactors.filter((t) => !MULTI_FACTORS.includes(t))) {
|
|
140
|
-
await call('DELETE', orgId, undefined, `${factorPath}/multi_factors/${encodeURIComponent(type)}`);
|
|
141
|
-
}
|
|
142
|
-
return { changed: true };
|
|
163
|
+
return conform(orgId, SECOND_FACTOR_SETTINGS, SECOND_FACTORS, MULTI_FACTORS);
|
|
164
|
+
},
|
|
165
|
+
async setCustomersLoginPolicy(orgId) {
|
|
166
|
+
if (!orgId)
|
|
167
|
+
throw new Error('setCustomersLoginPolicy: orgId is required');
|
|
168
|
+
return conform(orgId, CUSTOMERS_POLICY_SETTINGS, CUSTOMERS_SECOND_FACTORS, MULTI_FACTORS);
|
|
143
169
|
},
|
|
144
170
|
async closeSelfRegistration(orgId) {
|
|
145
171
|
if (!orgId)
|
|
@@ -78,6 +78,13 @@ export declare class ZitadelLoginClientRoleConflictError extends Error {
|
|
|
78
78
|
}
|
|
79
79
|
/** The read-only organisation role a directory reader holds. It lists the organisation's users and changes nothing. */
|
|
80
80
|
export declare const ORG_DIRECTORY_READER_ROLE = "ORG_OWNER_VIEWER";
|
|
81
|
+
/**
|
|
82
|
+
* Boule removes a customer with this credential (boule#495). ZITADEL's narrowest built-in organisation role
|
|
83
|
+
* that holds `user.delete` is `ORG_USER_MANAGER`; it cannot be cut down to delete alone (it also lets the
|
|
84
|
+
* holder change users in that organisation). It is granted in the customers organisation only, never in the
|
|
85
|
+
* company's.
|
|
86
|
+
*/
|
|
87
|
+
export declare const ORG_DIRECTORY_USER_MANAGER_ROLE = "ORG_USER_MANAGER";
|
|
81
88
|
/**
|
|
82
89
|
* C3: thrown by `mintOrgDirectoryReader` when the user already exists with an organisation role other
|
|
83
90
|
* than the read-only one. That user was not made by foundry; granting it more or handing out its PAT
|
|
@@ -88,6 +95,12 @@ export declare class ZitadelDirectoryRoleConflictError extends Error {
|
|
|
88
95
|
readonly roles: readonly string[];
|
|
89
96
|
constructor(userId: string, roles: readonly string[]);
|
|
90
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* The organisation role of the broker's registrar user in each organisation it serves: it may make
|
|
100
|
+
* projects there, and a project it makes it owns. No member, user or login-policy rights, which
|
|
101
|
+
* `ORG_OWNER` would add.
|
|
102
|
+
*/
|
|
103
|
+
export declare const REGISTRAR_ROLE = "ORG_PROJECT_CREATOR";
|
|
91
104
|
export interface ZitadelMinterOptions {
|
|
92
105
|
/** The new instance's base URL, e.g. `https://auth.companyincluded.com`. */
|
|
93
106
|
url: string;
|
|
@@ -96,4 +109,4 @@ export interface ZitadelMinterOptions {
|
|
|
96
109
|
fetchImpl?: typeof fetch;
|
|
97
110
|
instanceHost?: string;
|
|
98
111
|
}
|
|
99
|
-
export declare function createZitadelAdminMinter(options: ZitadelMinterOptions): Pick<ZitadelPort, 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'mintOrgDirectoryReader' | 'revokeOrgDirectoryReader'>;
|
|
112
|
+
export declare function createZitadelAdminMinter(options: ZitadelMinterOptions): Pick<ZitadelPort, 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'mintOrgDirectoryReader' | 'revokeOrgDirectoryReader' | 'ensureOrgDirectoryRoles' | 'ensureRegistrarUser' | 'mintRegistrarToken'>;
|