@wtfalch/foundry 0.26.4 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calls-sfu.d.ts +13 -0
- package/dist/calls-sfu.js +14 -0
- package/dist/index.d.ts +2 -1
- package/dist/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +44 -1
- package/dist/plan/executor.d.ts +28 -0
- package/dist/plan/executor.js +48 -4
- package/dist/plan/index.d.ts +1 -1
- package/dist/plan/plan-apply.d.ts +8 -0
- package/dist/plan/plan-apply.js +4 -0
- package/dist/plan/platform-diff.d.ts +22 -0
- package/dist/plan/platform-diff.js +48 -0
- package/dist/plan/platform-providers.d.ts +18 -1
- package/dist/plan/platform-providers.js +93 -7
- package/dist/plan/providers-readers.d.ts +5 -2
- package/package.json +1 -1
package/dist/calls-sfu.d.ts
CHANGED
|
@@ -154,6 +154,19 @@ export declare const CALLS_SFU_PRIVATE_URL = "http://10.10.10.105:7880";
|
|
|
154
154
|
* hostname out from under it.
|
|
155
155
|
*/
|
|
156
156
|
export declare const CALLS_SFU_PUBLIC_URL = "https://calls-sfu.oikos.host";
|
|
157
|
+
/**
|
|
158
|
+
* A host's own SFU addresses, for a stack where the shared SFU is not the
|
|
159
|
+
* estate's (a local run). Optional on `FoundryStepExecutorPorts`; absent, the
|
|
160
|
+
* two constants above apply unchanged.
|
|
161
|
+
*/
|
|
162
|
+
export interface CallsSfuUrls {
|
|
163
|
+
/** What a browser reaches: becomes the calls service's `LIVEKIT_PUBLIC_URL`. */
|
|
164
|
+
publicUrl: string;
|
|
165
|
+
/** What the calls service reaches: becomes its `LIVEKIT_URL`. */
|
|
166
|
+
privateUrl: string;
|
|
167
|
+
}
|
|
168
|
+
/** The SFU addresses to use: the override, validated, else the constants. Throws naming the bad field. */
|
|
169
|
+
export declare function resolveCallsSfuUrls(override?: CallsSfuUrls): CallsSfuUrls;
|
|
157
170
|
/**
|
|
158
171
|
* Exported for the test file. Reads `ufw status`'s own report rather than
|
|
159
172
|
* assuming the exit code of a blind `ufw delete allow 7880/tcp` -- real
|
package/dist/calls-sfu.js
CHANGED
|
@@ -144,6 +144,20 @@ export const CALLS_SFU_PRIVATE_URL = `http://10.10.10.105:${LIVEKIT_SIGNALLING_P
|
|
|
144
144
|
* hostname out from under it.
|
|
145
145
|
*/
|
|
146
146
|
export const CALLS_SFU_PUBLIC_URL = 'https://calls-sfu.oikos.host';
|
|
147
|
+
/** The SFU addresses to use: the override, validated, else the constants. Throws naming the bad field. */
|
|
148
|
+
export function resolveCallsSfuUrls(override) {
|
|
149
|
+
if (!override)
|
|
150
|
+
return { publicUrl: CALLS_SFU_PUBLIC_URL, privateUrl: CALLS_SFU_PRIVATE_URL };
|
|
151
|
+
for (const field of ['publicUrl', 'privateUrl']) {
|
|
152
|
+
const value = override[field];
|
|
153
|
+
if (typeof value !== 'string' ||
|
|
154
|
+
!URL.canParse(value) ||
|
|
155
|
+
!/^https?:$/.test(new URL(value).protocol)) {
|
|
156
|
+
throw new Error(`callsSfuUrls.${field} must be an absolute http(s) URL`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { publicUrl: override.publicUrl, privateUrl: override.privateUrl };
|
|
160
|
+
}
|
|
147
161
|
/**
|
|
148
162
|
* Matches `ufw status`'s report of the exact wide-open rule PR 125 added
|
|
149
163
|
* (`ufw allow 7880/tcp`) -- "Anywhere" in the From column -- and not the
|
package/dist/index.d.ts
CHANGED
|
@@ -41,13 +41,14 @@ export { checkMailIdentityAudience } from './plan/index.js';
|
|
|
41
41
|
export type { MailAllowlistApp } from './plan/index.js';
|
|
42
42
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
43
43
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
44
|
-
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
|
|
44
|
+
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
|
|
45
45
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
|
|
46
46
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
|
|
47
47
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
|
48
48
|
export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './plan/index.js';
|
|
49
49
|
export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './plan/index.js';
|
|
50
50
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './plan/index.js';
|
|
51
|
+
export type { CallsSfuUrls } from './calls-sfu.js';
|
|
51
52
|
export type { CallsSfuSshSession } from './calls-sfu-ssh-adapter.js';
|
|
52
53
|
export { connectCallsSfuSsh } from './calls-sfu-ssh-client.js';
|
|
53
54
|
export type { CallsSfuSshConnectOptions } from './calls-sfu-ssh-client.js';
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface DraftStep {
|
|
|
43
43
|
export interface PlanOrgOptions {
|
|
44
44
|
/** #267: the executor was built with a `loginHostBindings` port. */
|
|
45
45
|
loginHostBindings?: boolean;
|
|
46
|
+
/** The executor's ZITADEL client has `updateOidcAppIdTokenUserinfoAssertion`; without it `planIdTokenUserinfo` plans nothing. */
|
|
47
|
+
zitadelIdTokenUserinfo?: boolean;
|
|
46
48
|
/** F5: the executor was built with a `broker` port. */
|
|
47
49
|
broker?: boolean;
|
|
48
50
|
/** #161: the slugs of the other declared orgs, so a Boule whose hostname another org's slug would take is blocked. */
|
package/dist/plan/diff.js
CHANGED
|
@@ -12,7 +12,7 @@ import { appHostname, entryHostnames, tunnelDnsHostnames, tunnelDomainList } fro
|
|
|
12
12
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
13
13
|
import { LOGIN_BINDING_SCOPE_KEY, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
14
14
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
15
|
-
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, } from './platform-diff.js';
|
|
15
|
+
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
16
16
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
17
17
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
18
18
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec } from './proxy-labels.js';
|
|
@@ -441,6 +441,10 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
441
441
|
plannedSteps.push(step);
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
|
+
// Last: a repair that fails must not stop any step after it (`runSteps` stops at the first failure).
|
|
445
|
+
const idTokenUserinfo = planIdTokenUserinfo(org, live, recordedIds, plannedSteps, platformReadiness.authReady, options.zitadelIdTokenUserinfo ?? false);
|
|
446
|
+
if (idTokenUserinfo)
|
|
447
|
+
plannedSteps.push(idTokenUserinfo);
|
|
444
448
|
// #10 (resolved): "foundry manages only what the declared org names";
|
|
445
449
|
// anything left unclaimed is listed, never proposed for deletion.
|
|
446
450
|
// A resource foundry made for the org is not a finding: only a resource
|
|
@@ -861,6 +865,45 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
861
865
|
// repoint nothing until the port that writes the binding is wired.
|
|
862
866
|
return loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) ?? stepFor(false);
|
|
863
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Every sign-in app this org already has live (web or native, not api) gets
|
|
870
|
+
* "user info inside the ID token" switched on once: apps made before the
|
|
871
|
+
* setting existed have none, and `@wtfalch/auth` builds the signed-in user
|
|
872
|
+
* from ID token claims only. One `zitadel:id-token-userinfo` step names the
|
|
873
|
+
* apps whose recorded id differs from their marker
|
|
874
|
+
* ({@link zitadelAppIdTokenUserinfoAppliedScopeKey}); a re-created app has a
|
|
875
|
+
* new id, so it is named again. Plans nothing when the host's ZITADEL client
|
|
876
|
+
* lacks the method, before the org and project are recorded, or while
|
|
877
|
+
* the platform's sign-in is not ready.
|
|
878
|
+
*/
|
|
879
|
+
function planIdTokenUserinfo(org, live, recordedIds, plannedSteps, authReady, wired) {
|
|
880
|
+
if (!wired || !authReady || !recordedIds.zitadel_org || !recordedIds.zitadel_project)
|
|
881
|
+
return null;
|
|
882
|
+
const apps = live
|
|
883
|
+
.filter((r) => r.system === 'zitadel' &&
|
|
884
|
+
r.kind === 'app' &&
|
|
885
|
+
(r.config.appType === 'web' || r.config.appType === 'native'))
|
|
886
|
+
.map((r) => r.key)
|
|
887
|
+
// An app this plan recreates gets its marker when it is made.
|
|
888
|
+
.filter((appKey) => !plannedSteps.some((s) => s.action.system === 'zitadel' &&
|
|
889
|
+
s.action.kind === 'app' &&
|
|
890
|
+
s.action.key === appKey &&
|
|
891
|
+
s.action.desired.recreate === true))
|
|
892
|
+
.filter((appKey) => {
|
|
893
|
+
const appId = recordedIds[`zitadel:app:${appKey}`];
|
|
894
|
+
return (Boolean(appId) && recordedIds[zitadelAppIdTokenUserinfoAppliedScopeKey(appKey)] !== appId);
|
|
895
|
+
});
|
|
896
|
+
if (apps.length === 0)
|
|
897
|
+
return null;
|
|
898
|
+
return {
|
|
899
|
+
action: { system: 'zitadel', kind: 'id-token-userinfo', key: org.id, desired: { apps } },
|
|
900
|
+
step: {
|
|
901
|
+
description: `switch on user info in the ID token for ${org.slug}'s sign-in apps (${apps.join(', ')})`,
|
|
902
|
+
destroys: false,
|
|
903
|
+
migrates: false,
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
}
|
|
864
907
|
/** Why a login-host step cannot run yet, or `null` when nothing is missing. */
|
|
865
908
|
function loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) {
|
|
866
909
|
if (!bindingsWired) {
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OrgMailOptions, OrgMailOrg, OrgMailResult } from '@wtfalch/email/postmaster';
|
|
2
|
+
import { type CallsSfuUrls } from '../calls-sfu.js';
|
|
2
3
|
import type { DeclaredOrg, HostingBuildPack } from '../org.js';
|
|
3
4
|
import type { DeclaredOrgStore } from '../store.js';
|
|
4
5
|
import { type BrokerPort } from './broker-entries.js';
|
|
@@ -1009,6 +1010,18 @@ export interface ZitadelPort {
|
|
|
1009
1010
|
* the binding holds; `zitadelApp` never sets it at creation.
|
|
1010
1011
|
*/
|
|
1011
1012
|
updateOidcAppLoginBaseUri(orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
|
|
1013
|
+
/**
|
|
1014
|
+
* Switches on "user info inside the ID token" for an already-created app,
|
|
1015
|
+
* read-modify-write (`@wtfalch/providers`' `ZitadelClient.
|
|
1016
|
+
* updateOidcAppIdTokenUserinfoAssertion`): ZITADEL defaults it to off, and
|
|
1017
|
+
* `@wtfalch/auth` builds the signed-in user from ID token claims only, so
|
|
1018
|
+
* an app made without it has no `email` claim. Resolves `true` when it
|
|
1019
|
+
* wrote the setting, `false` when the app already had it on (no write).
|
|
1020
|
+
* Optional: a host whose client lacks it gets no repair step planned
|
|
1021
|
+
* (`FoundryPlanApplyOptions.zitadelIdTokenUserinfo`), and nothing throws.
|
|
1022
|
+
* `createOidcApp` and `createOidcAppWithSecret` send it on themselves.
|
|
1023
|
+
*/
|
|
1024
|
+
updateOidcAppIdTokenUserinfoAssertion?(orgId: string, projectId: string, appId: string): Promise<boolean>;
|
|
1012
1025
|
}
|
|
1013
1026
|
/**
|
|
1014
1027
|
* #274 fix round 4: bridges `@wtfalch/providers`' real `ZitadelClient.
|
|
@@ -1377,6 +1390,12 @@ export interface FoundryStepExecutorPorts {
|
|
|
1377
1390
|
jobsScheduler?: JobsSchedulerPort;
|
|
1378
1391
|
/** The shared calls SFU's own LiveKit credentials (#145) -- see {@link CallsSfuPort}'s own doc comment. */
|
|
1379
1392
|
callsSfu: CallsSfuPort;
|
|
1393
|
+
/**
|
|
1394
|
+
* The calls service's `LIVEKIT_URL` / `LIVEKIT_PUBLIC_URL`, for a host whose
|
|
1395
|
+
* SFU is not the estate's shared one. Absent, the estate's addresses apply.
|
|
1396
|
+
* Refused (the calls-service step throws) unless both are absolute http(s) URLs.
|
|
1397
|
+
*/
|
|
1398
|
+
callsSfuUrls?: CallsSfuUrls;
|
|
1380
1399
|
/**
|
|
1381
1400
|
* #113: the org's old-mail copy into Stalwart, ahead of a `received` ->
|
|
1382
1401
|
* `hosted` MX cutover -- see {@link MailCopyPort}'s own doc comment.
|
|
@@ -1696,6 +1715,15 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1696
1715
|
* recorded).
|
|
1697
1716
|
*/
|
|
1698
1717
|
private zitadelOwner;
|
|
1718
|
+
/**
|
|
1719
|
+
* Switches on "user info inside the ID token" for each live sign-in app
|
|
1720
|
+
* the planner named. An app's marker (its ZITADEL app id) is recorded and
|
|
1721
|
+
* persisted only after its call resolved to a boolean, so a call that throws
|
|
1722
|
+
* or resolves to anything else leaves no marker and the next plan names the
|
|
1723
|
+
* app again. The error names ids only,
|
|
1724
|
+
* never the cause.
|
|
1725
|
+
*/
|
|
1726
|
+
private zitadelIdTokenUserinfo;
|
|
1699
1727
|
/**
|
|
1700
1728
|
* `plan/diff.ts`'s `planLoginHost` (#267): adds the org's own company
|
|
1701
1729
|
* login host (`action.desired.host`) to the ZITADEL instance's trusted
|
package/dist/plan/executor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { URL } from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveCallsSfuUrls } from '../calls-sfu.js';
|
|
4
4
|
import { APP_ENTRY_KEY, appEntry, filesCollectionEnvName, ownerMailboxRecorded, ownerMailboxes, sharedSecretRef, } from '../org.js';
|
|
5
5
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatIntrospectionAppName, desiredAgoraEnv, entryClientIdScopeKey, } from './agora.js';
|
|
6
6
|
import { BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_DATABASE_SCOPE_KEY, BOULE_DEPLOYED_SCOPE_KEY, bouleEnvDrift, bouleHostnameFor, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
@@ -24,7 +24,7 @@ import { healthCheckSettings } from './health-check-settings.js';
|
|
|
24
24
|
import { appHostname, coolifyTunnelDomains, entryHostnames, tunnelDnsHostnames, tunnelDomainList, tunnelIngressHostnames, } from './hostnames.js';
|
|
25
25
|
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, customersEntryKeys, loginBindingState, loginHostClientIds, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
26
26
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
27
|
-
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appFolderDiffers, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, } from './platform-diff.js';
|
|
27
|
+
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appFolderDiffers, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
28
28
|
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, } from './platform-providers.js';
|
|
29
29
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryProxyLabels, sameLabelSets, } from './proxy-labels.js';
|
|
30
30
|
import { entryWorkerAppliedScopeKey, entryWorkerExclusionsScopeKey, entryWorkerRetiredScopeKey, entryWorkerRouteIdScopeKey, entryWorkerRoutePatternScopeKey, entryWorkerRoutePendingScopeKey, entryWorkerRouteZoneScopeKey, entryWorkerUploadedScopeKey, } from './worker.js';
|
|
@@ -770,6 +770,8 @@ export class FoundryStepExecutor {
|
|
|
770
770
|
return this.zitadelLoginHost(action, org, context);
|
|
771
771
|
case 'zitadel:app':
|
|
772
772
|
return this.zitadelApp(action, org, context);
|
|
773
|
+
case 'zitadel:id-token-userinfo':
|
|
774
|
+
return this.zitadelIdTokenUserinfo(action, org, context);
|
|
773
775
|
case 'zitadel:customers':
|
|
774
776
|
return this.zitadelCustomers(org, context);
|
|
775
777
|
case 'coolify:app':
|
|
@@ -1312,6 +1314,44 @@ export class FoundryStepExecutor {
|
|
|
1312
1314
|
}
|
|
1313
1315
|
return { resourceId: userId };
|
|
1314
1316
|
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Switches on "user info inside the ID token" for each live sign-in app
|
|
1319
|
+
* the planner named. An app's marker (its ZITADEL app id) is recorded and
|
|
1320
|
+
* persisted only after its call resolved to a boolean, so a call that throws
|
|
1321
|
+
* or resolves to anything else leaves no marker and the next plan names the
|
|
1322
|
+
* app again. The error names ids only,
|
|
1323
|
+
* never the cause.
|
|
1324
|
+
*/
|
|
1325
|
+
async zitadelIdTokenUserinfo(action, org, context) {
|
|
1326
|
+
const update = this.ports.zitadel.updateOidcAppIdTokenUserinfoAssertion?.bind(this.ports.zitadel);
|
|
1327
|
+
// The planner plans nothing without the method; a stale plan does nothing.
|
|
1328
|
+
if (!update)
|
|
1329
|
+
return {};
|
|
1330
|
+
const recorded = context.recordedIds();
|
|
1331
|
+
const zitadelOrgId = recorded.zitadel_org;
|
|
1332
|
+
const projectId = recorded.zitadel_project;
|
|
1333
|
+
if (!zitadelOrgId || !projectId) {
|
|
1334
|
+
throw new Error(`zitadel:id-token-userinfo for ${org.slug}: no ZITADEL org and project recorded yet -- the issuer must be provisioned first`);
|
|
1335
|
+
}
|
|
1336
|
+
for (const appKey of action.desired.apps) {
|
|
1337
|
+
const appId = recorded[`zitadel:app:${appKey}`];
|
|
1338
|
+
if (!appId)
|
|
1339
|
+
continue;
|
|
1340
|
+
let done;
|
|
1341
|
+
try {
|
|
1342
|
+
done = await update(zitadelOrgId, projectId, appId);
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
throw new Error(`zitadel:id-token-userinfo for ${org.slug}: switching on user info in the ID token failed for app '${appKey}' (${appId})`);
|
|
1346
|
+
}
|
|
1347
|
+
// A client that answers every property (`packages/local`) resolves to something else: no marker.
|
|
1348
|
+
if (typeof done !== 'boolean')
|
|
1349
|
+
continue;
|
|
1350
|
+
context.recordScopeId(zitadelAppIdTokenUserinfoAppliedScopeKey(appKey), appId);
|
|
1351
|
+
await context.persistScopeIds();
|
|
1352
|
+
}
|
|
1353
|
+
return {};
|
|
1354
|
+
}
|
|
1315
1355
|
/**
|
|
1316
1356
|
* `plan/diff.ts`'s `planLoginHost` (#267): adds the org's own company
|
|
1317
1357
|
* login host (`action.desired.host`) to the ZITADEL instance's trusted
|
|
@@ -1706,6 +1746,8 @@ export class FoundryStepExecutor {
|
|
|
1706
1746
|
// never something an app's env needs.
|
|
1707
1747
|
context.recordScopeId(clientIdScopeKey, created.clientId);
|
|
1708
1748
|
}
|
|
1749
|
+
// Made with the setting on (providers 0.9.0): nothing to repair on this id.
|
|
1750
|
+
context.recordScopeId(zitadelAppIdTokenUserinfoAppliedScopeKey(action.key), created.appId);
|
|
1709
1751
|
return { resourceId: created.appId };
|
|
1710
1752
|
}
|
|
1711
1753
|
/**
|
|
@@ -3771,6 +3813,8 @@ export class FoundryStepExecutor {
|
|
|
3771
3813
|
await this.ports.coolify.raw('POST', `/applications/${appId}/stop`);
|
|
3772
3814
|
return { resourceId: appId };
|
|
3773
3815
|
}
|
|
3816
|
+
// Refused before anything is created, like the missing-stack refusal below.
|
|
3817
|
+
const sfuUrls = resolveCallsSfuUrls(this.ports.callsSfuUrls);
|
|
3774
3818
|
const recorded = context.recordedIds();
|
|
3775
3819
|
const synapseId = recorded[CALLS_SYNAPSE_SCOPE_KEY];
|
|
3776
3820
|
const masId = recorded[CALLS_MAS_SCOPE_KEY];
|
|
@@ -3832,8 +3876,8 @@ export class FoundryStepExecutor {
|
|
|
3832
3876
|
await this.ensureLogVolume(app.appId, org, app.existed);
|
|
3833
3877
|
const bindingSecret = await this.ensureCallsBindingSecret(app.appId);
|
|
3834
3878
|
const envs = [
|
|
3835
|
-
{ key: 'LIVEKIT_URL', value:
|
|
3836
|
-
{ key: 'LIVEKIT_PUBLIC_URL', value:
|
|
3879
|
+
{ key: 'LIVEKIT_URL', value: sfuUrls.privateUrl },
|
|
3880
|
+
{ key: 'LIVEKIT_PUBLIC_URL', value: sfuUrls.publicUrl },
|
|
3837
3881
|
{ key: 'LIVEKIT_API_KEY', value: sfu.apiKey },
|
|
3838
3882
|
{ key: 'LIVEKIT_API_SECRET', value: sfu.apiSecret },
|
|
3839
3883
|
{ key: 'MAS_URL', value: `http://${masHost}:8080/` },
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { AdoptRefusedError } from './adopt.js';
|
|
|
15
15
|
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan-apply.js';
|
|
16
16
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
17
17
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
18
|
-
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './platform-providers.js';
|
|
18
|
+
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './platform-providers.js';
|
|
19
19
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
|
|
20
20
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
21
21
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
@@ -361,6 +361,13 @@ export interface FoundryPlanApplyOptions {
|
|
|
361
361
|
* before zitadel-login's binding can be written.
|
|
362
362
|
*/
|
|
363
363
|
loginHostBindings?: boolean;
|
|
364
|
+
/**
|
|
365
|
+
* True when the host's ZITADEL client has `updateOidcAppIdTokenUserinfoAssertion`
|
|
366
|
+
* (`@wtfalch/providers` at the version that has it). Optional, and false
|
|
367
|
+
* without it: no org plans the step that switches on user info in the ID
|
|
368
|
+
* token of its existing sign-in apps.
|
|
369
|
+
*/
|
|
370
|
+
zitadelIdTokenUserinfo?: boolean;
|
|
364
371
|
/**
|
|
365
372
|
* F5: true when the host's executor was built with a `broker` port.
|
|
366
373
|
* Optional, and false without it: each entry's broker entry is then
|
|
@@ -461,6 +468,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
461
468
|
private readonly agora;
|
|
462
469
|
private readonly jobsScheduler;
|
|
463
470
|
private readonly loginHostBindings;
|
|
471
|
+
private readonly zitadelIdTokenUserinfo;
|
|
464
472
|
private readonly broker;
|
|
465
473
|
private readonly edgeWorkers;
|
|
466
474
|
private readonly hostFiles;
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -121,6 +121,7 @@ export class FoundryPlanApply {
|
|
|
121
121
|
agora;
|
|
122
122
|
jobsScheduler;
|
|
123
123
|
loginHostBindings;
|
|
124
|
+
zitadelIdTokenUserinfo;
|
|
124
125
|
broker;
|
|
125
126
|
edgeWorkers;
|
|
126
127
|
hostFiles;
|
|
@@ -142,6 +143,7 @@ export class FoundryPlanApply {
|
|
|
142
143
|
this.agora = options.agora;
|
|
143
144
|
this.jobsScheduler = options.jobsScheduler ?? false;
|
|
144
145
|
this.loginHostBindings = options.loginHostBindings ?? false;
|
|
146
|
+
this.zitadelIdTokenUserinfo = options.zitadelIdTokenUserinfo ?? false;
|
|
145
147
|
this.broker = options.broker ?? false;
|
|
146
148
|
this.edgeWorkers = options.edgeWorkers ?? false;
|
|
147
149
|
this.hostFiles = options.filesOrganisation ?? false;
|
|
@@ -876,6 +878,7 @@ export class FoundryPlanApply {
|
|
|
876
878
|
const platformHostnames = await this.platformServiceHostnames();
|
|
877
879
|
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, {
|
|
878
880
|
loginHostBindings: this.loginHostBindings,
|
|
881
|
+
zitadelIdTokenUserinfo: this.zitadelIdTokenUserinfo,
|
|
879
882
|
broker: this.broker,
|
|
880
883
|
edgeWorkers: this.edgeWorkers,
|
|
881
884
|
otherSlugs,
|
|
@@ -1301,6 +1304,7 @@ const STEP_FIELDS = {
|
|
|
1301
1304
|
'zitadel:issuer': 'issuer.usesDomain',
|
|
1302
1305
|
'zitadel:owner': 'owners',
|
|
1303
1306
|
'zitadel:customers': 'hosting.entries.uses.auth',
|
|
1307
|
+
'zitadel:id-token-userinfo': 'hosting.entries.uses.auth',
|
|
1304
1308
|
'coolify:broker-entry': 'hosting.entries.uses.auth',
|
|
1305
1309
|
'coolify:mail': 'mail',
|
|
1306
1310
|
'coolify:chat': 'chat.usesDomain',
|
|
@@ -472,6 +472,22 @@ export declare function dbEnvAppliedScopeKey(entryKey: string): string;
|
|
|
472
472
|
* service's own marker, and vice versa.
|
|
473
473
|
*/
|
|
474
474
|
export declare function serviceDbEnvAppliedScopeKey(service: 'chat' | 'calls-service'): string;
|
|
475
|
+
/**
|
|
476
|
+
* The ZITADEL app id of company app `appKey` once "user info inside the ID
|
|
477
|
+
* token" was switched on for it (`updateOidcAppIdTokenUserinfoAssertion`).
|
|
478
|
+
* `diff.ts` plans one step for the org while any live sign-in app's recorded
|
|
479
|
+
* id differs from its marker, so an app made before the setting existed, or
|
|
480
|
+
* made again under a new id, is repaired once.
|
|
481
|
+
*/
|
|
482
|
+
export declare function zitadelAppIdTokenUserinfoAppliedScopeKey(appKey: string): string;
|
|
483
|
+
/** The ZITADEL app id of Archon's sign-in client in the platform's own ZITADEL. */
|
|
484
|
+
export declare const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = "zitadel_platform_archon_app";
|
|
485
|
+
/**
|
|
486
|
+
* The ZITADEL app id of Archon's sign-in client once "user info inside the
|
|
487
|
+
* ID token" was switched on for it. `planArchonIdTokenUserinfo` plans the
|
|
488
|
+
* repair while this differs from the recorded client app id.
|
|
489
|
+
*/
|
|
490
|
+
export declare const PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY = "archon_id_token_userinfo_applied";
|
|
475
491
|
/**
|
|
476
492
|
* The settings `coolifyApp` owns on an entry's app (`folder`, `port`,
|
|
477
493
|
* `autoDeploy`) as the JSON the marker below holds. `coolifyApp` records it
|
|
@@ -988,6 +1004,12 @@ export interface PlanPlatformOptions {
|
|
|
988
1004
|
};
|
|
989
1005
|
/** Why Archon's step must wait: the deployment it triggered is still running. Blocks even an applied step. */
|
|
990
1006
|
archonDeployRunning?: string;
|
|
1007
|
+
/**
|
|
1008
|
+
* The Archon sign-in client's ZITADEL client has
|
|
1009
|
+
* `updateOidcAppIdTokenUserinfoAssertion`. Not a block: without it the
|
|
1010
|
+
* repair step is not planned.
|
|
1011
|
+
*/
|
|
1012
|
+
archonIdTokenUserinfo?: boolean;
|
|
991
1013
|
};
|
|
992
1014
|
/**
|
|
993
1015
|
* Steps whose live state differs from the declared one in a way the recorded
|
|
@@ -521,6 +521,24 @@ export function dbEnvAppliedScopeKey(entryKey) {
|
|
|
521
521
|
export function serviceDbEnvAppliedScopeKey(service) {
|
|
522
522
|
return `service_db_env_applied.${service}`;
|
|
523
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* The ZITADEL app id of company app `appKey` once "user info inside the ID
|
|
526
|
+
* token" was switched on for it (`updateOidcAppIdTokenUserinfoAssertion`).
|
|
527
|
+
* `diff.ts` plans one step for the org while any live sign-in app's recorded
|
|
528
|
+
* id differs from its marker, so an app made before the setting existed, or
|
|
529
|
+
* made again under a new id, is repaired once.
|
|
530
|
+
*/
|
|
531
|
+
export function zitadelAppIdTokenUserinfoAppliedScopeKey(appKey) {
|
|
532
|
+
return `zitadel_app_id_token_userinfo_applied.${appKey}`;
|
|
533
|
+
}
|
|
534
|
+
/** The ZITADEL app id of Archon's sign-in client in the platform's own ZITADEL. */
|
|
535
|
+
export const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = 'zitadel_platform_archon_app';
|
|
536
|
+
/**
|
|
537
|
+
* The ZITADEL app id of Archon's sign-in client once "user info inside the
|
|
538
|
+
* ID token" was switched on for it. `planArchonIdTokenUserinfo` plans the
|
|
539
|
+
* repair while this differs from the recorded client app id.
|
|
540
|
+
*/
|
|
541
|
+
export const PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY = 'archon_id_token_userinfo_applied';
|
|
524
542
|
export function appSettingsOf(entry) {
|
|
525
543
|
return {
|
|
526
544
|
folder: entry.folder,
|
|
@@ -1137,6 +1155,10 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1137
1155
|
plannedSteps.push(uptime.step);
|
|
1138
1156
|
if (archon.step && blocked.archonRunsInsideApp)
|
|
1139
1157
|
plannedSteps.push(archon.step);
|
|
1158
|
+
// After Archon's own step: a repair that fails must not hold up the work before it.
|
|
1159
|
+
const idTokenUserinfo = planArchonIdTokenUserinfo(platform, recordedIds, blocked.archonIdTokenUserinfo ?? false);
|
|
1160
|
+
if (idTokenUserinfo)
|
|
1161
|
+
plannedSteps.push(idTokenUserinfo);
|
|
1140
1162
|
// #251: one line per service whose recorded applied ref no longer matches
|
|
1141
1163
|
// what the next apply would use (or, for zitadel, what is actually live).
|
|
1142
1164
|
// Never pushed into `plannedSteps` -- see `VersionChangeItem`'s own doc
|
|
@@ -2156,6 +2178,32 @@ function planEdge(platform, recordedIds, executorBlocked, drifted) {
|
|
|
2156
2178
|
blocked: null,
|
|
2157
2179
|
};
|
|
2158
2180
|
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Archon's sign-in client, once it exists, gets "user info inside the ID
|
|
2183
|
+
* token" switched on: a client made before the setting existed has none, so
|
|
2184
|
+
* its ID token carries no `email` and the console stores none for its
|
|
2185
|
+
* people. One `zitadel:id-token-userinfo` step keyed {@link PLATFORM_ARCHON_KEY},
|
|
2186
|
+
* planned while the recorded client app id differs from
|
|
2187
|
+
* {@link PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY} (a client made
|
|
2188
|
+
* again has a new id). Not planned when the host's ZITADEL client lacks the
|
|
2189
|
+
* method, and not blocked either: nothing is owed until the host wires it.
|
|
2190
|
+
*/
|
|
2191
|
+
function planArchonIdTokenUserinfo(platform, recordedIds, wired) {
|
|
2192
|
+
if (!wired || !platform.archon.enabled)
|
|
2193
|
+
return null;
|
|
2194
|
+
const appId = recordedIds[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
|
|
2195
|
+
if (!appId || recordedIds[PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY] === appId) {
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
return {
|
|
2199
|
+
action: { system: 'zitadel', kind: 'id-token-userinfo', key: PLATFORM_ARCHON_KEY, desired: {} },
|
|
2200
|
+
step: {
|
|
2201
|
+
description: "switch on user info in the ID token for Archon's sign-in client",
|
|
2202
|
+
destroys: false,
|
|
2203
|
+
migrates: false,
|
|
2204
|
+
},
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2159
2207
|
/**
|
|
2160
2208
|
* #604: the platform's ZITADEL as a sign-in provider in Cloudflare Access: a
|
|
2161
2209
|
* ZITADEL client for Cloudflare, then edge's `ensureProvider`. One
|
|
@@ -66,7 +66,23 @@ export interface PlatformZitadelSmtp {
|
|
|
66
66
|
changed: boolean;
|
|
67
67
|
}>;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Where ZITADEL's SMTP provider sends mail, instead of Lettermint's relay
|
|
71
|
+
* (`smtp.lettermint.co:587`, STARTTLS, user `lettermint`).
|
|
72
|
+
*/
|
|
73
|
+
export interface PlatformAuthSmtp {
|
|
74
|
+
host: string;
|
|
75
|
+
/** An integer from 1 to 65535. */
|
|
76
|
+
port: number;
|
|
77
|
+
/** ZITADEL's "TLS" flag for the connection. */
|
|
78
|
+
tls: boolean;
|
|
79
|
+
user: string;
|
|
80
|
+
/** Resolves the SMTP password. Absent, the broker's mail key (held, else the broker's env) is the password. */
|
|
81
|
+
password?: () => Promise<string>;
|
|
82
|
+
}
|
|
69
83
|
export interface PlatformAuthDeps {
|
|
84
|
+
/** Overrides the Lettermint SMTP relay ZITADEL is given. Absent, Lettermint exactly as before. */
|
|
85
|
+
smtp?: PlatformAuthSmtp;
|
|
70
86
|
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
|
|
71
87
|
/** F4.dns: every DNS record call, through edge at its recorded address (see {@link PlatformDnsFactory}). */
|
|
72
88
|
dns: PlatformDnsFactory;
|
|
@@ -319,6 +335,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
319
335
|
};
|
|
320
336
|
archonRunsInsideApp?: boolean;
|
|
321
337
|
archonDeployRunning?: string;
|
|
338
|
+
archonIdTokenUserinfo?: boolean;
|
|
322
339
|
}>;
|
|
323
340
|
/**
|
|
324
341
|
* foundry#542: why the uptime Worker cannot run yet, which optional secrets
|
|
@@ -516,7 +533,7 @@ export interface PlatformArchonDeps {
|
|
|
516
533
|
access: PlatformAccessFactory;
|
|
517
534
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
518
535
|
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
519
|
-
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp'>;
|
|
536
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'>;
|
|
520
537
|
/** {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client. */
|
|
521
538
|
zitadelForInstance?: (issuerUrl: string) => PlatformArchonDeps['zitadel'];
|
|
522
539
|
/**
|
|
@@ -9,7 +9,7 @@ import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrSta
|
|
|
9
9
|
import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
|
|
10
10
|
import { healthCheckSettings } from './health-check-settings.js';
|
|
11
11
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
12
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
12
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
13
13
|
import { applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
14
14
|
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
15
15
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
@@ -350,6 +350,21 @@ export class FoundryPlatformStepExecutor {
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
|
+
// Not a block: without the method the planner plans no repair step for Archon's sign-in client.
|
|
354
|
+
if (this.archon &&
|
|
355
|
+
platform.archon.enabled &&
|
|
356
|
+
recordedIds[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY]) {
|
|
357
|
+
try {
|
|
358
|
+
const zitadel = this.archon.zitadelForInstance?.(`https://${platform.auth.hostname}`) ??
|
|
359
|
+
this.archon.zitadel;
|
|
360
|
+
if (zitadel.updateOidcAppIdTokenUserinfoAssertion)
|
|
361
|
+
blocked.archonIdTokenUserinfo = true;
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
// No ZITADEL client for this instance: the flag stays unset and the repair is not planned.
|
|
365
|
+
console.error(`platform:archon: could not reach the platform's ZITADEL client to plan the ID token repair: ${e instanceof Error ? e.message : String(e)}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
353
368
|
// Not a block: tells the planner to order Archon's step last (`PlanPlatformOptions.blocked.archonRunsInsideApp`).
|
|
354
369
|
if (this.archon?.runsInsideApp) {
|
|
355
370
|
blocked.archonRunsInsideApp = true;
|
|
@@ -582,6 +597,14 @@ export class FoundryPlatformStepExecutor {
|
|
|
582
597
|
}
|
|
583
598
|
return provisionPlatformHc(context, this.hc);
|
|
584
599
|
}
|
|
600
|
+
if (action.system === 'zitadel' &&
|
|
601
|
+
action.kind === 'id-token-userinfo' &&
|
|
602
|
+
action.key === PLATFORM_ARCHON_KEY) {
|
|
603
|
+
if (!this.archon) {
|
|
604
|
+
throw new Error('platform:archon needs FoundryPlatformStepExecutor to be constructed with its archon dependencies (#175)');
|
|
605
|
+
}
|
|
606
|
+
return switchOnArchonIdTokenUserinfo(context, this.archon);
|
|
607
|
+
}
|
|
585
608
|
if (action.system === 'cloudflare' && action.kind === 'access-provider') {
|
|
586
609
|
if (!this.accessProvider) {
|
|
587
610
|
throw new Error('cloudflare:access-provider needs FoundryPlatformStepExecutor to be constructed with its Access provider dependencies (#604)');
|
|
@@ -1725,25 +1748,29 @@ async function provisionPlatformAuth(context, deps, owners = []) {
|
|
|
1725
1748
|
// The token is the held key, else the broker's live env. Absent, nothing
|
|
1726
1749
|
// here can run; `authBrokerBlockedReason` already reports it by name.
|
|
1727
1750
|
const mailToken = heldMailKey || (await currentEnvValue(deps.coolify, brokerAppId, 'LETTERMINT_API_TOKEN'));
|
|
1751
|
+
const smtpTarget = resolveAuthSmtp(deps.smtp);
|
|
1728
1752
|
if (mailToken) {
|
|
1729
1753
|
const mailZitadel = deps.zitadelForInstance?.(`https://${hostname}`) ?? deps.zitadel;
|
|
1754
|
+
let smtpPassword;
|
|
1730
1755
|
try {
|
|
1731
1756
|
// Runs every time the step does (idempotent), so a rotated token lands.
|
|
1757
|
+
smtpPassword = (await deps.smtp?.password?.()) ?? mailToken;
|
|
1732
1758
|
const smtp = await mailZitadel.ensureSmtpProvider({
|
|
1733
|
-
host:
|
|
1734
|
-
user:
|
|
1735
|
-
password:
|
|
1759
|
+
host: smtpTarget.host,
|
|
1760
|
+
user: smtpTarget.user,
|
|
1761
|
+
password: smtpPassword,
|
|
1736
1762
|
senderAddress: `noreply@${platform.apex}`,
|
|
1737
1763
|
senderName: PLATFORM_AUTH_MAIL_SENDER_NAME,
|
|
1738
|
-
tls:
|
|
1764
|
+
tls: smtpTarget.tls,
|
|
1739
1765
|
description: PLATFORM_AUTH_SMTP_DESCRIPTION,
|
|
1740
1766
|
});
|
|
1741
1767
|
await record(PLATFORM_AUTH_SMTP_SCOPE_KEY, smtp.id);
|
|
1742
1768
|
}
|
|
1743
1769
|
catch (error) {
|
|
1744
|
-
// The cause, with the Lettermint key redacted, so a failure says why.
|
|
1770
|
+
// The cause, with the Lettermint key (and an overriding SMTP password) redacted, so a failure says why.
|
|
1745
1771
|
const cause = redactSecrets(error instanceof Error ? error.message : String(error), [
|
|
1746
1772
|
mailToken,
|
|
1773
|
+
...(smtpPassword ? [smtpPassword] : []),
|
|
1747
1774
|
]);
|
|
1748
1775
|
throw new Error(`platform:auth: could not configure ZITADEL's SMTP provider for ${hostname}: ${cause}`);
|
|
1749
1776
|
}
|
|
@@ -1773,6 +1800,7 @@ async function provisionPlatformAuth(context, deps, owners = []) {
|
|
|
1773
1800
|
owner.email,
|
|
1774
1801
|
owner.email.toLowerCase(),
|
|
1775
1802
|
mailToken,
|
|
1803
|
+
...(smtpPassword ? [smtpPassword] : []),
|
|
1776
1804
|
]);
|
|
1777
1805
|
throw new Error(`platform:auth: could not invite the platform owner into ZITADEL org ${ownerOrgId}: ${cause}`);
|
|
1778
1806
|
}
|
|
@@ -1785,6 +1813,29 @@ async function provisionPlatformAuth(context, deps, owners = []) {
|
|
|
1785
1813
|
/** Lettermint's SMTP relay, as ZITADEL's SMTP provider wants it: `host:port`, STARTTLS. */
|
|
1786
1814
|
const PLATFORM_AUTH_SMTP_HOST = 'smtp.lettermint.co:587';
|
|
1787
1815
|
const PLATFORM_AUTH_SMTP_USER = 'lettermint';
|
|
1816
|
+
/**
|
|
1817
|
+
* The SMTP target for ZITADEL's provider: Lettermint's, or the host's override
|
|
1818
|
+
* once validated. Throws a message that never holds the password.
|
|
1819
|
+
*/
|
|
1820
|
+
function resolveAuthSmtp(override) {
|
|
1821
|
+
if (!override) {
|
|
1822
|
+
return { host: PLATFORM_AUTH_SMTP_HOST, user: PLATFORM_AUTH_SMTP_USER, tls: true };
|
|
1823
|
+
}
|
|
1824
|
+
const { host, port, tls, user } = override;
|
|
1825
|
+
if (typeof host !== 'string' || !host.trim() || /[\s/:]/.test(host)) {
|
|
1826
|
+
throw new Error('platform:auth: smtp.host must be a non-empty host name with no port or path');
|
|
1827
|
+
}
|
|
1828
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1829
|
+
throw new Error('platform:auth: smtp.port must be an integer from 1 to 65535');
|
|
1830
|
+
}
|
|
1831
|
+
if (typeof tls !== 'boolean') {
|
|
1832
|
+
throw new Error('platform:auth: smtp.tls must be true or false');
|
|
1833
|
+
}
|
|
1834
|
+
if (typeof user !== 'string' || !user.trim()) {
|
|
1835
|
+
throw new Error('platform:auth: smtp.user must be non-empty');
|
|
1836
|
+
}
|
|
1837
|
+
return { host: `${host}:${port}`, user, tls };
|
|
1838
|
+
}
|
|
1788
1839
|
/** Marks the provider as foundry's, so `ensureSmtpProvider` finds it again. */
|
|
1789
1840
|
const PLATFORM_AUTH_SMTP_DESCRIPTION = 'foundry:platform:lettermint';
|
|
1790
1841
|
/** The same name the broker's `from` uses (`Company Included <noreply@apex>`). */
|
|
@@ -2636,11 +2687,44 @@ const PLATFORM_ARCHON_ACCESS_SCOPE_KEY = 'cloudflare_platform_archon_access_app'
|
|
|
2636
2687
|
/** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
|
|
2637
2688
|
export const PLATFORM_ZITADEL_ORG_SCOPE_KEY = 'zitadel_platform_org';
|
|
2638
2689
|
const PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY = 'zitadel_platform_archon_project';
|
|
2639
|
-
const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = 'zitadel_platform_archon_app';
|
|
2640
2690
|
/** The hostname the sign-in client's redirect URIs were made for; a change makes a new client. */
|
|
2641
2691
|
const PLATFORM_ARCHON_CLIENT_HOSTNAME_SCOPE_KEY = 'zitadel_platform_archon_client_hostname';
|
|
2642
2692
|
/** Archon's OIDC client id: public, not a secret, so a plain scope id. */
|
|
2643
2693
|
export const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = 'zitadel_platform_archon_client_id';
|
|
2694
|
+
/**
|
|
2695
|
+
* Switches on "user info inside the ID token" for Archon's sign-in client
|
|
2696
|
+
* (`zitadel:id-token-userinfo` keyed {@link PLATFORM_ARCHON_KEY}). The marker
|
|
2697
|
+
* is the client's app id, recorded only after the call returned, so a call
|
|
2698
|
+
* that throws leaves none and the next plan names the step again. The error
|
|
2699
|
+
* names the ids only, never the cause.
|
|
2700
|
+
*/
|
|
2701
|
+
async function switchOnArchonIdTokenUserinfo(context, deps) {
|
|
2702
|
+
const { platform } = context;
|
|
2703
|
+
const recorded = context.recordedIds();
|
|
2704
|
+
const zitadel = deps.zitadelForInstance?.(`https://${platform.auth.hostname}`) ?? deps.zitadel;
|
|
2705
|
+
// The planner plans nothing without the method; a stale plan does nothing.
|
|
2706
|
+
if (!zitadel.updateOidcAppIdTokenUserinfoAssertion)
|
|
2707
|
+
return {};
|
|
2708
|
+
const orgId = recorded[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
|
|
2709
|
+
const projectId = recorded[PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY];
|
|
2710
|
+
const appId = recorded[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
|
|
2711
|
+
if (!orgId || !projectId || !appId) {
|
|
2712
|
+
throw new Error('platform:archon: the sign-in client is not recorded yet -- cannot switch on user info in its ID token');
|
|
2713
|
+
}
|
|
2714
|
+
let done;
|
|
2715
|
+
try {
|
|
2716
|
+
done = await zitadel.updateOidcAppIdTokenUserinfoAssertion(orgId, projectId, appId);
|
|
2717
|
+
}
|
|
2718
|
+
catch {
|
|
2719
|
+
throw new Error(`platform:archon: switching on user info in the ID token failed for the sign-in client ${appId}`);
|
|
2720
|
+
}
|
|
2721
|
+
// A client that answers every property (`packages/local`) resolves to something else: no marker.
|
|
2722
|
+
if (typeof done !== 'boolean')
|
|
2723
|
+
return {};
|
|
2724
|
+
context.recordScopeId(PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, appId);
|
|
2725
|
+
await context.persist?.();
|
|
2726
|
+
return {};
|
|
2727
|
+
}
|
|
2644
2728
|
/**
|
|
2645
2729
|
* #175: makes `coolify:app` keyed {@link PLATFORM_ARCHON_KEY} true: Archon
|
|
2646
2730
|
* from https://github.com/wtfalch/archon on `platform.archon.gitRef`, its own
|
|
@@ -2916,6 +3000,8 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
2916
3000
|
postLogoutRedirectUris: [`https://${hostname}/`],
|
|
2917
3001
|
});
|
|
2918
3002
|
context.recordScopeId(PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, created.appId);
|
|
3003
|
+
// Made with the setting on (providers 0.9.0): nothing to repair on this id.
|
|
3004
|
+
context.recordScopeId(PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, created.appId);
|
|
2919
3005
|
context.recordScopeId(PLATFORM_ARCHON_CLIENT_HOSTNAME_SCOPE_KEY, hostname);
|
|
2920
3006
|
await record(PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY, created.clientId);
|
|
2921
3007
|
clientId = created.clientId;
|
|
@@ -4,6 +4,8 @@ import { type ZitadelClient } from '@wtfalch/providers/zitadel';
|
|
|
4
4
|
import type { OrgId } from '../org.js';
|
|
5
5
|
import type { DeclaredOrgStore } from '../store.js';
|
|
6
6
|
import type { AiReader, CloudflareReader, CmsReader, CoolifyReader, LiveResource, ZitadelReader } from './live.js';
|
|
7
|
+
/** The `CoolifyClient` methods the readers call, so a host's own implementation needs no cast. */
|
|
8
|
+
type CoolifyReaderClient = Pick<CoolifyClient, 'listResources' | 'getApplication' | 'getDatabase' | 'listEnvs' | 'listStorages'>;
|
|
7
9
|
/**
|
|
8
10
|
* Real `CoolifyReader`, `CloudflareReader` and `ZitadelReader` implementations
|
|
9
11
|
* (`live.ts:31-45`), backed by `@wtfalch/providers`' HTTP clients instead of
|
|
@@ -94,7 +96,7 @@ export declare class ProvidersCoolifyReader implements CoolifyReader {
|
|
|
94
96
|
private readonly client;
|
|
95
97
|
private readonly resolveScope;
|
|
96
98
|
private readonly resolveRecordedIds;
|
|
97
|
-
constructor(client:
|
|
99
|
+
constructor(client: CoolifyReaderClient, resolveScope: ScopeResolver, resolveRecordedIds: RecordedIds);
|
|
98
100
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
99
101
|
/** The project's resources as Coolify reports them, unmapped: what adopt checks against a declaration. */
|
|
100
102
|
listProject(projectUuid: string): Promise<LiveResource[]>;
|
|
@@ -229,6 +231,7 @@ export declare class ProvidersCmsReader implements CmsReader {
|
|
|
229
231
|
private readonly resolveProjectScope;
|
|
230
232
|
private readonly resolveCmsAppId;
|
|
231
233
|
private readonly resolveSiteAppId;
|
|
232
|
-
constructor(client:
|
|
234
|
+
constructor(client: CoolifyReaderClient, resolveProjectScope: ScopeResolver, resolveCmsAppId: ScopeResolver, resolveSiteAppId: ScopeResolver);
|
|
233
235
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
234
236
|
}
|
|
237
|
+
export {};
|