@wtfalch/foundry 0.5.0 → 0.6.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/bootstrap/bootstrap.d.ts +16 -1
- package/dist/bootstrap/bootstrap.js +40 -2
- package/dist/bootstrap/declare.js +6 -1
- package/dist/calls-sfu-deploy-support.d.ts +8 -0
- package/dist/calls-sfu-deploy-support.js +10 -1
- package/dist/calls-sfu.d.ts +31 -8
- package/dist/calls-sfu.js +36 -5
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -2
- package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
- package/dist/migrations/0020_mail_apps.sql +7 -0
- package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
- package/dist/org.d.ts +81 -0
- package/dist/org.js +88 -0
- package/dist/plan/diff.js +93 -10
- package/dist/plan/executor.d.ts +236 -35
- package/dist/plan/executor.js +274 -76
- package/dist/plan/index.d.ts +7 -1
- package/dist/plan/index.js +4 -1
- package/dist/plan/mail-copy.d.ts +125 -0
- package/dist/plan/mail-copy.js +166 -0
- package/dist/plan/mail-identity-allowlist.d.ts +50 -0
- package/dist/plan/mail-identity-allowlist.js +11 -0
- package/dist/plan/mail-identity.d.ts +68 -0
- package/dist/plan/mail-identity.js +17 -0
- package/dist/plan/plan-apply.d.ts +5 -0
- package/dist/plan/platform-diff.d.ts +184 -9
- package/dist/plan/platform-diff.js +368 -15
- package/dist/plan/platform-org.d.ts +3 -2
- package/dist/plan/platform-providers.d.ts +138 -24
- package/dist/plan/platform-providers.js +1460 -79
- package/dist/plan/platform-sfu.d.ts +54 -19
- package/dist/plan/platform-sfu.js +64 -22
- package/dist/plan/zitadel-machine-user.d.ts +60 -11
- package/dist/plan/zitadel-machine-user.js +182 -39
- package/dist/requested-changes.d.ts +17 -7
- package/dist/requested-changes.js +37 -7
- package/dist/store/postgres.js +31 -2
- package/dist/store/schema.d.ts +72 -0
- package/dist/store/schema.js +13 -0
- package/dist/store.d.ts +7 -0
- package/package.json +1 -1
|
@@ -47,6 +47,17 @@ export const PLATFORM_ARCHON_KEY = 'platform:archon';
|
|
|
47
47
|
*/
|
|
48
48
|
export const PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY = 'archon_applied_hostname';
|
|
49
49
|
export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref';
|
|
50
|
+
/**
|
|
51
|
+
* #211 round 4: the broker app id Archon last registered its entry with.
|
|
52
|
+
* `provisionPlatformArchon` reads the broker to register with from
|
|
53
|
+
* {@link PLATFORM_AUTH_BROKER_SCOPE_KEY} alone, so a broker recreated under a
|
|
54
|
+
* new app id (the old one deleted by hand, or `platform:auth` minting a fresh
|
|
55
|
+
* one) leaves Archon "applied" against a broker that no longer holds its
|
|
56
|
+
* entry. `planArchon` plans the step again whenever this differs from the
|
|
57
|
+
* currently recorded {@link PLATFORM_AUTH_BROKER_SCOPE_KEY}, the same way a
|
|
58
|
+
* changed hostname or `gitRef` does.
|
|
59
|
+
*/
|
|
60
|
+
export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_app';
|
|
50
61
|
/**
|
|
51
62
|
* #175: set to '1' before the step points the tunnel at `http_status:404`
|
|
52
63
|
* (Access app rebuild), cleared to '' once the real ingress is written back.
|
|
@@ -56,6 +67,132 @@ export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref
|
|
|
56
67
|
export const PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY = 'archon_gate_closed';
|
|
57
68
|
export const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = 'zitadel_platform_admin_user_id';
|
|
58
69
|
export const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_admin_user_undelivered';
|
|
70
|
+
/**
|
|
71
|
+
* #216 (building on #209/#215): the `IAM_LOGIN_CLIENT` machine user minted
|
|
72
|
+
* for the login app and the broker, mirroring
|
|
73
|
+
* {@link PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY}/
|
|
74
|
+
* {@link PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY} in shape -- a recorded
|
|
75
|
+
* id once delivered, an undelivered id recorded first, the moment the user
|
|
76
|
+
* is minted.
|
|
77
|
+
*
|
|
78
|
+
* Unlike the admin pair, an undelivered login-client key never blocks
|
|
79
|
+
* {@link planAuth} from planning the step again -- #216 round 2: `planAuth`
|
|
80
|
+
* below does read it, but only to surface a non-blocking note (alongside a
|
|
81
|
+
* planned step, the way #211 round 3's broker-deploy block already is)
|
|
82
|
+
* rather than to hold the step back. Revoking a stale login-client PAT
|
|
83
|
+
* destroys nothing -- unlike the admin user's, whose loss the admin pair's
|
|
84
|
+
* own "delete that user by hand" message exists for because nothing else
|
|
85
|
+
* here re-derives the ZITADEL masterkey a lost admin PAT might have been the
|
|
86
|
+
* only record of -- so `mintLoginClientMachineUser` (`zitadel-machine-user.ts`)
|
|
87
|
+
* always lists and deletes every PAT the user already holds before minting a
|
|
88
|
+
* fresh one. That is what lets `provisionPlatformAuth` simply re-mint on the
|
|
89
|
+
* next run, with no manual step, which is #216's whole fix for the bug PR
|
|
90
|
+
* #215 shipped with: PR #215 mirrored the admin pair's forever-blocking
|
|
91
|
+
* behaviour for this key too, which meant one failed Coolify call lost the
|
|
92
|
+
* login-client token for good.
|
|
93
|
+
*
|
|
94
|
+
* #216 round 4: the value recorded here is JSON ({@link
|
|
95
|
+
* PlatformAuthLoginClientRecord}, parsed back by
|
|
96
|
+
* {@link parseLoginClientRecord}), not the bare user id round 2-3 wrote --
|
|
97
|
+
* the user id alone can never tell a healthy delivery from one a concurrent
|
|
98
|
+
* apply has since clobbered, because `mintLoginClientMachineUser` reuses the
|
|
99
|
+
* same user on every mint. The PAT id travels alongside it so {@link
|
|
100
|
+
* planAuth} can ask ZITADEL, on every plan, whether that exact PAT is still
|
|
101
|
+
* the sole live one.
|
|
102
|
+
*
|
|
103
|
+
* #216 round 5: that alone is still not enough -- a recorded PAT can be the
|
|
104
|
+
* sole live one in ZITADEL while Coolify holds a *different*, already-dead
|
|
105
|
+
* token, when two concurrent applies interleave their own writes and
|
|
106
|
+
* records in the right (wrong) order (see `authLoginClientDrifted`'s own
|
|
107
|
+
* doc comment in `platform-providers.ts` for the exact ordering). So the
|
|
108
|
+
* record also carries `tokenHash`, a sha256 of the exact token that was
|
|
109
|
+
* delivered -- never the token itself -- which `authLoginClientDrifted`
|
|
110
|
+
* compares against what is actually live in both apps' Coolify envs.
|
|
111
|
+
*/
|
|
112
|
+
export const PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY = 'zitadel_platform_login_client_user_id';
|
|
113
|
+
export const PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_login_client_user_undelivered';
|
|
114
|
+
/**
|
|
115
|
+
* Parses a {@link PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY} scope id back
|
|
116
|
+
* into a {@link PlatformAuthLoginClientRecord}. `null` for anything that is
|
|
117
|
+
* not one -- absent, the pre-round-4 plain user-id string, the round 2-4
|
|
118
|
+
* `{userId, patId}` shape with no `tokenHash`, or corrupt JSON -- the same
|
|
119
|
+
* "malformed reads as unapplied, never throws" rule
|
|
120
|
+
* {@link parseBrokerDeployRecord} follows and for the same reason: this is a
|
|
121
|
+
* planning read, not a validation boundary. A pre-round-5 record reading as
|
|
122
|
+
* unapplied is the intended migration: the next apply's fresh mint recomputes
|
|
123
|
+
* it in the new shape, the same way a pre-round-4 plain-string record already
|
|
124
|
+
* did.
|
|
125
|
+
*/
|
|
126
|
+
export function parseLoginClientRecord(raw) {
|
|
127
|
+
if (!raw)
|
|
128
|
+
return null;
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(raw);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
137
|
+
return null;
|
|
138
|
+
const candidate = parsed;
|
|
139
|
+
if (typeof candidate.userId !== 'string' || !candidate.userId)
|
|
140
|
+
return null;
|
|
141
|
+
if (typeof candidate.patId !== 'string' || !candidate.patId)
|
|
142
|
+
return null;
|
|
143
|
+
if (typeof candidate.tokenHash !== 'string' || !candidate.tokenHash)
|
|
144
|
+
return null;
|
|
145
|
+
return { userId: candidate.userId, patId: candidate.patId, tokenHash: candidate.tokenHash };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* #204/#211: the sign-in broker app `provisionPlatformAuth` creates alongside
|
|
149
|
+
* ZITADEL and the login app. Defined here, not in `platform-providers.ts`
|
|
150
|
+
* (which imports it), because `planAuth` below needs it too -- a recorded
|
|
151
|
+
* `coolify:app:platform:auth` key with no recorded broker id is a platform
|
|
152
|
+
* provisioned before #204 shipped, and must still plan the step again so the
|
|
153
|
+
* broker gets created rather than being read as fully applied.
|
|
154
|
+
*/
|
|
155
|
+
export const PLATFORM_AUTH_BROKER_SCOPE_KEY = 'coolify_platform_auth_broker_app';
|
|
156
|
+
/**
|
|
157
|
+
* #211: recorded once the broker has actually been deployed -- distinct from
|
|
158
|
+
* {@link PLATFORM_AUTH_BROKER_SCOPE_KEY} (the app exists) precisely because
|
|
159
|
+
* the app can exist for a while before it is ever deployed (see
|
|
160
|
+
* `missingBrokerInputs` in `platform-providers.ts`). Defined here, not there,
|
|
161
|
+
* for the same reason as {@link PLATFORM_AUTH_BROKER_SCOPE_KEY}: `planAuth`
|
|
162
|
+
* below needs it too, so a broker that is created but not yet deployed still
|
|
163
|
+
* plans the step again instead of reading as fully applied for good.
|
|
164
|
+
*/
|
|
165
|
+
export const PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY = 'coolify_platform_auth_broker_deployed';
|
|
166
|
+
/**
|
|
167
|
+
* Parses a {@link PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY} scope id back into
|
|
168
|
+
* a {@link BrokerDeployRecord}. `null` for anything that is not one --
|
|
169
|
+
* absent, the pre-round-5 plain broker-id string, or corrupt JSON -- because
|
|
170
|
+
* this is a planning read, not a validation boundary: a malformed record
|
|
171
|
+
* must read as "not deployed" so the step plans (and overwrites it) again,
|
|
172
|
+
* never throw and take the rest of the plan down with it. Exported here
|
|
173
|
+
* rather than defined in `platform-providers.ts` for the same reason
|
|
174
|
+
* {@link PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY} itself was moved here in
|
|
175
|
+
* round 2 -- {@link planAuth} below needs it too, without a circular import.
|
|
176
|
+
*/
|
|
177
|
+
export function parseBrokerDeployRecord(raw) {
|
|
178
|
+
if (!raw)
|
|
179
|
+
return null;
|
|
180
|
+
let parsed;
|
|
181
|
+
try {
|
|
182
|
+
parsed = JSON.parse(raw);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
188
|
+
return null;
|
|
189
|
+
const candidate = parsed;
|
|
190
|
+
if (typeof candidate.brokerAppId !== 'string' || !candidate.brokerAppId)
|
|
191
|
+
return null;
|
|
192
|
+
if (typeof candidate.authAppsHash !== 'string' || !candidate.authAppsHash)
|
|
193
|
+
return null;
|
|
194
|
+
return { brokerAppId: candidate.brokerAppId, authAppsHash: candidate.authAppsHash };
|
|
195
|
+
}
|
|
59
196
|
/**
|
|
60
197
|
* #197: the `cloudflared` connector app `provisionPlatformAuth` runs for its
|
|
61
198
|
* own tunnel. Consulted by `planAuth` below so that a tunnel recorded with no
|
|
@@ -68,6 +205,21 @@ export const PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY = 'coolify_platform_auth_conn
|
|
|
68
205
|
export const PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY = 'coolify_platform_mail_connector_app';
|
|
69
206
|
/** #197: the same connector scope id for the Archon step's own tunnel, read by `planArchon`. */
|
|
70
207
|
export const PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY = 'coolify_platform_archon_connector_app';
|
|
208
|
+
/**
|
|
209
|
+
* #213: the three held-key refs, under tenant `platform`, Archon's own
|
|
210
|
+
* onboarding reads before it can start (archon#101 round 3, archon#104).
|
|
211
|
+
* `planArchon`'s step puts each one, once, before Archon's app deploys; an
|
|
212
|
+
* entry already held is left alone, never overwritten
|
|
213
|
+
* (`putArchonMasterKeys` in `platform-providers.ts`). The Coolify URL and
|
|
214
|
+
* token are values this package already deploys with; the Cloudflare
|
|
215
|
+
* parent token ("API Tokens Write", so Archon can mint its own scoped
|
|
216
|
+
* child tokens from it) is a human-created credential the host supplies --
|
|
217
|
+
* neither Coolify nor Cloudflare lets either be minted over an API (#213
|
|
218
|
+
* "Human steps").
|
|
219
|
+
*/
|
|
220
|
+
export const PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF = 'platform:master:coolify-url';
|
|
221
|
+
export const PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF = 'platform:master:coolify-token';
|
|
222
|
+
export const PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF = 'platform:master:cloudflare-token';
|
|
71
223
|
/**
|
|
72
224
|
* #169: `platform:<service>` for the platform's own mail server (Stalwart),
|
|
73
225
|
* mirroring {@link PLATFORM_AUTH_KEY} exactly.
|
|
@@ -144,6 +296,27 @@ export const PLATFORM_AI_INTERNAL_URL_SCOPE_KEY = 'ai_platform_internal_url';
|
|
|
144
296
|
*/
|
|
145
297
|
export const PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY = 'ai_platform_management_key_id';
|
|
146
298
|
export const PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY = 'ai_platform_management_key_undelivered';
|
|
299
|
+
/**
|
|
300
|
+
* #201: set when ai's `POST /v1/platform/keys` reports its `archon` key
|
|
301
|
+
* already exists but returns no secret -- a mint this record never learned
|
|
302
|
+
* about (most likely the crash window {@link PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY}'s
|
|
303
|
+
* own comment describes: the secret returned once and was never recorded).
|
|
304
|
+
* ai shows a credential's secret only at mint time, so it cannot be
|
|
305
|
+
* recovered; the planner reports this instead of minting under another
|
|
306
|
+
* name. A person revokes the live `archon` key in ai and removes this
|
|
307
|
+
* recorded key to let the step mint a fresh one.
|
|
308
|
+
*/
|
|
309
|
+
export const PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY = 'ai_platform_management_key_conflict';
|
|
310
|
+
/**
|
|
311
|
+
* #201: the same two-key discipline as {@link PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY}
|
|
312
|
+
* and {@link PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY}, for the Ed25519 pair
|
|
313
|
+
* foundry mints to sign its calls to ai's `POST /v1/platform/keys`. The
|
|
314
|
+
* undelivered key is recorded the moment the pair is minted; the set key,
|
|
315
|
+
* only once the private half was delivered and the public half pinned on
|
|
316
|
+
* the runner as `MANAGEMENT_PUBLIC_KEYS`.
|
|
317
|
+
*/
|
|
318
|
+
export const PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY = 'ai_platform_signing_key_undelivered';
|
|
319
|
+
export const PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY = 'ai_platform_signing_key_set';
|
|
147
320
|
/**
|
|
148
321
|
* #172: ai's platform (operator) tenant id, which the management key belongs
|
|
149
322
|
* to -- what ai's own README calls `AI_ORGANISATION_ID`. Not a secret.
|
|
@@ -169,14 +342,6 @@ export const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = 'ai_platform_migrate_blocke
|
|
|
169
342
|
* that throws) is repaired by the next one.
|
|
170
343
|
*/
|
|
171
344
|
export const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = 'ai_platform_deploy_pending';
|
|
172
|
-
/**
|
|
173
|
-
* #172: what the planner says while no AI management key minter is supplied.
|
|
174
|
-
* A fixed text: it is part of the stale-plan comparison. ai issues its first
|
|
175
|
-
* platform credential only through its `keys:issue-archon` script, run
|
|
176
|
-
* against its database with its keys worker credential; it has no HTTP route
|
|
177
|
-
* for that, so foundry cannot build a minter over ai's admin API yet.
|
|
178
|
-
*/
|
|
179
|
-
export const PLATFORM_AI_MINTER_BLOCKED = "the platform's AI host is blocked, not planned: no AI management key minter was supplied. ai mints its first platform key only through its keys:issue-archon script against its database, so the host must supply PlatformAiDeps.managementKeyMinter";
|
|
180
345
|
/**
|
|
181
346
|
* #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
|
|
182
347
|
* dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
|
|
@@ -261,7 +426,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
261
426
|
const sfu = platform.sfu.enabled && sfuBlocked ? null : planSfu(platform);
|
|
262
427
|
if (sfu)
|
|
263
428
|
plannedSteps.push(sfu);
|
|
264
|
-
const auth = planAuth(platform, recordedIds, blocked.auth);
|
|
429
|
+
const auth = planAuth(platform, recordedIds, blocked.auth, blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? []);
|
|
265
430
|
if (auth.step)
|
|
266
431
|
plannedSteps.push(auth.step);
|
|
267
432
|
// #174: the integrations host. Planned after auth; neither step reads the
|
|
@@ -476,14 +641,123 @@ function planSfu(platform) {
|
|
|
476
641
|
* -- `provisionPlatformAuth` re-runs idempotently against everything it
|
|
477
642
|
* already built and only adds the connector, so this never mints or
|
|
478
643
|
* redeploys what a prior run already finished.
|
|
644
|
+
*
|
|
645
|
+
* #211: the same holds for the broker ({@link PLATFORM_AUTH_BROKER_SCOPE_KEY}).
|
|
646
|
+
* A platform applied before #204 shipped the broker recorded the app and the
|
|
647
|
+
* connector and never the broker, so without this check it would read as
|
|
648
|
+
* fully applied and Archon would never find a broker to register with.
|
|
649
|
+
*
|
|
650
|
+
* #211 round 2: a recorded broker id is not the same as a *deployed* broker
|
|
651
|
+
* ({@link PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY}) -- `provisionPlatformAuth`
|
|
652
|
+
* creates and records the app before it has `ZITADEL_SERVICE_USER_TOKEN`/
|
|
653
|
+
* `RESEND_API_TOKEN` to deploy it with (#209, #210). So this still plans the
|
|
654
|
+
* step again while the broker sits undeployed: `executorBlocked` (from
|
|
655
|
+
* `authBrokerBlockedReason`) reports it blocked, by name, for as long as the
|
|
656
|
+
* inputs stay missing, and the moment they arrive the same replanned step
|
|
657
|
+
* deploys it -- no apply is left blocked for good once its inputs exist.
|
|
658
|
+
*
|
|
659
|
+
* #211 round 5: "deployed" ({@link PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY})
|
|
660
|
+
* is read back through {@link parseBrokerDeployRecord}, not `Boolean(...)`,
|
|
661
|
+
* and only counts while the record's `brokerAppId` matches the currently
|
|
662
|
+
* recorded {@link PLATFORM_AUTH_BROKER_SCOPE_KEY} -- a broker recreated under
|
|
663
|
+
* a new app id (the old one deleted by hand, or a fresh mint) leaves the old
|
|
664
|
+
* record pointing at an app that no longer exists, and a plain `Boolean`
|
|
665
|
+
* check (round 2-4) could never see that. This function has no live Coolify
|
|
666
|
+
* access, so it cannot also compare the record's `authAppsHash` against a
|
|
667
|
+
* live `AUTH_APPS` -- that half of the same rule (`brokerDeployNeeded` in
|
|
668
|
+
* `platform-providers.ts`) only runs inside the two `provision*` functions,
|
|
669
|
+
* which do have one. A malformed record parses to `null` and is treated the
|
|
670
|
+
* same as none recorded -- see {@link parseBrokerDeployRecord}'s own doc
|
|
671
|
+
* comment.
|
|
672
|
+
*
|
|
673
|
+
* #211 round 3: `executorBlocked` must not hold back the step itself once
|
|
674
|
+
* the broker exists (`brokerApplied`). `authBrokerBlockedReason` only ever
|
|
675
|
+
* returns non-null once a broker app id is recorded, so `executorBlocked &&
|
|
676
|
+
* brokerApplied` can only mean "broker created, not yet deployed" -- never
|
|
677
|
+
* "no executor". Blocking the whole step on that would deadlock: #209 mints
|
|
678
|
+
* the broker's own token as part of running this step, and a step that is
|
|
679
|
+
* never planned never runs. So this still plans the step -- surfacing
|
|
680
|
+
* `executorBlocked` in `blocked` for visibility, not to stop planning.
|
|
681
|
+
*
|
|
682
|
+
* #211 round 6: round 3's `!brokerApplied` guard assumed "no executor" and
|
|
683
|
+
* "broker not yet created" always coincide. They don't: a broker recorded on
|
|
684
|
+
* an earlier apply, then run again against a host built without its auth
|
|
685
|
+
* dependencies (`this.auth` undefined in `PlatformProvidersExecutor`), has
|
|
686
|
+
* `brokerApplied === true` and `executorBlocked` set to the no-deps message,
|
|
687
|
+
* so round 3's check let the step through and apply threw on the missing
|
|
688
|
+
* executor. `executorMissing` is that case's own signal
|
|
689
|
+
* ({@link PlanPlatformOptions.blocked}'s `authExecutorMissing`) and holds the
|
|
690
|
+
* step back unconditionally -- there is no executor to run it with, whatever
|
|
691
|
+
* `recordedIds` says.
|
|
692
|
+
*
|
|
693
|
+
* #216: a platform with every one of the above applied but no recorded
|
|
694
|
+
* {@link PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY} is not fully applied
|
|
695
|
+
* either -- the same "provisioned before this shipped" case
|
|
696
|
+
* {@link PLATFORM_AUTH_BROKER_SCOPE_KEY} already covers, here for a platform
|
|
697
|
+
* applied before #216's login-client mint existed at all.
|
|
698
|
+
* `provisionPlatformAuth` re-runs idempotently against everything it already
|
|
699
|
+
* built and only mints and delivers the login-client token, so this never
|
|
700
|
+
* redoes what a prior run already finished. Unlike the admin key, an
|
|
701
|
+
* undelivered login-client key never blocks this check -- see
|
|
702
|
+
* {@link PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY}'s own doc comment
|
|
703
|
+
* for why -- though it is still read, below, for a non-blocking note.
|
|
704
|
+
*
|
|
705
|
+
* #216 round 4: a *recorded* login-client key is also not enough on its own
|
|
706
|
+
* -- `authDrifted` (the host's own `driftedSteps`, reading ZITADEL's live
|
|
707
|
+
* PATs for the recorded user) means the PAT this platform's two apps were
|
|
708
|
+
* handed is no longer the sole live one: a concurrent apply's own mint
|
|
709
|
+
* revoked or replaced it, or minted an extra one alongside it. Round 2 and
|
|
710
|
+
* round 3 both tried to catch that from inside the same run that delivered
|
|
711
|
+
* the token (checking once after every write, then checking again before
|
|
712
|
+
* every write) and both left a window a slow concurrent write could still
|
|
713
|
+
* land in. This closes it from the other end instead: every plan re-checks
|
|
714
|
+
* the live state no matter which run last touched it, so whichever run's
|
|
715
|
+
* write actually won is caught on the very next plan, not raced against
|
|
716
|
+
* from inside the run that might lose.
|
|
717
|
+
*
|
|
718
|
+
* #216 round 5: `otherInputsMissing` keeps the note below honest when a
|
|
719
|
+
* re-mint is needed (undelivered or drifted) but cannot actually happen yet
|
|
720
|
+
* -- `provisionPlatformAuth` only mints and delivers once the broker's
|
|
721
|
+
* inputs *other than* `ZITADEL_SERVICE_USER_TOKEN` are all present
|
|
722
|
+
* (deferred, not blocked -- see that function's own doc comment), so a note
|
|
723
|
+
* promising "this apply re-mints" while one of those is still missing would
|
|
724
|
+
* be wrong the moment it is read.
|
|
479
725
|
*/
|
|
480
|
-
function planAuth(platform, recordedIds, executorBlocked) {
|
|
726
|
+
function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = []) {
|
|
481
727
|
if (!platform.auth.enabled)
|
|
482
728
|
return { step: null, blocked: null };
|
|
483
729
|
const key = resourceKey('coolify', 'app', PLATFORM_AUTH_KEY);
|
|
484
730
|
const connectorApplied = Boolean(recordedIds[PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY]);
|
|
485
|
-
|
|
731
|
+
// #211: a recorded app and connector with no recorded broker id is a
|
|
732
|
+
// platform provisioned before #204 shipped the broker, or a run that threw
|
|
733
|
+
// before the broker's own id was recorded -- still not fully applied, the
|
|
734
|
+
// same rule `connectorApplied` already follows for #197's connector.
|
|
735
|
+
const brokerApplied = Boolean(recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY]);
|
|
736
|
+
// #211 round 2: also require the broker to be deployed, not merely created.
|
|
737
|
+
// #211 round 5: and only while the deploy record still names the broker
|
|
738
|
+
// that is currently recorded -- see this function's own doc comment.
|
|
739
|
+
const brokerDeployRecord = parseBrokerDeployRecord(recordedIds[PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY]);
|
|
740
|
+
const brokerDeployed = brokerDeployRecord !== null &&
|
|
741
|
+
brokerDeployRecord.brokerAppId === recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY];
|
|
742
|
+
// #216 round 4: parsed back, not just `Boolean(...)` -- see this
|
|
743
|
+
// function's own doc comment and {@link parseLoginClientRecord}.
|
|
744
|
+
const loginClientRecord = parseLoginClientRecord(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]);
|
|
745
|
+
// #216 round 4: a recorded record whose PAT has since drifted (revoked,
|
|
746
|
+
// replaced, or joined by an extra live PAT) reads as unapplied, same as
|
|
747
|
+
// none recorded at all -- see this function's own doc comment.
|
|
748
|
+
const loginClientApplied = loginClientRecord !== null && !authDrifted;
|
|
749
|
+
// #216 round 2: read for a non-blocking note only -- see
|
|
750
|
+
// {@link PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY}'s own doc
|
|
751
|
+
// comment for why this never holds the step back the way the admin key's
|
|
752
|
+
// own `undelivered` check below does.
|
|
753
|
+
const loginClientUndelivered = recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY];
|
|
754
|
+
if (recordedIds[key] &&
|
|
755
|
+
connectorApplied &&
|
|
756
|
+
brokerApplied &&
|
|
757
|
+
brokerDeployed &&
|
|
758
|
+
loginClientApplied) {
|
|
486
759
|
return { step: null, blocked: null };
|
|
760
|
+
}
|
|
487
761
|
// The admin key was minted but never reached the host. ZITADEL shows a
|
|
488
762
|
// token once, so it is lost: minting another would hide that, and skipping
|
|
489
763
|
// would leave foundry without its admin key. Report it, plan nothing.
|
|
@@ -494,7 +768,16 @@ function planAuth(platform, recordedIds, executorBlocked) {
|
|
|
494
768
|
blocked: `the platform's sign-in is blocked, not planned: the admin key of ZITADEL machine user ${undelivered} was minted but never delivered and cannot be shown again; delete that user in ZITADEL and remove the recorded key ${PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY} to mint a new one`,
|
|
495
769
|
};
|
|
496
770
|
}
|
|
497
|
-
|
|
771
|
+
// #211 round 6: the executor itself is unusable on this host -- nothing to
|
|
772
|
+
// plan, whatever the broker's recorded state says.
|
|
773
|
+
if (executorMissing)
|
|
774
|
+
return { step: null, blocked: executorBlocked ?? null };
|
|
775
|
+
// #211 round 3: a blocked reason with no broker yet created also means the
|
|
776
|
+
// executor is unusable (no auth dependencies) -- nothing to plan.
|
|
777
|
+
// A blocked reason with the broker already created means only its deploy
|
|
778
|
+
// is held back; plan the step anyway so it can keep retrying and, once
|
|
779
|
+
// #209/#210 land, deploy on the very next apply.
|
|
780
|
+
if (executorBlocked && !brokerApplied)
|
|
498
781
|
return { step: null, blocked: executorBlocked };
|
|
499
782
|
return {
|
|
500
783
|
step: {
|
|
@@ -505,14 +788,48 @@ function planAuth(platform, recordedIds, executorBlocked) {
|
|
|
505
788
|
desired: { hostname: platform.auth.hostname, gitRef: platform.auth.gitRef },
|
|
506
789
|
},
|
|
507
790
|
step: {
|
|
508
|
-
description: `provision the platform's own sign-in (ZITADEL) at ${platform.auth.hostname}`,
|
|
791
|
+
description: `provision the platform's own sign-in (ZITADEL, the login app and the sign-in broker) at ${platform.auth.hostname}`,
|
|
509
792
|
destroys: false,
|
|
510
793
|
migrates: false,
|
|
511
794
|
},
|
|
512
795
|
},
|
|
513
|
-
|
|
796
|
+
// #211 round 3: still surface a broker-deploy block for visibility even
|
|
797
|
+
// though the step above is planned -- see the doc comment above. #216
|
|
798
|
+
// round 2: when nothing executor-side is blocked, surface an in-flight
|
|
799
|
+
// (or abandoned) login-client mint the same way -- informational only,
|
|
800
|
+
// never a reason to skip planning. #216 round 4: same treatment for a
|
|
801
|
+
// drifted (already-delivered, now stale) login-client token. #216 round
|
|
802
|
+
// 5: and only claim "this apply re-mints" when `otherInputsMissing` is
|
|
803
|
+
// actually empty -- see this function's own doc comment.
|
|
804
|
+
blocked: executorBlocked ??
|
|
805
|
+
loginClientNeedsMintNote(loginClientUndelivered, loginClientApplied, authDrifted, loginClientRecord, otherInputsMissing),
|
|
514
806
|
};
|
|
515
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* #216 round 5: the note {@link planAuth} surfaces when the login-client key
|
|
810
|
+
* needs a re-mint it cannot yet perform, split out of `planAuth`'s own return
|
|
811
|
+
* statement so the "needs minting" question and the "can it actually mint
|
|
812
|
+
* right now" question are each answered in one place.
|
|
813
|
+
*/
|
|
814
|
+
function loginClientNeedsMintNote(loginClientUndelivered, loginClientApplied, authDrifted, loginClientRecord, otherInputsMissing) {
|
|
815
|
+
const undeliveredNeedsMint = Boolean(loginClientUndelivered) && !loginClientApplied;
|
|
816
|
+
let subject = null;
|
|
817
|
+
if (undeliveredNeedsMint) {
|
|
818
|
+
subject = `the login-client key of ZITADEL machine user ${loginClientUndelivered} was minted but not yet confirmed delivered`;
|
|
819
|
+
}
|
|
820
|
+
else if (authDrifted && loginClientRecord) {
|
|
821
|
+
subject = `the login-client token delivered for ZITADEL machine user ${loginClientRecord.userId} is no longer the sole live token on that user, likely revoked or replaced by a concurrent apply`;
|
|
822
|
+
}
|
|
823
|
+
if (subject === null)
|
|
824
|
+
return null;
|
|
825
|
+
if (otherInputsMissing.length > 0) {
|
|
826
|
+
return `${subject}; this apply cannot re-mint and redeliver it yet -- the platform's sign-in broker still needs ${otherInputsMissing.join(' and ')} first`;
|
|
827
|
+
}
|
|
828
|
+
const action = undeliveredNeedsMint
|
|
829
|
+
? 'this apply re-mints (revoking it) and retries'
|
|
830
|
+
: 'this apply re-mints and redelivers';
|
|
831
|
+
return `${subject}; ${action} -- no manual step needed`;
|
|
832
|
+
}
|
|
516
833
|
/**
|
|
517
834
|
* #169: the platform's own mail server (Stalwart), built fresh -- Stalwart on
|
|
518
835
|
* Coolify with its own storage, reached at `platform.mail.hostname` for
|
|
@@ -763,6 +1080,18 @@ function planAi(platform, recordedIds, executorBlocked) {
|
|
|
763
1080
|
recordedIds[PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY] !== platform.ai.gitRef;
|
|
764
1081
|
if (applied && !drifted)
|
|
765
1082
|
return { step: null, blocked: null };
|
|
1083
|
+
// #201: the signing pair foundry mints to authenticate its own calls to
|
|
1084
|
+
// ai's platform/keys route. Minted but not confirmed delivered and pinned:
|
|
1085
|
+
// the private half cannot be recovered, so report it rather than mint
|
|
1086
|
+
// another (the same reason the management key check below never mints
|
|
1087
|
+
// twice).
|
|
1088
|
+
const signingUndelivered = recordedIds[PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY];
|
|
1089
|
+
if (signingUndelivered && !recordedIds[PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY]) {
|
|
1090
|
+
return {
|
|
1091
|
+
step: null,
|
|
1092
|
+
blocked: `the platform's AI host is blocked, not planned: its signing key ${signingUndelivered} was minted but not confirmed delivered and pinned, and the private half cannot be recovered; check the held key and the runner's MANAGEMENT_PUBLIC_KEYS, then remove the recorded key ${PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY} to mint a new one`,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
766
1095
|
// Minted but never delivered: ai shows a key's secret once, so it is lost.
|
|
767
1096
|
// Minting another would hide that; report it and plan nothing.
|
|
768
1097
|
const undelivered = recordedIds[PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY];
|
|
@@ -772,6 +1101,16 @@ function planAi(platform, recordedIds, executorBlocked) {
|
|
|
772
1101
|
blocked: `the platform's AI host is blocked, not planned: AI management key ${undelivered} was minted but never delivered and cannot be shown again; revoke it in ai (keys:revoke --id ${undelivered}) and remove the recorded key ${PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY} to mint a new one`,
|
|
773
1102
|
};
|
|
774
1103
|
}
|
|
1104
|
+
// #201: ai reported its 'archon' key already exists with no secret to
|
|
1105
|
+
// show -- a mint this record never learned about. Never minted under
|
|
1106
|
+
// another name; a person must revoke the live key first.
|
|
1107
|
+
const conflict = recordedIds[PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY];
|
|
1108
|
+
if (conflict) {
|
|
1109
|
+
return {
|
|
1110
|
+
step: null,
|
|
1111
|
+
blocked: `the platform's AI host is blocked, not planned: ai already holds a management key named 'archon' (${conflict}) that was never recorded delivered, so its secret cannot be shown again; revoke it in ai and remove the recorded key ${PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY} to mint a new one`,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
775
1114
|
const migrateBlocked = recordedIds[PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY];
|
|
776
1115
|
if (migrateBlocked) {
|
|
777
1116
|
return {
|
|
@@ -819,6 +1158,11 @@ function planAi(platform, recordedIds, executorBlocked) {
|
|
|
819
1158
|
* planned in this same plan: Archon's sign-in client is created inside it, so
|
|
820
1159
|
* the step could only throw.
|
|
821
1160
|
*
|
|
1161
|
+
* #213: also puts {@link PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF},
|
|
1162
|
+
* {@link PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF} and
|
|
1163
|
+
* {@link PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF} as held keys, before
|
|
1164
|
+
* the app deploys -- `putArchonMasterKeys` in `platform-providers.ts`.
|
|
1165
|
+
*
|
|
822
1166
|
* #197: a recorded app with no recorded `cloudflared` connector
|
|
823
1167
|
* ({@link PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY}) plans the step again, as
|
|
824
1168
|
* {@link planAuth} does.
|
|
@@ -831,6 +1175,13 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted)
|
|
|
831
1175
|
Boolean(recordedIds[PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY]) &&
|
|
832
1176
|
recordedIds[PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY] === platform.archon.hostname &&
|
|
833
1177
|
recordedIds[PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY] === platform.archon.gitRef &&
|
|
1178
|
+
// #211 round 4: a broker recreated under a new app id (the old one
|
|
1179
|
+
// deleted by hand, or `platform:auth` minting a fresh one) leaves
|
|
1180
|
+
// Archon's last registration on a broker that no longer exists, and
|
|
1181
|
+
// nothing else here would ever plan the step again to redo it.
|
|
1182
|
+
Boolean(recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY]) &&
|
|
1183
|
+
recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ===
|
|
1184
|
+
recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY] &&
|
|
834
1185
|
!recordedIds[PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY];
|
|
835
1186
|
if (applied && !drifted)
|
|
836
1187
|
return { step: null, blocked: null };
|
|
@@ -857,7 +1208,9 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted)
|
|
|
857
1208
|
desired: { hostname: platform.archon.hostname, gitRef: platform.archon.gitRef },
|
|
858
1209
|
},
|
|
859
1210
|
step: {
|
|
860
|
-
description: `provision Archon at ${platform.archon.hostname}
|
|
1211
|
+
description: `provision Archon at ${platform.archon.hostname}, putting its master keys ` +
|
|
1212
|
+
`(${PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF}, ${PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF}, ` +
|
|
1213
|
+
`${PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF}) as held keys if they are not held yet`,
|
|
861
1214
|
destroys: false,
|
|
862
1215
|
migrates: false,
|
|
863
1216
|
},
|
|
@@ -4,8 +4,9 @@ import type { AgoraHostValues } from './agora.js';
|
|
|
4
4
|
* https://github.com/wtfalch/foundry/issues/177: what an org step needs to
|
|
5
5
|
* know about the platform it runs on -- whether each shared service it
|
|
6
6
|
* depends on is enabled *and* applied (not just declared), and the one
|
|
7
|
-
* address `coolify:mail`'s own step used to read
|
|
8
|
-
*
|
|
7
|
+
* address `coolify:mail`'s own step used to read off two fixed, hand-built
|
|
8
|
+
* constants in `executor.ts` (removed for #205; `mailHost` below is what
|
|
9
|
+
* replaced them). Built by
|
|
9
10
|
* {@link orgPlatformReadinessFrom} from the very `DeclaredPlatform` + applied
|
|
10
11
|
* service ids `planPlatform` (`platform-diff.ts`) already reasons over -- the
|
|
11
12
|
* same "declared vs applied" distinction every platform step already makes
|