@wtfalch/foundry 0.71.0 → 0.72.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/index.d.ts +1 -1
- package/dist/modules/sign-in.js +81 -9
- package/dist/modules/types.d.ts +2 -0
- package/dist/native-build.d.ts +4 -1
- package/dist/native-build.js +8 -2
- 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
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1718,7 +1718,9 @@ export class FoundryPlanApply {
|
|
|
1718
1718
|
oikosZoneReady: platformReadiness.oikosIngressApplied === true,
|
|
1719
1719
|
ownMailKeyHeld: await this.ownMailKeyHeld(org),
|
|
1720
1720
|
ownMailKeyMintable: this.executor.ownMailKeyMintable?.() ?? false,
|
|
1721
|
+
customersLoginPolicy: this.executor.customersLoginPolicyAvailable?.() ?? true,
|
|
1721
1722
|
customersDirectory: this.executor.customersDirectoryAvailable?.() ?? false,
|
|
1723
|
+
customersDirectoryRoles: this.executor.customersDirectoryRolesAvailable?.() ?? false,
|
|
1722
1724
|
bouleArchonCredential: this.executor.bouleArchonCredentialAvailable?.() ?? false,
|
|
1723
1725
|
...(blueGreenServers ? { blueGreenServers } : {}),
|
|
1724
1726
|
...(stageSinkDown ? { stageSinkDown } : {}),
|
|
@@ -1741,6 +1743,10 @@ export class FoundryPlanApply {
|
|
|
1741
1743
|
ownSigninDbKeysPuttable: this.executor.ownSigninDbKeysPuttable?.() ?? true,
|
|
1742
1744
|
ownSigninDbBackupConfigured: this.executor.ownSigninDbBackupConfigured?.() ?? true,
|
|
1743
1745
|
ownSigninMailWritable: this.executor.ownSigninMailWritable?.() ?? true,
|
|
1746
|
+
...(await this.ownAuthHeadFor(org, platformReadiness)),
|
|
1747
|
+
registrarGrants: this.executor.registrarGrantsAvailable?.() ?? false,
|
|
1748
|
+
ownRegistrarTokenWritable: this.executor.ownRegistrarTokenWritable?.(),
|
|
1749
|
+
ownRegistrarTokenHeld: await this.ownRegistrarTokenHeld(org),
|
|
1744
1750
|
...(this.backupGate
|
|
1745
1751
|
? {
|
|
1746
1752
|
backupStorageUuid: (await this.platformDeps?.store.getServiceIds())?.[PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY] || null,
|
|
@@ -1964,6 +1970,14 @@ export class FoundryPlanApply {
|
|
|
1964
1970
|
}
|
|
1965
1971
|
return { store: Boolean(heldKeys), refs, differs, unreadable, invalid };
|
|
1966
1972
|
}
|
|
1973
|
+
/** #886: the head of an own org's declared auth branch, or nothing when the org is not own or the executor has no read. */
|
|
1974
|
+
async ownAuthHeadFor(org, readiness) {
|
|
1975
|
+
const gitRef = readiness.serviceGitRefs?.auth;
|
|
1976
|
+
if (!ownsServices(org) || !gitRef || !this.executor.ownAuthHead)
|
|
1977
|
+
return {};
|
|
1978
|
+
const head = await this.executor.ownAuthHead(gitRef);
|
|
1979
|
+
return head ? { ownAuthHead: head } : {};
|
|
1980
|
+
}
|
|
1967
1981
|
/** An own org's Lettermint key is held. A resolver that throws counts as not held. */
|
|
1968
1982
|
async ownMailKeyHeld(org) {
|
|
1969
1983
|
const held = this.platformDeps?.heldKeys;
|
|
@@ -1971,6 +1985,13 @@ export class FoundryPlanApply {
|
|
|
1971
1985
|
return false;
|
|
1972
1986
|
return (await heldOwnKeyRefs(held, org)).has(orgTarget(org).refs.brokerMailKey);
|
|
1973
1987
|
}
|
|
1988
|
+
/** An own org's registrar token is held (a rerun after a crash delivers it instead of blocking). */
|
|
1989
|
+
async ownRegistrarTokenHeld(org) {
|
|
1990
|
+
const held = this.platformDeps?.heldKeys;
|
|
1991
|
+
if (!ownsServices(org) || !held)
|
|
1992
|
+
return false;
|
|
1993
|
+
return (await heldOwnKeyRefs(held, org)).has(orgTarget(org).refs.registrarToken);
|
|
1994
|
+
}
|
|
1974
1995
|
/** Which of an own org's undelivered-secret refs hold a value now, so a rerun resumes instead of blocking. */
|
|
1975
1996
|
async ownResumableRefs(org) {
|
|
1976
1997
|
const held = this.platformDeps?.heldKeys;
|
|
@@ -70,16 +70,18 @@ export declare const AUTH_TUNNEL_ROUTES_VERSION = "2";
|
|
|
70
70
|
export declare const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = "auth_tunnel_routes_applied";
|
|
71
71
|
/**
|
|
72
72
|
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
73
|
-
* `LETTERMINT_API_TOKEN`). Raise it when a key is added or its meaning changes, so an applied
|
|
73
|
+
* `LETTERMINT_API_TOKEN`, `LOGIN_PASSKEY_RP_ID`). Raise it when a key is added or its meaning changes, so an applied
|
|
74
74
|
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
75
75
|
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
76
|
+
* 2: a company's login apps get `LOGIN_PASSKEY_RP_ID` (foundry 0.70.2).
|
|
76
77
|
*/
|
|
77
|
-
export declare const AUTH_LOGIN_ENV_VERSION = "
|
|
78
|
+
export declare const AUTH_LOGIN_ENV_VERSION = "2";
|
|
78
79
|
/**
|
|
79
|
-
* What the login apps' env is written for: the version
|
|
80
|
-
*
|
|
80
|
+
* What the login apps' env is written for: the version, the sender address (`LOGIN_MAIL_FROM`) and the
|
|
81
|
+
* passkey RP ID (`LOGIN_PASSKEY_RP_ID`, none for the platform and a company without its own domain).
|
|
82
|
+
* Neither is a secret; a new mail domain or a changed RP ID gives a new marker, so the step is planned again.
|
|
81
83
|
*/
|
|
82
|
-
export declare function authLoginEnvMarker(smtpSender: string): string;
|
|
84
|
+
export declare function authLoginEnvMarker(smtpSender: string, passkeyRpId?: string): string;
|
|
83
85
|
/** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
|
|
84
86
|
export declare const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = "auth_login_env_applied";
|
|
85
87
|
/**
|
|
@@ -1327,6 +1329,8 @@ export interface PlanPlatformOptions {
|
|
|
1327
1329
|
authMailKeyHeld?: boolean;
|
|
1328
1330
|
/** C3: why the owner's invite cannot be made (no work address, no mail port). The rest of the step still plans. */
|
|
1329
1331
|
authOwnerInvite?: string;
|
|
1332
|
+
/** The broker's registrar token was minted and never held: reported, nothing planned for it. */
|
|
1333
|
+
authRegistrar?: string;
|
|
1330
1334
|
files?: string;
|
|
1331
1335
|
integrations?: string;
|
|
1332
1336
|
ai?: string;
|
|
@@ -77,17 +77,19 @@ export const AUTH_TUNNEL_ROUTES_VERSION = '2';
|
|
|
77
77
|
export const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = 'auth_tunnel_routes_applied';
|
|
78
78
|
/**
|
|
79
79
|
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
80
|
-
* `LETTERMINT_API_TOKEN`). Raise it when a key is added or its meaning changes, so an applied
|
|
80
|
+
* `LETTERMINT_API_TOKEN`, `LOGIN_PASSKEY_RP_ID`). Raise it when a key is added or its meaning changes, so an applied
|
|
81
81
|
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
82
82
|
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
83
|
+
* 2: a company's login apps get `LOGIN_PASSKEY_RP_ID` (foundry 0.70.2).
|
|
83
84
|
*/
|
|
84
|
-
export const AUTH_LOGIN_ENV_VERSION = '
|
|
85
|
+
export const AUTH_LOGIN_ENV_VERSION = '2';
|
|
85
86
|
/**
|
|
86
|
-
* What the login apps' env is written for: the version
|
|
87
|
-
*
|
|
87
|
+
* What the login apps' env is written for: the version, the sender address (`LOGIN_MAIL_FROM`) and the
|
|
88
|
+
* passkey RP ID (`LOGIN_PASSKEY_RP_ID`, none for the platform and a company without its own domain).
|
|
89
|
+
* Neither is a secret; a new mail domain or a changed RP ID gives a new marker, so the step is planned again.
|
|
88
90
|
*/
|
|
89
|
-
export function authLoginEnvMarker(smtpSender) {
|
|
90
|
-
return `${AUTH_LOGIN_ENV_VERSION}:${smtpSender}`;
|
|
91
|
+
export function authLoginEnvMarker(smtpSender, passkeyRpId) {
|
|
92
|
+
return `${AUTH_LOGIN_ENV_VERSION}:${smtpSender}${passkeyRpId ? `:${passkeyRpId}` : ''}`;
|
|
91
93
|
}
|
|
92
94
|
/** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
|
|
93
95
|
export const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = 'auth_login_env_applied';
|
|
@@ -1553,7 +1555,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1553
1555
|
// reconcile below can be planned in the *same* round hc first comes up,
|
|
1554
1556
|
// the same way files/mail/archon are planned in auth's own first round.
|
|
1555
1557
|
const hcReady = hc.step !== null || Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_HC_KEY)]);
|
|
1556
|
-
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false, blocked.authMailKeyHeld ?? true);
|
|
1558
|
+
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false, blocked.authMailKeyHeld ?? true, blocked.authRegistrar);
|
|
1557
1559
|
if (auth.step)
|
|
1558
1560
|
plannedSteps.push(auth.step);
|
|
1559
1561
|
// #174: the integrations host. Planned after auth; neither step reads the
|
|
@@ -2229,11 +2231,15 @@ function planSfu(platform, callsOrgSlugs, recordedIds) {
|
|
|
2229
2231
|
* promising "this apply re-mints" while one of those is still missing would
|
|
2230
2232
|
* be wrong the moment it is read.
|
|
2231
2233
|
*/
|
|
2232
|
-
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false, mailKeyHeld = true) {
|
|
2234
|
+
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false, mailKeyHeld = true, registrarBlocked) {
|
|
2233
2235
|
if (!platform.auth.enabled)
|
|
2234
2236
|
return { step: null, blocked: null };
|
|
2235
|
-
// C3/S2: standing notes. The invite and the domain wait on a person; they hold nothing else back.
|
|
2236
|
-
const standing = [
|
|
2237
|
+
// C3/S2: standing notes. The invite, the registrar token and the domain wait on a person; they hold nothing else back.
|
|
2238
|
+
const standing = [
|
|
2239
|
+
ownerInviteBlocked,
|
|
2240
|
+
registrarBlocked,
|
|
2241
|
+
ownerDomainBlockedText(recordedIds),
|
|
2242
|
+
].filter((t) => Boolean(t));
|
|
2237
2243
|
const standingNote = standing.length > 0 ? standing.join('; ') : null;
|
|
2238
2244
|
const key = resourceKey('coolify', 'app', PLATFORM_AUTH_KEY);
|
|
2239
2245
|
const connectorApplied = Boolean(recordedIds[PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY]);
|
|
@@ -145,11 +145,16 @@ export interface PlatformAuthDeps {
|
|
|
145
145
|
* and the planner reports it. The port may be a proxy that answers any method name, so the host says it.
|
|
146
146
|
*/
|
|
147
147
|
signinMailWritable?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* The host's ZITADEL port has `ensureRegistrarUser` and `mintRegistrarToken` (the broker's registrar token,
|
|
150
|
+
* `plan/broker-registrar-token.ts`). Only `true` makes it; otherwise the planner reports it blocked.
|
|
151
|
+
*/
|
|
152
|
+
registrarTokenWritable?: boolean;
|
|
148
153
|
/** Overrides the Lettermint SMTP relay ZITADEL is given. Absent, Lettermint exactly as before. */
|
|
149
154
|
smtp?: PlatformAuthSmtp;
|
|
150
155
|
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'> &
|
|
151
156
|
/** An own org's sign-in database needs a volume and the CA file mount (#746); the platform's does not. */
|
|
152
|
-
Partial<Pick<CoolifyPort, 'listStorages' | 'createPersistentStorage' | 'createFileStorage'>>;
|
|
157
|
+
Partial<Pick<CoolifyPort, 'listStorages' | 'createPersistentStorage' | 'createFileStorage' | 'deleteEnv'>>;
|
|
153
158
|
/** F4.dns: every DNS record call, through edge at its recorded address (see {@link PlatformDnsFactory}). */
|
|
154
159
|
dns: PlatformDnsFactory;
|
|
155
160
|
/** F4.tunnels: every tunnel call, through edge at its recorded address (see {@link PlatformTunnelsFactory}). */
|
|
@@ -161,7 +166,12 @@ export interface PlatformAuthDeps {
|
|
|
161
166
|
requestHeaders?: PlatformRequestHeadersFactory;
|
|
162
167
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
163
168
|
/** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
|
|
164
|
-
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'enforceSecondFactor' | 'inviteOrgOwner' | 'ensureDomainPolicy' | 'addOrgDomain' | 'removeOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp & Partial<PlatformZitadelMailBranding
|
|
169
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'enforceSecondFactor' | 'inviteOrgOwner' | 'ensureDomainPolicy' | 'addOrgDomain' | 'removeOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp & Partial<Pick<ZitadelPort, 'ensureRegistrarUser' | 'mintRegistrarToken'>> & Partial<PlatformZitadelMailBranding> & {
|
|
170
|
+
getOrg?: (id: string) => Promise<{
|
|
171
|
+
id: string;
|
|
172
|
+
name: string;
|
|
173
|
+
}>;
|
|
174
|
+
};
|
|
165
175
|
/**
|
|
166
176
|
* Mails the owner's first-sign-in link to their personal address (the same
|
|
167
177
|
* port as `FoundryStepExecutorPorts.ownerInviteMail`). The link carries the
|
|
@@ -411,6 +421,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
411
421
|
authOtherInputsMissing?: string[];
|
|
412
422
|
authMailKeyHeld?: boolean;
|
|
413
423
|
authOwnerInvite?: string;
|
|
424
|
+
authRegistrar?: string;
|
|
414
425
|
files?: string;
|
|
415
426
|
integrations?: string;
|
|
416
427
|
ai?: string;
|
|
@@ -575,6 +586,7 @@ export declare const AUTH_BROKER_MCP_PATH_PATTERN = "^/(\\.well-known/oauth-auth
|
|
|
575
586
|
export declare const PLATFORM_AUTH_DB_SCOPE_KEY = "coolify_platform_auth_database";
|
|
576
587
|
export declare const PLATFORM_AUTH_ZITADEL_SCOPE_KEY = "coolify_platform_auth_zitadel_app";
|
|
577
588
|
export declare const PLATFORM_AUTH_LOGIN_SCOPE_KEY = "coolify_platform_auth_login_app";
|
|
589
|
+
export declare const PLATFORM_AUTH_ORG_NAME = "foundry";
|
|
578
590
|
/**
|
|
579
591
|
* #283: the ref the new instance's IAM_OWNER machine token (a PAT) is
|
|
580
592
|
* delivered under, once. A host reads it back to build `zitadelForInstance`
|
|
@@ -600,6 +612,11 @@ export interface AuthStackInput {
|
|
|
600
612
|
network: ServiceNetwork;
|
|
601
613
|
/** The ZITADEL SMTP sender address. */
|
|
602
614
|
smtpSender: string;
|
|
615
|
+
/**
|
|
616
|
+
* `LOGIN_PASSKEY_RP_ID` for both login apps: a bare domain every host they serve sits under (the login
|
|
617
|
+
* refuses any other). Absent: unset, and each passkey belongs to the host it was made on.
|
|
618
|
+
*/
|
|
619
|
+
passkeyRpId?: string;
|
|
603
620
|
/** Where the login app sends a user with no sign-in request in flight, or null to leave it unset. */
|
|
604
621
|
defaultRedirectUri: string | null;
|
|
605
622
|
/** The company whose owner is invited into its own sign-in org; null for a company's own stack. */
|
|
@@ -620,6 +637,11 @@ export interface AuthStackInput {
|
|
|
620
637
|
mailBranding?: {
|
|
621
638
|
companyName: string;
|
|
622
639
|
};
|
|
640
|
+
/**
|
|
641
|
+
* The broker gets a registrar token (`ZITADEL_REGISTRAR_TOKEN`, `plan/broker-registrar-token.ts`) covering the
|
|
642
|
+
* instance's first organisation and these (the company's sign-in and customers organisations). Absent: none.
|
|
643
|
+
*/
|
|
644
|
+
registrarOrgIds?: readonly string[];
|
|
623
645
|
}
|
|
624
646
|
/**
|
|
625
647
|
* #168: makes `coolify:app` keyed {@link PLATFORM_AUTH_KEY} true -- a Coolify
|
|
@@ -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
|
}>;
|