@wtfalch/foundry 0.26.4 → 0.28.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 +19 -0
- package/dist/calls-sfu.js +31 -8
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/migrations/0050_platform_sfu_connection_revision.sql +7 -0
- package/dist/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +44 -1
- package/dist/plan/executor.d.ts +35 -7
- package/dist/plan/executor.js +52 -8
- package/dist/plan/index.d.ts +3 -3
- package/dist/plan/index.js +2 -2
- package/dist/plan/live.d.ts +3 -3
- package/dist/plan/plan-apply.d.ts +8 -0
- package/dist/plan/plan-apply.js +8 -0
- package/dist/plan/platform-diff.d.ts +32 -0
- package/dist/plan/platform-diff.js +77 -14
- package/dist/plan/platform-org.d.ts +3 -4
- package/dist/plan/platform-providers.d.ts +19 -2
- package/dist/plan/platform-providers.js +108 -15
- package/dist/plan/platform-sfu.d.ts +10 -0
- package/dist/plan/platform-sfu.js +39 -0
- package/dist/plan/providers-readers.d.ts +5 -2
- package/dist/platform.d.ts +11 -1
- package/dist/platform.js +6 -0
- package/dist/store/postgres.js +7 -1
- package/dist/store/schema.d.ts +17 -0
- package/dist/store/schema.js +2 -0
- package/package.json +1 -1
|
@@ -99,6 +99,11 @@ export declare function versionNotAppliedLine(service: string, appLive: boolean,
|
|
|
99
99
|
* the same `platform:<service>` shape as {@link PLATFORM_AUTH_KEY}.
|
|
100
100
|
*/
|
|
101
101
|
export declare const PLATFORM_ARCHON_KEY = "platform:archon";
|
|
102
|
+
/**
|
|
103
|
+
* The fingerprint (`callsSfuAppliedFingerprint`) of the last successful `sfu`
|
|
104
|
+
* apply. `planSfu` plans the step again when it differs from the desired one.
|
|
105
|
+
*/
|
|
106
|
+
export declare const PLATFORM_SFU_APPLIED_SCOPE_KEY = "sfu_applied_fingerprint";
|
|
102
107
|
/**
|
|
103
108
|
* #175: the hostname and `gitRef` the last successful Archon run applied.
|
|
104
109
|
* `planArchon` plans the step again when either differs from the declared
|
|
@@ -472,6 +477,22 @@ export declare function dbEnvAppliedScopeKey(entryKey: string): string;
|
|
|
472
477
|
* service's own marker, and vice versa.
|
|
473
478
|
*/
|
|
474
479
|
export declare function serviceDbEnvAppliedScopeKey(service: 'chat' | 'calls-service'): string;
|
|
480
|
+
/**
|
|
481
|
+
* The ZITADEL app id of company app `appKey` once "user info inside the ID
|
|
482
|
+
* token" was switched on for it (`updateOidcAppIdTokenUserinfoAssertion`).
|
|
483
|
+
* `diff.ts` plans one step for the org while any live sign-in app's recorded
|
|
484
|
+
* id differs from its marker, so an app made before the setting existed, or
|
|
485
|
+
* made again under a new id, is repaired once.
|
|
486
|
+
*/
|
|
487
|
+
export declare function zitadelAppIdTokenUserinfoAppliedScopeKey(appKey: string): string;
|
|
488
|
+
/** The ZITADEL app id of Archon's sign-in client in the platform's own ZITADEL. */
|
|
489
|
+
export declare const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = "zitadel_platform_archon_app";
|
|
490
|
+
/**
|
|
491
|
+
* The ZITADEL app id of Archon's sign-in client once "user info inside the
|
|
492
|
+
* ID token" was switched on for it. `planArchonIdTokenUserinfo` plans the
|
|
493
|
+
* repair while this differs from the recorded client app id.
|
|
494
|
+
*/
|
|
495
|
+
export declare const PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY = "archon_id_token_userinfo_applied";
|
|
475
496
|
/**
|
|
476
497
|
* The settings `coolifyApp` owns on an entry's app (`folder`, `port`,
|
|
477
498
|
* `autoDeploy`) as the JSON the marker below holds. `coolifyApp` records it
|
|
@@ -906,6 +927,11 @@ export interface PlanPlatformOptions {
|
|
|
906
927
|
jobsObservable?: boolean;
|
|
907
928
|
/** Why the shared SFU is blocked, or `null` when it is not (default `null`). */
|
|
908
929
|
sfuBlocked?: string | null;
|
|
930
|
+
/**
|
|
931
|
+
* The slugs of orgs with `calls.enabled`. With it, `planSfu` plans the step
|
|
932
|
+
* only when the recorded fingerprint differs; absent, it always plans it.
|
|
933
|
+
*/
|
|
934
|
+
callsOrgSlugs?: readonly string[];
|
|
909
935
|
/** The platform's recorded resource/scope ids (default `{}`). */
|
|
910
936
|
recordedIds?: Readonly<Record<string, string>>;
|
|
911
937
|
/** Per-step block reasons the executor reports (default `{}`). */
|
|
@@ -988,6 +1014,12 @@ export interface PlanPlatformOptions {
|
|
|
988
1014
|
};
|
|
989
1015
|
/** Why Archon's step must wait: the deployment it triggered is still running. Blocks even an applied step. */
|
|
990
1016
|
archonDeployRunning?: string;
|
|
1017
|
+
/**
|
|
1018
|
+
* The Archon sign-in client's ZITADEL client has
|
|
1019
|
+
* `updateOidcAppIdTokenUserinfoAssertion`. Not a block: without it the
|
|
1020
|
+
* repair step is not planned.
|
|
1021
|
+
*/
|
|
1022
|
+
archonIdTokenUserinfo?: boolean;
|
|
991
1023
|
};
|
|
992
1024
|
/**
|
|
993
1025
|
* Steps whose live state differs from the declared one in a way the recorded
|
|
@@ -4,6 +4,7 @@ import { APP_ENTRY_KEY } from '../org.js';
|
|
|
4
4
|
import { VERSION_CHANGE_KIND, } from '../plan.js';
|
|
5
5
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
6
6
|
import { legacyAppHostname } from './hostnames.js';
|
|
7
|
+
import { callsSfuAppliedFingerprint } from './platform-sfu.js';
|
|
7
8
|
import { withStepSystem } from './step-system.js';
|
|
8
9
|
/**
|
|
9
10
|
* The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
|
|
@@ -122,6 +123,11 @@ export function versionNotAppliedLine(service, appLive, applied, pinned) {
|
|
|
122
123
|
* the same `platform:<service>` shape as {@link PLATFORM_AUTH_KEY}.
|
|
123
124
|
*/
|
|
124
125
|
export const PLATFORM_ARCHON_KEY = 'platform:archon';
|
|
126
|
+
/**
|
|
127
|
+
* The fingerprint (`callsSfuAppliedFingerprint`) of the last successful `sfu`
|
|
128
|
+
* apply. `planSfu` plans the step again when it differs from the desired one.
|
|
129
|
+
*/
|
|
130
|
+
export const PLATFORM_SFU_APPLIED_SCOPE_KEY = 'sfu_applied_fingerprint';
|
|
125
131
|
/**
|
|
126
132
|
* #175: the hostname and `gitRef` the last successful Archon run applied.
|
|
127
133
|
* `planArchon` plans the step again when either differs from the declared
|
|
@@ -521,6 +527,24 @@ export function dbEnvAppliedScopeKey(entryKey) {
|
|
|
521
527
|
export function serviceDbEnvAppliedScopeKey(service) {
|
|
522
528
|
return `service_db_env_applied.${service}`;
|
|
523
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* The ZITADEL app id of company app `appKey` once "user info inside the ID
|
|
532
|
+
* token" was switched on for it (`updateOidcAppIdTokenUserinfoAssertion`).
|
|
533
|
+
* `diff.ts` plans one step for the org while any live sign-in app's recorded
|
|
534
|
+
* id differs from its marker, so an app made before the setting existed, or
|
|
535
|
+
* made again under a new id, is repaired once.
|
|
536
|
+
*/
|
|
537
|
+
export function zitadelAppIdTokenUserinfoAppliedScopeKey(appKey) {
|
|
538
|
+
return `zitadel_app_id_token_userinfo_applied.${appKey}`;
|
|
539
|
+
}
|
|
540
|
+
/** The ZITADEL app id of Archon's sign-in client in the platform's own ZITADEL. */
|
|
541
|
+
export const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = 'zitadel_platform_archon_app';
|
|
542
|
+
/**
|
|
543
|
+
* The ZITADEL app id of Archon's sign-in client once "user info inside the
|
|
544
|
+
* ID token" was switched on for it. `planArchonIdTokenUserinfo` plans the
|
|
545
|
+
* repair while this differs from the recorded client app id.
|
|
546
|
+
*/
|
|
547
|
+
export const PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY = 'archon_id_token_userinfo_applied';
|
|
524
548
|
export function appSettingsOf(entry) {
|
|
525
549
|
return {
|
|
526
550
|
folder: entry.folder,
|
|
@@ -1038,7 +1062,7 @@ export function uptimeRecorded(recordedIds) {
|
|
|
1038
1062
|
* unchanged).
|
|
1039
1063
|
*/
|
|
1040
1064
|
export function planPlatform(platform, live, options = {}) {
|
|
1041
|
-
const { jobsObservable = true, sfuBlocked = null, recordedIds = {}, blocked = {}, drifted = {}, } = options;
|
|
1065
|
+
const { jobsObservable = true, sfuBlocked = null, callsOrgSlugs, recordedIds = {}, blocked = {}, drifted = {}, } = options;
|
|
1042
1066
|
const byKey = new Map(live.map((r) => [resourceKey(r.system, r.kind, r.key), r]));
|
|
1043
1067
|
const claimed = new Set();
|
|
1044
1068
|
const plannedSteps = [];
|
|
@@ -1053,7 +1077,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1053
1077
|
// A blocked SFU is reported in `unmanaged`, not planned: a step the
|
|
1054
1078
|
// executor must refuse would halt every step after it (`diff.ts` does the
|
|
1055
1079
|
// same for a blocked Agora).
|
|
1056
|
-
const sfu = platform.sfu.enabled && sfuBlocked ? null : planSfu(platform);
|
|
1080
|
+
const sfu = platform.sfu.enabled && sfuBlocked ? null : planSfu(platform, callsOrgSlugs, recordedIds);
|
|
1057
1081
|
if (sfu)
|
|
1058
1082
|
plannedSteps.push(sfu);
|
|
1059
1083
|
// #478, F4.dns: first of the services, ahead of auth. Edge is the only
|
|
@@ -1137,6 +1161,10 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1137
1161
|
plannedSteps.push(uptime.step);
|
|
1138
1162
|
if (archon.step && blocked.archonRunsInsideApp)
|
|
1139
1163
|
plannedSteps.push(archon.step);
|
|
1164
|
+
// After Archon's own step: a repair that fails must not hold up the work before it.
|
|
1165
|
+
const idTokenUserinfo = planArchonIdTokenUserinfo(platform, recordedIds, blocked.archonIdTokenUserinfo ?? false);
|
|
1166
|
+
if (idTokenUserinfo)
|
|
1167
|
+
plannedSteps.push(idTokenUserinfo);
|
|
1140
1168
|
// #251: one line per service whose recorded applied ref no longer matches
|
|
1141
1169
|
// what the next apply would use (or, for zitadel, what is actually live).
|
|
1142
1170
|
// Never pushed into `plannedSteps` -- see `VersionChangeItem`'s own doc
|
|
@@ -1483,20 +1511,29 @@ function planBackups(byKey, claimed, recordedIds, executorMissing, storageDrifte
|
|
|
1483
1511
|
/**
|
|
1484
1512
|
* The platform's shared calls SFU (#170), keyed `sfu:livekit:platform` --
|
|
1485
1513
|
* there is exactly one, so `key` names the platform itself rather than a
|
|
1486
|
-
* domain or an org id
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
function planSfu(platform) {
|
|
1514
|
+
* domain or an org id. It is never matched against a live resource: there is
|
|
1515
|
+
* no cheap way to read the SFU VM's own state over SSH during planning
|
|
1516
|
+
* (`LiveResource`'s doc comment on the `sfu` system). Instead the executor
|
|
1517
|
+
* records {@link callsSfuAppliedFingerprint} after a successful apply, and
|
|
1518
|
+
* with `callsOrgSlugs` the step is planned only when that record differs
|
|
1519
|
+
* from the desired one (new org set, changed calls files, new
|
|
1520
|
+
* `sfu.connectionRevision`). Without `callsOrgSlugs` it is planned whenever
|
|
1521
|
+
* `platform.sfu.enabled` is true. Planning it needlessly is safe:
|
|
1522
|
+
* `applyCallsSfu` is built to be re-run.
|
|
1523
|
+
*/
|
|
1524
|
+
function planSfu(platform, callsOrgSlugs, recordedIds) {
|
|
1498
1525
|
if (!platform.sfu.enabled)
|
|
1499
1526
|
return null;
|
|
1527
|
+
if (callsOrgSlugs &&
|
|
1528
|
+
recordedIds[PLATFORM_SFU_APPLIED_SCOPE_KEY] ===
|
|
1529
|
+
callsSfuAppliedFingerprint({
|
|
1530
|
+
orgSlugs: callsOrgSlugs,
|
|
1531
|
+
...(platform.sfu.connectionRevision
|
|
1532
|
+
? { connectionRevision: platform.sfu.connectionRevision }
|
|
1533
|
+
: {}),
|
|
1534
|
+
})) {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1500
1537
|
const action = {
|
|
1501
1538
|
system: 'sfu',
|
|
1502
1539
|
kind: 'livekit',
|
|
@@ -2156,6 +2193,32 @@ function planEdge(platform, recordedIds, executorBlocked, drifted) {
|
|
|
2156
2193
|
blocked: null,
|
|
2157
2194
|
};
|
|
2158
2195
|
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Archon's sign-in client, once it exists, gets "user info inside the ID
|
|
2198
|
+
* token" switched on: a client made before the setting existed has none, so
|
|
2199
|
+
* its ID token carries no `email` and the console stores none for its
|
|
2200
|
+
* people. One `zitadel:id-token-userinfo` step keyed {@link PLATFORM_ARCHON_KEY},
|
|
2201
|
+
* planned while the recorded client app id differs from
|
|
2202
|
+
* {@link PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY} (a client made
|
|
2203
|
+
* again has a new id). Not planned when the host's ZITADEL client lacks the
|
|
2204
|
+
* method, and not blocked either: nothing is owed until the host wires it.
|
|
2205
|
+
*/
|
|
2206
|
+
function planArchonIdTokenUserinfo(platform, recordedIds, wired) {
|
|
2207
|
+
if (!wired || !platform.archon.enabled)
|
|
2208
|
+
return null;
|
|
2209
|
+
const appId = recordedIds[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
|
|
2210
|
+
if (!appId || recordedIds[PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY] === appId) {
|
|
2211
|
+
return null;
|
|
2212
|
+
}
|
|
2213
|
+
return {
|
|
2214
|
+
action: { system: 'zitadel', kind: 'id-token-userinfo', key: PLATFORM_ARCHON_KEY, desired: {} },
|
|
2215
|
+
step: {
|
|
2216
|
+
description: "switch on user info in the ID token for Archon's sign-in client",
|
|
2217
|
+
destroys: false,
|
|
2218
|
+
migrates: false,
|
|
2219
|
+
},
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2159
2222
|
/**
|
|
2160
2223
|
* #604: the platform's ZITADEL as a sign-in provider in Cloudflare Access: a
|
|
2161
2224
|
* ZITADEL client for Cloudflare, then edge's `ensureProvider`. One
|
|
@@ -79,10 +79,9 @@ export interface OrgPlatformReadiness {
|
|
|
79
79
|
aiUrl?: string | null;
|
|
80
80
|
/**
|
|
81
81
|
* The shared calls SFU (#170) an org's own calls Matrix stack depends on.
|
|
82
|
-
* Declared is enough here:
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* SSH during planning"), so "applied" has no separate signal to check.
|
|
82
|
+
* Declared is enough here: `platform-diff.ts`'s `planSfu` plans the step
|
|
83
|
+
* from a recorded fingerprint, which is no signal an org's plan could
|
|
84
|
+
* read, so "applied" has no separate check.
|
|
86
85
|
*/
|
|
87
86
|
callsReady: boolean;
|
|
88
87
|
/** The platform's own sign-in (#168), which every org's own ZITADEL org lives inside. */
|
|
@@ -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
|
/**
|
|
@@ -788,7 +805,7 @@ export declare function createAiManagementKeyMinter(options: {
|
|
|
788
805
|
* `_FILE` (#209 mints and pushes this once it lands) and `LETTERMINT_API_TOKEN`
|
|
789
806
|
* (#210: `wtfalch/auth#138` moved the broker's account mail onto
|
|
790
807
|
* `@wtfalch/mailer`'s Lettermint transport, so this is the env name the
|
|
791
|
-
* broker actually reads now, not
|
|
808
|
+
* broker actually reads now, not the earlier provider's token). Read from the broker's
|
|
792
809
|
* own live env, not a recorded scope id: whichever of #209/#210 lands first
|
|
793
810
|
* (or a human, setting either directly on the app) is enough to clear that
|
|
794
811
|
* one input, in either order, with no coupling to this step's own execution.
|
|
@@ -9,8 +9,8 @@ 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';
|
|
13
|
-
import { applyCallsSfuPlatformStep } from './platform-sfu.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_SFU_APPLIED_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
|
+
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } 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';
|
|
16
16
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
@@ -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)');
|
|
@@ -621,7 +644,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
621
644
|
return provisionPlatformBackupStorage(context, this.backup);
|
|
622
645
|
}
|
|
623
646
|
if (action.system === 'sfu' && action.kind === 'livekit') {
|
|
624
|
-
return this.executeSfu();
|
|
647
|
+
return this.executeSfu(context);
|
|
625
648
|
}
|
|
626
649
|
throw new Error(`platform step ${action.system}:${action.kind} is not built yet`);
|
|
627
650
|
}
|
|
@@ -647,7 +670,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
647
670
|
* Sorted by slug so a re-run with the same set of enabled orgs is a no-op
|
|
648
671
|
* even before `applyCallsSfuPlatformStep`'s own sort (its doc comment).
|
|
649
672
|
*/
|
|
650
|
-
async executeSfu() {
|
|
673
|
+
async executeSfu(context) {
|
|
651
674
|
if (!this.sfu) {
|
|
652
675
|
throw new Error('platform step sfu:livekit is not built yet (#170: pass `sfu` to FoundryPlatformStepExecutor)');
|
|
653
676
|
}
|
|
@@ -657,6 +680,13 @@ export class FoundryPlatformStepExecutor {
|
|
|
657
680
|
.map((org) => org.slug)
|
|
658
681
|
.sort();
|
|
659
682
|
await applyCallsSfuPlatformStep(this.sfu, orgSlugs);
|
|
683
|
+
// Only after the apply succeeded: a failed apply records nothing, so the
|
|
684
|
+
// next plan shows the step again.
|
|
685
|
+
const { connectionRevision } = context.platform.sfu;
|
|
686
|
+
context.recordScopeId(PLATFORM_SFU_APPLIED_SCOPE_KEY, callsSfuAppliedFingerprint({
|
|
687
|
+
orgSlugs,
|
|
688
|
+
...(connectionRevision ? { connectionRevision } : {}),
|
|
689
|
+
}));
|
|
660
690
|
return undefined;
|
|
661
691
|
}
|
|
662
692
|
async coolifyJobs(action, context) {
|
|
@@ -1127,7 +1157,7 @@ async function liveLoginClientTokenHash(coolify, appId) {
|
|
|
1127
1157
|
* `IAM_LOGIN_CLIENT` machine user (never the admin one, which is
|
|
1128
1158
|
* `IAM_OWNER`) and delivers its token to both apps, once the broker's
|
|
1129
1159
|
* other required inputs are already met.
|
|
1130
|
-
* - ~~The broker never receives
|
|
1160
|
+
* - ~~The broker never receives the earlier mail provider's token either, so it will not
|
|
1131
1161
|
* start on `AUTH_APPS` alone.~~ Closed by #210, once `wtfalch/auth#138`
|
|
1132
1162
|
* moved the broker's account mail onto `@wtfalch/mailer`'s Lettermint
|
|
1133
1163
|
* transport: this function now reads the key back from
|
|
@@ -1725,25 +1755,29 @@ async function provisionPlatformAuth(context, deps, owners = []) {
|
|
|
1725
1755
|
// The token is the held key, else the broker's live env. Absent, nothing
|
|
1726
1756
|
// here can run; `authBrokerBlockedReason` already reports it by name.
|
|
1727
1757
|
const mailToken = heldMailKey || (await currentEnvValue(deps.coolify, brokerAppId, 'LETTERMINT_API_TOKEN'));
|
|
1758
|
+
const smtpTarget = resolveAuthSmtp(deps.smtp);
|
|
1728
1759
|
if (mailToken) {
|
|
1729
1760
|
const mailZitadel = deps.zitadelForInstance?.(`https://${hostname}`) ?? deps.zitadel;
|
|
1761
|
+
let smtpPassword;
|
|
1730
1762
|
try {
|
|
1731
1763
|
// Runs every time the step does (idempotent), so a rotated token lands.
|
|
1764
|
+
smtpPassword = (await deps.smtp?.password?.()) ?? mailToken;
|
|
1732
1765
|
const smtp = await mailZitadel.ensureSmtpProvider({
|
|
1733
|
-
host:
|
|
1734
|
-
user:
|
|
1735
|
-
password:
|
|
1766
|
+
host: smtpTarget.host,
|
|
1767
|
+
user: smtpTarget.user,
|
|
1768
|
+
password: smtpPassword,
|
|
1736
1769
|
senderAddress: `noreply@${platform.apex}`,
|
|
1737
1770
|
senderName: PLATFORM_AUTH_MAIL_SENDER_NAME,
|
|
1738
|
-
tls:
|
|
1771
|
+
tls: smtpTarget.tls,
|
|
1739
1772
|
description: PLATFORM_AUTH_SMTP_DESCRIPTION,
|
|
1740
1773
|
});
|
|
1741
1774
|
await record(PLATFORM_AUTH_SMTP_SCOPE_KEY, smtp.id);
|
|
1742
1775
|
}
|
|
1743
1776
|
catch (error) {
|
|
1744
|
-
// The cause, with the Lettermint key redacted, so a failure says why.
|
|
1777
|
+
// The cause, with the Lettermint key (and an overriding SMTP password) redacted, so a failure says why.
|
|
1745
1778
|
const cause = redactSecrets(error instanceof Error ? error.message : String(error), [
|
|
1746
1779
|
mailToken,
|
|
1780
|
+
...(smtpPassword ? [smtpPassword] : []),
|
|
1747
1781
|
]);
|
|
1748
1782
|
throw new Error(`platform:auth: could not configure ZITADEL's SMTP provider for ${hostname}: ${cause}`);
|
|
1749
1783
|
}
|
|
@@ -1773,6 +1807,7 @@ async function provisionPlatformAuth(context, deps, owners = []) {
|
|
|
1773
1807
|
owner.email,
|
|
1774
1808
|
owner.email.toLowerCase(),
|
|
1775
1809
|
mailToken,
|
|
1810
|
+
...(smtpPassword ? [smtpPassword] : []),
|
|
1776
1811
|
]);
|
|
1777
1812
|
throw new Error(`platform:auth: could not invite the platform owner into ZITADEL org ${ownerOrgId}: ${cause}`);
|
|
1778
1813
|
}
|
|
@@ -1785,6 +1820,29 @@ async function provisionPlatformAuth(context, deps, owners = []) {
|
|
|
1785
1820
|
/** Lettermint's SMTP relay, as ZITADEL's SMTP provider wants it: `host:port`, STARTTLS. */
|
|
1786
1821
|
const PLATFORM_AUTH_SMTP_HOST = 'smtp.lettermint.co:587';
|
|
1787
1822
|
const PLATFORM_AUTH_SMTP_USER = 'lettermint';
|
|
1823
|
+
/**
|
|
1824
|
+
* The SMTP target for ZITADEL's provider: Lettermint's, or the host's override
|
|
1825
|
+
* once validated. Throws a message that never holds the password.
|
|
1826
|
+
*/
|
|
1827
|
+
function resolveAuthSmtp(override) {
|
|
1828
|
+
if (!override) {
|
|
1829
|
+
return { host: PLATFORM_AUTH_SMTP_HOST, user: PLATFORM_AUTH_SMTP_USER, tls: true };
|
|
1830
|
+
}
|
|
1831
|
+
const { host, port, tls, user } = override;
|
|
1832
|
+
if (typeof host !== 'string' || !host.trim() || /[\s/:]/.test(host)) {
|
|
1833
|
+
throw new Error('platform:auth: smtp.host must be a non-empty host name with no port or path');
|
|
1834
|
+
}
|
|
1835
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1836
|
+
throw new Error('platform:auth: smtp.port must be an integer from 1 to 65535');
|
|
1837
|
+
}
|
|
1838
|
+
if (typeof tls !== 'boolean') {
|
|
1839
|
+
throw new Error('platform:auth: smtp.tls must be true or false');
|
|
1840
|
+
}
|
|
1841
|
+
if (typeof user !== 'string' || !user.trim()) {
|
|
1842
|
+
throw new Error('platform:auth: smtp.user must be non-empty');
|
|
1843
|
+
}
|
|
1844
|
+
return { host: `${host}:${port}`, user, tls };
|
|
1845
|
+
}
|
|
1788
1846
|
/** Marks the provider as foundry's, so `ensureSmtpProvider` finds it again. */
|
|
1789
1847
|
const PLATFORM_AUTH_SMTP_DESCRIPTION = 'foundry:platform:lettermint';
|
|
1790
1848
|
/** The same name the broker's `from` uses (`Company Included <noreply@apex>`). */
|
|
@@ -1978,8 +2036,8 @@ const PLATFORM_MAIL_ZITADEL_PROJECT_NAME = 'mail';
|
|
|
1978
2036
|
* instance-bootstrap helper `wtfalch-manage` already uses for the hand-built
|
|
1979
2037
|
* `mail.wtfalch.dev`: that plan also wires an `AcmeProvider` and `DnsServer`
|
|
1980
2038
|
* for Stalwart's own DNS-01 ACME challenge, and an `MtaRoute` that relays
|
|
1981
|
-
* every outbound message through
|
|
1982
|
-
* mail off
|
|
2039
|
+
* every outbound message through the earlier mail provider -- the estate moved transactional
|
|
2040
|
+
* mail off it onto Lettermint before this issue (`@wtfalch/mailer` ADR
|
|
1983
2041
|
* 0006), and this step provisions no Cloudflare DNS token for Stalwart's own
|
|
1984
2042
|
* ACME either. Calling it as-is would mint an outbound relay this platform
|
|
1985
2043
|
* does not want and a credential requirement this issue does not cover, so
|
|
@@ -1991,7 +2049,7 @@ const PLATFORM_MAIL_ZITADEL_PROJECT_NAME = 'mail';
|
|
|
1991
2049
|
* William's review"):
|
|
1992
2050
|
* - **No ACME/TLS for the SMTP listener.** `mx.<apex>` answers with
|
|
1993
2051
|
* Stalwart's own self-signed certificate until a follow-up wires a real
|
|
1994
|
-
* one (`mailObjects`'s `AcmeProvider`/`DnsServer` shape, minus the
|
|
2052
|
+
* one (`mailObjects`'s `AcmeProvider`/`DnsServer` shape, minus the earlier relay
|
|
1995
2053
|
* relay it also carries).
|
|
1996
2054
|
* - **The Lettermint team token the org app step reads still comes from
|
|
1997
2055
|
* wherever the host already reads it, not held keys.** Needs
|
|
@@ -2636,11 +2694,44 @@ const PLATFORM_ARCHON_ACCESS_SCOPE_KEY = 'cloudflare_platform_archon_access_app'
|
|
|
2636
2694
|
/** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
|
|
2637
2695
|
export const PLATFORM_ZITADEL_ORG_SCOPE_KEY = 'zitadel_platform_org';
|
|
2638
2696
|
const PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY = 'zitadel_platform_archon_project';
|
|
2639
|
-
const PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY = 'zitadel_platform_archon_app';
|
|
2640
2697
|
/** The hostname the sign-in client's redirect URIs were made for; a change makes a new client. */
|
|
2641
2698
|
const PLATFORM_ARCHON_CLIENT_HOSTNAME_SCOPE_KEY = 'zitadel_platform_archon_client_hostname';
|
|
2642
2699
|
/** Archon's OIDC client id: public, not a secret, so a plain scope id. */
|
|
2643
2700
|
export const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = 'zitadel_platform_archon_client_id';
|
|
2701
|
+
/**
|
|
2702
|
+
* Switches on "user info inside the ID token" for Archon's sign-in client
|
|
2703
|
+
* (`zitadel:id-token-userinfo` keyed {@link PLATFORM_ARCHON_KEY}). The marker
|
|
2704
|
+
* is the client's app id, recorded only after the call returned, so a call
|
|
2705
|
+
* that throws leaves none and the next plan names the step again. The error
|
|
2706
|
+
* names the ids only, never the cause.
|
|
2707
|
+
*/
|
|
2708
|
+
async function switchOnArchonIdTokenUserinfo(context, deps) {
|
|
2709
|
+
const { platform } = context;
|
|
2710
|
+
const recorded = context.recordedIds();
|
|
2711
|
+
const zitadel = deps.zitadelForInstance?.(`https://${platform.auth.hostname}`) ?? deps.zitadel;
|
|
2712
|
+
// The planner plans nothing without the method; a stale plan does nothing.
|
|
2713
|
+
if (!zitadel.updateOidcAppIdTokenUserinfoAssertion)
|
|
2714
|
+
return {};
|
|
2715
|
+
const orgId = recorded[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
|
|
2716
|
+
const projectId = recorded[PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY];
|
|
2717
|
+
const appId = recorded[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
|
|
2718
|
+
if (!orgId || !projectId || !appId) {
|
|
2719
|
+
throw new Error('platform:archon: the sign-in client is not recorded yet -- cannot switch on user info in its ID token');
|
|
2720
|
+
}
|
|
2721
|
+
let done;
|
|
2722
|
+
try {
|
|
2723
|
+
done = await zitadel.updateOidcAppIdTokenUserinfoAssertion(orgId, projectId, appId);
|
|
2724
|
+
}
|
|
2725
|
+
catch {
|
|
2726
|
+
throw new Error(`platform:archon: switching on user info in the ID token failed for the sign-in client ${appId}`);
|
|
2727
|
+
}
|
|
2728
|
+
// A client that answers every property (`packages/local`) resolves to something else: no marker.
|
|
2729
|
+
if (typeof done !== 'boolean')
|
|
2730
|
+
return {};
|
|
2731
|
+
context.recordScopeId(PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, appId);
|
|
2732
|
+
await context.persist?.();
|
|
2733
|
+
return {};
|
|
2734
|
+
}
|
|
2644
2735
|
/**
|
|
2645
2736
|
* #175: makes `coolify:app` keyed {@link PLATFORM_ARCHON_KEY} true: Archon
|
|
2646
2737
|
* from https://github.com/wtfalch/archon on `platform.archon.gitRef`, its own
|
|
@@ -2916,6 +3007,8 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
2916
3007
|
postLogoutRedirectUris: [`https://${hostname}/`],
|
|
2917
3008
|
});
|
|
2918
3009
|
context.recordScopeId(PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, created.appId);
|
|
3010
|
+
// Made with the setting on (providers 0.9.0): nothing to repair on this id.
|
|
3011
|
+
context.recordScopeId(PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, created.appId);
|
|
2919
3012
|
context.recordScopeId(PLATFORM_ARCHON_CLIENT_HOSTNAME_SCOPE_KEY, hostname);
|
|
2920
3013
|
await record(PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY, created.clientId);
|
|
2921
3014
|
clientId = created.clientId;
|
|
@@ -4958,7 +5051,7 @@ async function findOrCreatePlatformApp(coolify, name, environment, create) {
|
|
|
4958
5051
|
* `_FILE` (#209 mints and pushes this once it lands) and `LETTERMINT_API_TOKEN`
|
|
4959
5052
|
* (#210: `wtfalch/auth#138` moved the broker's account mail onto
|
|
4960
5053
|
* `@wtfalch/mailer`'s Lettermint transport, so this is the env name the
|
|
4961
|
-
* broker actually reads now, not
|
|
5054
|
+
* broker actually reads now, not the earlier provider's token). Read from the broker's
|
|
4962
5055
|
* own live env, not a recorded scope id: whichever of #209/#210 lands first
|
|
4963
5056
|
* (or a human, setting either directly on the app) is enough to clear that
|
|
4964
5057
|
* one input, in either order, with no coupling to this step's own execution.
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
*/
|
|
38
38
|
import { type LivekitCredentials } from '../calls-sfu-keys.js';
|
|
39
39
|
import { type CallsSfuSshSession } from '../calls-sfu-ssh-adapter.js';
|
|
40
|
+
/**
|
|
41
|
+
* What the last successful `sfu` apply is recorded as
|
|
42
|
+
* (`PLATFORM_SFU_APPLIED_SCOPE_KEY`): a SHA-256 hex digest of foundry's calls
|
|
43
|
+
* files, the sorted distinct org slugs and the connection revision. It holds
|
|
44
|
+
* no secret and does not depend on slug order or duplicates.
|
|
45
|
+
*/
|
|
46
|
+
export declare function callsSfuAppliedFingerprint(input: {
|
|
47
|
+
orgSlugs: readonly string[];
|
|
48
|
+
connectionRevision?: string;
|
|
49
|
+
}): string;
|
|
40
50
|
/** The SFU VM's own address, SSH credentials and host key fingerprint -- held keys a human stored ahead of time. */
|
|
41
51
|
export interface CallsSfuConnection {
|
|
42
52
|
address: string;
|
|
@@ -35,9 +35,48 @@
|
|
|
35
35
|
* live outside this package. `calls-sfu-ssh-adapter.ts`'s {@link CallsSfuSshSession} is the narrow shape
|
|
36
36
|
* a test fakes instead.
|
|
37
37
|
*/
|
|
38
|
+
import { createHash } from 'node:crypto';
|
|
38
39
|
import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
39
40
|
import { mintLivekitCredentials } from '../calls-sfu-keys.js';
|
|
40
41
|
import { applyCallsSfuOverSession } from '../calls-sfu-ssh-adapter.js';
|
|
42
|
+
import { CALLS_SFU_FIREWALL_COMMANDS, CALLS_SFU_SSH_RULE_COMMAND, CALLS_SFU_UP_COMMAND, cloudflaredEnvFile, dockerComposeYaml, livekitConfigYaml, } from '../calls-sfu.js';
|
|
43
|
+
/**
|
|
44
|
+
* Hash of what `applyCallsSfu` writes and runs, rendered from placeholder
|
|
45
|
+
* values so no secret is involved: a change to the compose file, the
|
|
46
|
+
* `livekit.yaml` template, the env file template or a command changes it.
|
|
47
|
+
*/
|
|
48
|
+
function callsSfuFilesVersion() {
|
|
49
|
+
const placeholder = {
|
|
50
|
+
address: 'address',
|
|
51
|
+
livekitKeys: { key: 'secret' },
|
|
52
|
+
cloudflaredTunnelToken: 'token',
|
|
53
|
+
};
|
|
54
|
+
return createHash('sha256')
|
|
55
|
+
.update(JSON.stringify([
|
|
56
|
+
dockerComposeYaml(),
|
|
57
|
+
livekitConfigYaml(placeholder),
|
|
58
|
+
cloudflaredEnvFile(placeholder.cloudflaredTunnelToken),
|
|
59
|
+
CALLS_SFU_UP_COMMAND,
|
|
60
|
+
CALLS_SFU_SSH_RULE_COMMAND,
|
|
61
|
+
CALLS_SFU_FIREWALL_COMMANDS,
|
|
62
|
+
]))
|
|
63
|
+
.digest('hex');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* What the last successful `sfu` apply is recorded as
|
|
67
|
+
* (`PLATFORM_SFU_APPLIED_SCOPE_KEY`): a SHA-256 hex digest of foundry's calls
|
|
68
|
+
* files, the sorted distinct org slugs and the connection revision. It holds
|
|
69
|
+
* no secret and does not depend on slug order or duplicates.
|
|
70
|
+
*/
|
|
71
|
+
export function callsSfuAppliedFingerprint(input) {
|
|
72
|
+
return createHash('sha256')
|
|
73
|
+
.update(JSON.stringify([
|
|
74
|
+
callsSfuFilesVersion(),
|
|
75
|
+
[...new Set(input.orgSlugs)].sort(),
|
|
76
|
+
input.connectionRevision ?? '',
|
|
77
|
+
]))
|
|
78
|
+
.digest('hex');
|
|
79
|
+
}
|
|
41
80
|
/**
|
|
42
81
|
* The key id the platform's own LiveKit pair is held under, alongside every
|
|
43
82
|
* org's own pair, in the same {@link CallsSfuKeysPort} (#152 round 2 review:
|
|
@@ -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 {};
|