@wtfalch/foundry 0.32.0 → 0.34.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 +18 -1
- package/dist/bootstrap/bootstrap.js +167 -28
- package/dist/bootstrap/declare.d.ts +5 -1
- package/dist/bootstrap/declare.js +3 -1
- package/dist/bootstrap/index.d.ts +1 -1
- package/dist/bootstrap/versions.d.ts +4 -0
- package/dist/calls-sfu.d.ts +10 -0
- package/dist/calls-sfu.js +41 -0
- package/dist/index.d.ts +18 -8
- package/dist/index.js +8 -3
- package/dist/migrations/0051_org_services.sql +13 -0
- package/dist/migrations/0052_platform_mail_relay.sql +8 -0
- package/dist/org.d.ts +24 -2
- package/dist/org.js +42 -0
- package/dist/plan/boule.d.ts +24 -1
- package/dist/plan/boule.js +29 -1
- package/dist/plan/calls-matrix.d.ts +9 -0
- package/dist/plan/calls-matrix.js +17 -0
- package/dist/plan/cloudflared-connector.d.ts +7 -1
- package/dist/plan/cloudflared-connector.js +14 -1
- package/dist/plan/diff.d.ts +32 -1
- package/dist/plan/diff.js +383 -26
- package/dist/plan/executor.d.ts +177 -7
- package/dist/plan/executor.js +738 -51
- package/dist/plan/health-check-settings.d.ts +28 -0
- package/dist/plan/health-check-settings.js +36 -0
- package/dist/plan/health-gaps.d.ts +33 -0
- package/dist/plan/health-gaps.js +89 -0
- package/dist/plan/health-targets.js +1 -1
- package/dist/plan/held-key-refs.d.ts +10 -0
- package/dist/plan/held-key-refs.js +10 -0
- package/dist/plan/index.d.ts +16 -6
- package/dist/plan/index.js +8 -3
- package/dist/plan/live.d.ts +5 -2
- package/dist/plan/live.js +15 -2
- package/dist/plan/locked-hostnames.d.ts +5 -2
- package/dist/plan/locked-hostnames.js +9 -1
- package/dist/plan/login-host.d.ts +62 -2
- package/dist/plan/login-host.js +104 -1
- package/dist/plan/mail-relay.d.ts +120 -0
- package/dist/plan/mail-relay.js +323 -0
- package/dist/plan/org-names.d.ts +24 -0
- package/dist/plan/org-names.js +52 -0
- package/dist/plan/own-diff.d.ts +169 -0
- package/dist/plan/own-diff.js +432 -0
- package/dist/plan/own-dns.d.ts +6 -0
- package/dist/plan/own-dns.js +15 -0
- package/dist/plan/own-limits.d.ts +22 -0
- package/dist/plan/own-limits.js +101 -0
- package/dist/plan/own-services.d.ts +193 -0
- package/dist/plan/own-services.js +167 -0
- package/dist/plan/own-sfu.d.ts +69 -0
- package/dist/plan/own-sfu.js +255 -0
- package/dist/plan/plan-apply.d.ts +101 -7
- package/dist/plan/plan-apply.js +216 -12
- package/dist/plan/platform-diff.d.ts +61 -12
- package/dist/plan/platform-diff.js +140 -34
- package/dist/plan/platform-org.d.ts +36 -0
- package/dist/plan/platform-org.js +96 -2
- package/dist/plan/platform-providers.d.ts +407 -12
- package/dist/plan/platform-providers.js +947 -422
- package/dist/plan/sign-in-org.d.ts +99 -0
- package/dist/plan/sign-in-org.js +151 -0
- package/dist/plan/zitadel-login-policy.js +1 -1
- package/dist/plan/zitadel-system-api.d.ts +22 -1
- package/dist/plan/zitadel-system-api.js +49 -8
- package/dist/plan.d.ts +18 -0
- package/dist/platform.d.ts +8 -0
- package/dist/slug.js +1 -0
- package/dist/store/postgres.js +51 -2
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +5 -0
- package/package.json +1 -1
- package/versions.json +3 -1
package/dist/plan/diff.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
|
-
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, ownerMailboxRecorded, ownerMailboxes, } from '../org.js';
|
|
2
|
+
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, ownerMailboxRecorded, ownerMailboxes, ownsServices, } from '../org.js';
|
|
3
3
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, chatCallsApplied, chatHasUpstreams, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
4
|
-
import { BOULE_BROKER_ENTRY_KEY, BOULE_MAIL_TOKEN_ENV, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
4
|
+
import { BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_MAIL_TOKEN_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
6
|
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
7
7
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
@@ -10,12 +10,15 @@ import { planFilesKeys } from './entry-files-key.js';
|
|
|
10
10
|
import { hasWildcardGate, planGates } from './gates.js';
|
|
11
11
|
import { appHostname, entryHostnames, tunnelDnsHostnames, tunnelDomainList } from './hostnames.js';
|
|
12
12
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
13
|
-
import { LOGIN_BINDING_SCOPE_KEY, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
13
|
+
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, orgLoginHost, ownAuthApplied, 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 {
|
|
15
|
+
import { OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, planOwnSfu, } from './own-diff.js';
|
|
16
|
+
import { ownDnsHostnames } from './own-dns.js';
|
|
17
|
+
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, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
16
18
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
17
19
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
18
20
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec } from './proxy-labels.js';
|
|
21
|
+
import { ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY, ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, signInDomainBlockedText, signInOrgDomain, } from './sign-in-org.js';
|
|
19
22
|
import { withStepSystem } from './step-system.js';
|
|
20
23
|
import { planWorkers } from './worker.js';
|
|
21
24
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
@@ -55,6 +58,10 @@ function resourceKey(system, kind, key) {
|
|
|
55
58
|
function madeByFoundry(org, r, recordedIds, recordedValues, platformNames) {
|
|
56
59
|
if (PROVIDER_KINDS[r.system]?.includes(r.kind) && recordedValues.has(r.key))
|
|
57
60
|
return true;
|
|
61
|
+
// The reader maps an applied own step's app onto its marker (`coolify:own-<service>:<org.id>`); only this org's id is its own.
|
|
62
|
+
if (r.system === 'coolify' && OWN_MARKER_KINDS.has(r.kind) && r.key === org.id) {
|
|
63
|
+
return Boolean(recordedIds[`coolify:${r.kind}:${org.id}`]);
|
|
64
|
+
}
|
|
58
65
|
if (r.system === 'coolify') {
|
|
59
66
|
if (r.kind === 'environment')
|
|
60
67
|
return true;
|
|
@@ -77,6 +84,11 @@ function madeByFoundry(org, r, recordedIds, recordedValues, platformNames) {
|
|
|
77
84
|
if (type === 'CNAME' && content.endsWith('.cfargotunnel.com') && tunnelHostnames.includes(name)) {
|
|
78
85
|
return true;
|
|
79
86
|
}
|
|
87
|
+
if (type === 'CNAME' &&
|
|
88
|
+
content.endsWith('.cfargotunnel.com') &&
|
|
89
|
+
ownDnsHostnames(recordedIds).includes(name)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
80
92
|
const mailDomain = recordedIds[MAIL_DOMAIN_SCOPE_KEY]?.toLowerCase();
|
|
81
93
|
if (!mailDomain)
|
|
82
94
|
return false;
|
|
@@ -85,6 +97,14 @@ function madeByFoundry(org, r, recordedIds, recordedValues, platformNames) {
|
|
|
85
97
|
}
|
|
86
98
|
return name === `_dmarc.${mailDomain}` || name.endsWith(`._domainkey.${mailDomain}`);
|
|
87
99
|
}
|
|
100
|
+
const OWN_MARKER_KINDS = new Set([
|
|
101
|
+
'own-auth',
|
|
102
|
+
'own-mail',
|
|
103
|
+
'own-files',
|
|
104
|
+
'own-files-r2',
|
|
105
|
+
'own-jobs',
|
|
106
|
+
'own-sfu',
|
|
107
|
+
]);
|
|
88
108
|
/** The kinds a provider lists a resource under before a recorded `system:kind:key` maps it to foundry's own. */
|
|
89
109
|
const PROVIDER_KINDS = {
|
|
90
110
|
coolify: ['application', 'database'],
|
|
@@ -165,11 +185,34 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
165
185
|
// The platform's own org: its domain is the platform apex (the same fact
|
|
166
186
|
// #491 reads for mail). It signs in on the shared instance's own host, so
|
|
167
187
|
// `issuer.usesDomain` never moves it.
|
|
168
|
-
const isPlatformOrg = isPlatformOwnOrg(org, platformReadiness.
|
|
188
|
+
const isPlatformOrg = isPlatformOwnOrg(org, platformReadiness.platformOwnerOrgId);
|
|
189
|
+
// An own org's sign-in comes before every step that signs in through it.
|
|
190
|
+
const ownAuth = planOwnAuth({
|
|
191
|
+
org,
|
|
192
|
+
recordedIds,
|
|
193
|
+
readiness: platformReadiness,
|
|
194
|
+
isPlatformOrg,
|
|
195
|
+
ownServices: options.ownServices ?? false,
|
|
196
|
+
zone: org.domain ? zoneReadiness(org.domain, byKey) : null,
|
|
197
|
+
mailKeyHeld: options.ownMailKeyHeld ?? false,
|
|
198
|
+
mailKeyMintable: options.ownMailKeyMintable ?? false,
|
|
199
|
+
});
|
|
200
|
+
if (ownAuth.step)
|
|
201
|
+
plannedSteps.push(ownAuth.step);
|
|
202
|
+
// An own org's ZITADEL is the company's own instance: nothing that needs it plans before it exists.
|
|
203
|
+
const ownSignInPending = ownsServices(org) && !isPlatformOrg && !ownAuthApplied(org, recordedIds);
|
|
169
204
|
const issuer = planIssuer(org, byKey, claimed, platformReadiness.authReady, isPlatformOrg);
|
|
170
205
|
if (issuer.step)
|
|
171
206
|
plannedSteps.push(issuer.step);
|
|
172
|
-
|
|
207
|
+
// Before the domain and the owners: no invite runs on an org still open to registration.
|
|
208
|
+
const registrationClosed = planOrgRegistrationClosed(org, recordedIds, platformReadiness.authReady, issuer.step);
|
|
209
|
+
if (registrationClosed)
|
|
210
|
+
plannedSteps.push(registrationClosed);
|
|
211
|
+
const orgDomain = planOrgDomain(org, recordedIds, platformReadiness.authReady, issuer.step);
|
|
212
|
+
if (orgDomain)
|
|
213
|
+
plannedSteps.push(orgDomain);
|
|
214
|
+
const owners = planOwners(org, recordedIds, platformReadiness.authReady, platformReadiness.authIssuer, options.ownerInviteMail ?? false, registrationClosed !== null);
|
|
215
|
+
plannedSteps.push(...owners.steps);
|
|
173
216
|
// #267: after the ZITADEL org step, before any app's own issuer
|
|
174
217
|
// application. Apps created later in this run are not in the binding yet;
|
|
175
218
|
// the next run replans this step and repoints them.
|
|
@@ -204,6 +247,17 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
204
247
|
else if (zone.step) {
|
|
205
248
|
plannedSteps.push(zone.step);
|
|
206
249
|
}
|
|
250
|
+
const ownMail = planOwnMail({
|
|
251
|
+
org,
|
|
252
|
+
recordedIds,
|
|
253
|
+
readiness: platformReadiness,
|
|
254
|
+
isPlatformOrg,
|
|
255
|
+
ownServices: options.ownServices ?? false,
|
|
256
|
+
mailRelayApplied: options.mailRelayApplied ?? false,
|
|
257
|
+
zoneActive: zone.step === null && !zoneWaiting,
|
|
258
|
+
});
|
|
259
|
+
if (ownMail.step)
|
|
260
|
+
plannedSteps.push(ownMail.step);
|
|
207
261
|
const mail = planMail(org, byKey, claimed, recordedIds, platformReadiness.mailHost, platformReadiness.platformApex, platformReadiness.platformLettermintProjectId);
|
|
208
262
|
// #238: own-domain mail publishes DNS records into the zone, so it waits for
|
|
209
263
|
// an active zone -- blocked, not planned (rule 2).
|
|
@@ -229,6 +283,19 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
229
283
|
const calls = planCalls(org, byKey, claimed, platformReadiness.callsReady);
|
|
230
284
|
if (calls.step)
|
|
231
285
|
plannedSteps.push(calls.step);
|
|
286
|
+
// An own org's media server, before the calls service that reads its addresses.
|
|
287
|
+
const ownSfu = planOwnSfu({
|
|
288
|
+
org,
|
|
289
|
+
recordedIds,
|
|
290
|
+
isPlatformOrg,
|
|
291
|
+
ownServices: options.ownServices ?? false,
|
|
292
|
+
zoneActive: zone.step === null && !zoneWaiting,
|
|
293
|
+
readiness: platformReadiness,
|
|
294
|
+
sfuPortsOpen: options.sfuPortsOpen ?? false,
|
|
295
|
+
keysHeld: options.ownSfuKeysHeld ?? false,
|
|
296
|
+
});
|
|
297
|
+
if (ownSfu.step)
|
|
298
|
+
plannedSteps.push(ownSfu.step);
|
|
232
299
|
// Right after the Matrix stack (#145): its own executor
|
|
233
300
|
// (`executor.ts`'s `coolifyCallsService`) reads the Synapse/MAS addresses
|
|
234
301
|
// and MAS client credentials `coolifyCalls` just recorded, the same
|
|
@@ -260,7 +327,30 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
260
327
|
if (masChange)
|
|
261
328
|
versionChanges.push(masChange);
|
|
262
329
|
}
|
|
263
|
-
|
|
330
|
+
// An own org's files host, before its organisation on it.
|
|
331
|
+
const ownFiles = planOwnFiles({
|
|
332
|
+
org,
|
|
333
|
+
recordedIds,
|
|
334
|
+
readiness: platformReadiness,
|
|
335
|
+
isPlatformOrg,
|
|
336
|
+
ownServices: options.ownServices ?? false,
|
|
337
|
+
filesDeps: options.ownFilesDeps ?? { keys: false, bucket: false },
|
|
338
|
+
});
|
|
339
|
+
if (ownFiles.step)
|
|
340
|
+
plannedSteps.push(ownFiles.step);
|
|
341
|
+
// After the host is applied: its storage key is delivered to the running host.
|
|
342
|
+
const ownFilesR2 = options.ownFilesR2Ports
|
|
343
|
+
? planOwnFilesR2({
|
|
344
|
+
org,
|
|
345
|
+
recordedIds,
|
|
346
|
+
isPlatformOrg,
|
|
347
|
+
ownServices: options.ownServices ?? false,
|
|
348
|
+
ports: options.ownFilesR2Ports,
|
|
349
|
+
})
|
|
350
|
+
: { step: null, blocked: null };
|
|
351
|
+
if (ownFilesR2.step)
|
|
352
|
+
plannedSteps.push(ownFilesR2.step);
|
|
353
|
+
const filesOrganisation = planFilesOrganisation(org, byKey, claimed, platformReadiness.filesReady, platformReadiness.missingKeys.files, platformReadiness.noKeyReader, ownsServices(org) && !isPlatformOrg && !recordedIds[`coolify:own-files:${org.id}`]);
|
|
264
354
|
if (filesOrganisation.step)
|
|
265
355
|
plannedSteps.push(filesOrganisation.step);
|
|
266
356
|
const aiOrganisation = planAiOrganisation(org, byKey, claimed, platformReadiness.aiReady, platformReadiness.missingKeys.ai, platformReadiness.noKeyReader);
|
|
@@ -402,7 +492,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
402
492
|
const agoraGuarded = guardApp(agoraClient);
|
|
403
493
|
if (agoraGuarded)
|
|
404
494
|
plannedSteps.push(agoraGuarded);
|
|
405
|
-
const agora = planAgora(org, byKey, claimed, recordedIds, agoraHost
|
|
495
|
+
const agora = planAgora(org, byKey, claimed, recordedIds, agoraHost, isPlatformOrg, {
|
|
496
|
+
servesDiscovery: options.loginHostServesDiscovery ?? false,
|
|
497
|
+
});
|
|
406
498
|
if (agora.step)
|
|
407
499
|
plannedSteps.push(agora.step);
|
|
408
500
|
// #161 (Boule part): every org's own Boule -- always on, no toggle, the same
|
|
@@ -424,10 +516,23 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
424
516
|
const bouleBroker = planBouleBrokerEntry(org, recordedIds, plannedSteps, options.broker ?? false);
|
|
425
517
|
if (bouleBroker.step)
|
|
426
518
|
plannedSteps.push(bouleBroker.step);
|
|
519
|
+
// An own org's scheduler, before its registration below.
|
|
520
|
+
const ownJobs = planOwnJobs({
|
|
521
|
+
org,
|
|
522
|
+
recordedIds,
|
|
523
|
+
readiness: platformReadiness,
|
|
524
|
+
isPlatformOrg,
|
|
525
|
+
ownServices: options.ownServices ?? false,
|
|
526
|
+
tokenHeld: options.ownJobsTokenHeld ?? false,
|
|
527
|
+
});
|
|
528
|
+
if (ownJobs.step)
|
|
529
|
+
plannedSteps.push(ownJobs.step);
|
|
427
530
|
// #173: register the org's own app with the platform's jobs scheduler.
|
|
428
531
|
// After Agora, so a failing registration can never stop Agora from
|
|
429
532
|
// provisioning (`runSteps` stops at the first failing step).
|
|
430
|
-
const jobsRegistration = planJobsRegistration(org, recordedIds,
|
|
533
|
+
const jobsRegistration = planJobsRegistration(org, recordedIds,
|
|
534
|
+
// An own org never registers with the shared scheduler the host supplies.
|
|
535
|
+
(jobsAvailable && !ownsServices(org)) || platformReadiness.jobsReady, platformReadiness.missingKeys.jobs, platformReadiness.noKeyReader, platformReadiness.jobsUrlMissing ?? false);
|
|
431
536
|
if (jobsRegistration.step)
|
|
432
537
|
plannedSteps.push(jobsRegistration.step);
|
|
433
538
|
// F8 (#318), after Agora and Boule, not right after the zone: a route
|
|
@@ -480,6 +585,8 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
480
585
|
// item list, so it rides along with the unmanaged items.
|
|
481
586
|
if (agora.blocked)
|
|
482
587
|
unmanaged.push({ description: agora.blocked, key: 'coolify:agora' });
|
|
588
|
+
if (agora.issuerNote)
|
|
589
|
+
unmanaged.push({ description: agora.issuerNote, key: 'coolify:agora' });
|
|
483
590
|
if (boule.blocked)
|
|
484
591
|
unmanaged.push({ description: boule.blocked, key: 'coolify:boule' });
|
|
485
592
|
if (boule.mailBlocked) {
|
|
@@ -494,6 +601,22 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
494
601
|
unmanaged.push(...edgeWorkers.blocked);
|
|
495
602
|
unmanaged.push(...entryEdgeKeys.blocked);
|
|
496
603
|
unmanaged.push(...edgeReservedLabels.blocked);
|
|
604
|
+
if (ownAuth.blocked)
|
|
605
|
+
unmanaged.push({ description: ownAuth.blocked, key: OWN_AUTH_BLOCKED_KEY });
|
|
606
|
+
if (ownJobs.blocked)
|
|
607
|
+
unmanaged.push({ description: ownJobs.blocked, key: OWN_JOBS_BLOCKED_KEY });
|
|
608
|
+
if (ownMail.blocked)
|
|
609
|
+
unmanaged.push({ description: ownMail.blocked, key: OWN_MAIL_BLOCKED_KEY });
|
|
610
|
+
if (ownSfu.blocked)
|
|
611
|
+
unmanaged.push({ description: ownSfu.blocked, key: OWN_SFU_BLOCKED_KEY });
|
|
612
|
+
if (ownSfu.firewall)
|
|
613
|
+
unmanaged.push({ description: ownSfu.firewall, key: OWN_SFU_FIREWALL_KEY });
|
|
614
|
+
if (ownFiles.blocked) {
|
|
615
|
+
unmanaged.push({ description: ownFiles.blocked, key: OWN_FILES_BLOCKED_KEY });
|
|
616
|
+
}
|
|
617
|
+
if (ownFilesR2.blocked) {
|
|
618
|
+
unmanaged.push({ description: ownFilesR2.blocked, key: OWN_FILES_R2_BLOCKED_KEY });
|
|
619
|
+
}
|
|
497
620
|
// Same for a jobs registration that cannot run (no scheduler wired) or must
|
|
498
621
|
// not run again (minted, never delivered).
|
|
499
622
|
if (jobsRegistration.blocked) {
|
|
@@ -504,8 +627,23 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
504
627
|
// `platformReadiness` instead of a host-supplied port. Each is reported at
|
|
505
628
|
// most once even though a dependent step (owner, calls-service, storage,
|
|
506
629
|
// budget, key) would otherwise also have nothing to act on.
|
|
507
|
-
if (
|
|
508
|
-
|
|
630
|
+
if (ownSignInPending) {
|
|
631
|
+
// The sign-in step's own reason, when it has one, already says why the rest waits.
|
|
632
|
+
if (!ownAuth.blocked) {
|
|
633
|
+
unmanaged.push({
|
|
634
|
+
description: `${org.slug}'s ZITADEL steps are blocked, not planned: its own sign-in service is not applied yet`,
|
|
635
|
+
key: 'coolify:own-auth',
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
if (issuer.blocked)
|
|
641
|
+
unmanaged.push({ description: issuer.blocked, key: 'zitadel:issuer' });
|
|
642
|
+
unmanaged.push(...owners.blocked);
|
|
643
|
+
}
|
|
644
|
+
const domainBlocked = orgDomainBlocked(org, recordedIds);
|
|
645
|
+
if (domainBlocked)
|
|
646
|
+
unmanaged.push(domainBlocked);
|
|
509
647
|
// #267 (rule 2): a login host with no ZITADEL org to trust it in is blocked, not planned to throw.
|
|
510
648
|
if (loginHost.blocked) {
|
|
511
649
|
unmanaged.push({ description: loginHost.blocked, key: 'zitadel:login-host' });
|
|
@@ -555,12 +693,31 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
555
693
|
unmanaged.push(...bouleBroker.blocked);
|
|
556
694
|
return {
|
|
557
695
|
orgId: org.id,
|
|
558
|
-
plannedSteps:
|
|
696
|
+
plannedSteps: (ownSignInPending
|
|
697
|
+
? plannedSteps.filter((s) => !NEEDS_OWN_ZITADEL.has(`${s.action.system}:${s.action.kind}`))
|
|
698
|
+
: plannedSteps).map(withStepSystem),
|
|
559
699
|
unmanaged,
|
|
560
700
|
versionChanges,
|
|
561
701
|
orgPostgres,
|
|
562
702
|
};
|
|
563
703
|
}
|
|
704
|
+
/** Steps that run against the org's own ZITADEL instance (its projects, apps and broker entries). */
|
|
705
|
+
const NEEDS_OWN_ZITADEL = new Set([
|
|
706
|
+
'zitadel:issuer',
|
|
707
|
+
'zitadel:registration-closed',
|
|
708
|
+
'zitadel:domain',
|
|
709
|
+
'zitadel:owner',
|
|
710
|
+
'zitadel:app',
|
|
711
|
+
'zitadel:customers',
|
|
712
|
+
'zitadel:id-token-userinfo',
|
|
713
|
+
'zitadel:login-host',
|
|
714
|
+
'coolify:chat',
|
|
715
|
+
'coolify:calls-service',
|
|
716
|
+
'coolify:calls-chat',
|
|
717
|
+
'coolify:broker-entry',
|
|
718
|
+
'coolify:boule-broker-entry',
|
|
719
|
+
'files:organisation',
|
|
720
|
+
]);
|
|
564
721
|
/**
|
|
565
722
|
* #553: a plain ZITADEL app provision (not a `recreate`) is planned only when
|
|
566
723
|
* the live listing lacks the app by name. If an app id is recorded for it,
|
|
@@ -698,6 +855,70 @@ function planIssuer(org, byKey, claimed, authReady, isPlatformOrg = false) {
|
|
|
698
855
|
blocked: null,
|
|
699
856
|
};
|
|
700
857
|
}
|
|
858
|
+
/**
|
|
859
|
+
* Closes self-registration on a ZITADEL org that already exists and has no
|
|
860
|
+
* recorded close: one made before foundry closed it, or one whose close failed
|
|
861
|
+
* inside the issuer step. A fresh org is closed inside the issuer step, so
|
|
862
|
+
* nothing is planned beside that step. While this step is planned no owner is
|
|
863
|
+
* invited ({@link planOwners}).
|
|
864
|
+
*/
|
|
865
|
+
function planOrgRegistrationClosed(org, recordedIds, authReady, issuerStep) {
|
|
866
|
+
if (!authReady)
|
|
867
|
+
return null;
|
|
868
|
+
if (issuerStep?.action.desired.provision === true)
|
|
869
|
+
return null;
|
|
870
|
+
if (!recordedIds.zitadel_org)
|
|
871
|
+
return null;
|
|
872
|
+
if (recordedIds[ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY] === recordedIds.zitadel_org) {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
action: { system: 'zitadel', kind: 'registration-closed', key: org.id, desired: {} },
|
|
877
|
+
step: {
|
|
878
|
+
description: `switch self-registration off on ${org.slug}'s sign-in org`,
|
|
879
|
+
destroys: false,
|
|
880
|
+
migrates: false,
|
|
881
|
+
},
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
/** The blocked item for an org sign-in domain the sign-in server refused to make primary. */
|
|
885
|
+
function orgDomainBlocked(org, recordedIds) {
|
|
886
|
+
const domain = signInOrgDomain(org);
|
|
887
|
+
if (recordedIds[ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY]?.toLowerCase() !== domain)
|
|
888
|
+
return null;
|
|
889
|
+
if (recordedIds[ZITADEL_ORG_DOMAIN_SCOPE_KEY]?.toLowerCase() === domain)
|
|
890
|
+
return null;
|
|
891
|
+
return { description: signInDomainBlockedText(org.slug, domain), key: 'zitadel:domain' };
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Registers the org's sign-in domain (its own, or `<slug>.oikos.host`) on its ZITADEL org
|
|
895
|
+
* and makes it primary, so an owner signs in as `name@<domain>`. Idempotent on the
|
|
896
|
+
* *recorded* `zitadel_org_domain`: a changed declared domain plans it again. A fresh
|
|
897
|
+
* org gets its domain inside the issuer step, so nothing is planned beside that step.
|
|
898
|
+
* Nothing is planned until the ZITADEL org exists.
|
|
899
|
+
*/
|
|
900
|
+
function planOrgDomain(org, recordedIds, authReady, issuerStep) {
|
|
901
|
+
if (!authReady)
|
|
902
|
+
return null;
|
|
903
|
+
if (issuerStep?.action.desired.provision === true)
|
|
904
|
+
return null;
|
|
905
|
+
if (!recordedIds.zitadel_org)
|
|
906
|
+
return null;
|
|
907
|
+
const domain = signInOrgDomain(org);
|
|
908
|
+
if (recordedIds[ZITADEL_ORG_DOMAIN_SCOPE_KEY]?.toLowerCase() === domain)
|
|
909
|
+
return null;
|
|
910
|
+
// Refused once: reported as blocked (see `orgDomainBlocked`), not planned again.
|
|
911
|
+
if (recordedIds[ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY]?.toLowerCase() === domain)
|
|
912
|
+
return null;
|
|
913
|
+
return {
|
|
914
|
+
action: { system: 'zitadel', kind: 'domain', key: org.id, desired: { domain } },
|
|
915
|
+
step: {
|
|
916
|
+
description: `register ${domain} as ${org.slug}'s sign-in domain`,
|
|
917
|
+
destroys: false,
|
|
918
|
+
migrates: false,
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
}
|
|
701
922
|
/**
|
|
702
923
|
* The org's owners, one step each, each invited by ZITADEL email once the org's ZITADEL org
|
|
703
924
|
* exists (goal line: "the org's owner is invited by ZITADEL email and sets a
|
|
@@ -718,6 +939,11 @@ function planIssuer(org, byKey, claimed, authReady, isPlatformOrg = false) {
|
|
|
718
939
|
* issuer step immediately before it in the same run: `plan-apply.ts`'s
|
|
719
940
|
* `runSteps` awaits each planned step in array order, one at a time.
|
|
720
941
|
*
|
|
942
|
+
* An owner signs in as `name@<org domain>`, so each unrecorded owner needs a work
|
|
943
|
+
* address (`mailboxAddress`, or `mail.ownerMailboxAddress` for the first owner) and the
|
|
944
|
+
* host needs an `ownerInviteMail` port to send the first-sign-in link to the personal
|
|
945
|
+
* address. Without either, that owner is a blocked item named by position (rule 2).
|
|
946
|
+
*
|
|
721
947
|
* `desired` carries the owner's email so the executor can pass it to
|
|
722
948
|
* `ZitadelClient.inviteOrgOwner` — but the step's own `description` never
|
|
723
949
|
* does. `Plan` (the public contract `AuditLog.recordPlan`/`recordApply`
|
|
@@ -726,19 +952,40 @@ function planIssuer(org, byKey, claimed, authReady, isPlatformOrg = false) {
|
|
|
726
952
|
* nothing here puts it anywhere beyond what audit already records for the
|
|
727
953
|
* org.
|
|
728
954
|
*/
|
|
729
|
-
function planOwners(org, recordedIds, authReady, authIssuer) {
|
|
955
|
+
function planOwners(org, recordedIds, authReady, authIssuer, ownerInviteMail, registrationOpen = false) {
|
|
956
|
+
const steps = [];
|
|
957
|
+
const blocked = [];
|
|
958
|
+
// The close step is planned in this plan; the invites come with the next one, after it succeeded.
|
|
959
|
+
if (registrationOpen)
|
|
960
|
+
return { steps, blocked };
|
|
730
961
|
// #177: nothing to invite into yet -- `planIssuer` already reports the
|
|
731
962
|
// platform's sign-in as blocked, so these steps stay silent rather than
|
|
732
963
|
// repeating it.
|
|
733
964
|
if (!authReady)
|
|
734
|
-
return
|
|
735
|
-
const
|
|
965
|
+
return { steps, blocked };
|
|
966
|
+
const mailboxes = ownerMailboxes(org);
|
|
736
967
|
org.owners.forEach((owner, index) => {
|
|
737
968
|
// Every owner is keyed by its lowercased email (as `validateOwners` compares
|
|
738
969
|
// it), so reordering the list never moves a key onto another person.
|
|
739
970
|
const key = `${org.id}:${owner.email.toLowerCase()}`;
|
|
740
971
|
if (recordedIds[resourceKey('zitadel', 'owner', key)])
|
|
741
972
|
return; // already invited.
|
|
973
|
+
const position = index + 1;
|
|
974
|
+
const mailboxAddress = mailboxes[index]?.address;
|
|
975
|
+
if (!mailboxAddress) {
|
|
976
|
+
blocked.push({
|
|
977
|
+
description: `${org.slug}'s owner ${position} is blocked, not planned: no mailboxAddress is declared, and the owner signs in as name@<the org's domain>`,
|
|
978
|
+
key: `zitadel:owner:${position}`,
|
|
979
|
+
});
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (!ownerInviteMail) {
|
|
983
|
+
blocked.push({
|
|
984
|
+
description: `${org.slug}'s owner ${position} is blocked, not planned: the host has no ownerInviteMail port, and the owner cannot open their work mailbox yet`,
|
|
985
|
+
key: `zitadel:owner:${position}`,
|
|
986
|
+
});
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
742
989
|
steps.push({
|
|
743
990
|
action: {
|
|
744
991
|
system: 'zitadel',
|
|
@@ -746,6 +993,7 @@ function planOwners(org, recordedIds, authReady, authIssuer) {
|
|
|
746
993
|
key,
|
|
747
994
|
desired: {
|
|
748
995
|
email: owner.email,
|
|
996
|
+
mailboxAddress,
|
|
749
997
|
givenName: owner.givenName,
|
|
750
998
|
familyName: owner.familyName,
|
|
751
999
|
// The platform's login app, where the invite link must land (it serves `/verify`).
|
|
@@ -761,7 +1009,7 @@ function planOwners(org, recordedIds, authReady, authIssuer) {
|
|
|
761
1009
|
},
|
|
762
1010
|
});
|
|
763
1011
|
});
|
|
764
|
-
return steps;
|
|
1012
|
+
return { steps, blocked };
|
|
765
1013
|
}
|
|
766
1014
|
/**
|
|
767
1015
|
* #488: the bare hostname out of an issuer URL (`https://<hostname>`, the
|
|
@@ -771,8 +1019,8 @@ function planOwners(org, recordedIds, authReady, authIssuer) {
|
|
|
771
1019
|
* computed `auth.<domain>` against, so the platform's own org never plans
|
|
772
1020
|
* its login host equal to the shared ZITADEL instance's own.
|
|
773
1021
|
*/
|
|
774
|
-
export function isPlatformOwnOrg(org,
|
|
775
|
-
return Boolean(
|
|
1022
|
+
export function isPlatformOwnOrg(org, platformOwnerOrgId) {
|
|
1023
|
+
return Boolean(platformOwnerOrgId && org.id === platformOwnerOrgId);
|
|
776
1024
|
}
|
|
777
1025
|
function platformAuthHostnameFrom(issuer) {
|
|
778
1026
|
if (!issuer)
|
|
@@ -822,6 +1070,9 @@ function platformAuthHostnameFrom(issuer) {
|
|
|
822
1070
|
*/
|
|
823
1071
|
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired, platformAuthHostname, isPlatformOrg) {
|
|
824
1072
|
const none = { step: null, blocked: null };
|
|
1073
|
+
// An own org's instance has no shared login host to bind: its issuer is the sign-in service's own.
|
|
1074
|
+
if (ownsServices(org) && !isPlatformOrg)
|
|
1075
|
+
return none;
|
|
825
1076
|
const key = resourceKey('zitadel', 'login-host', org.id);
|
|
826
1077
|
// The platform's own org signs in on the shared instance's host whatever
|
|
827
1078
|
// `issuer.usesDomain` says (turning it on would move the issuer).
|
|
@@ -834,7 +1085,7 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
834
1085
|
};
|
|
835
1086
|
}
|
|
836
1087
|
const sharedInstanceHost = isPlatformOrg && platformAuthHostname !== null;
|
|
837
|
-
const host = sharedInstanceHost ? platformAuthHostname :
|
|
1088
|
+
const host = orgLoginHost(org, sharedInstanceHost ? platformAuthHostname : null);
|
|
838
1089
|
// #488: the guard in `loginHostnameFor` fired -- the org's own domain host
|
|
839
1090
|
// would have been the platform's shared ZITADEL instance host.
|
|
840
1091
|
if (sharedInstanceHost) {
|
|
@@ -1391,7 +1642,9 @@ function planCalls(org, byKey, claimed, callsReady) {
|
|
|
1391
1642
|
if (org.calls.enabled && !byKey.get(key) && !callsReady) {
|
|
1392
1643
|
return {
|
|
1393
1644
|
step: null,
|
|
1394
|
-
blocked:
|
|
1645
|
+
blocked: ownsServices(org)
|
|
1646
|
+
? `${org.slug}'s calls Matrix stack is blocked, not planned: its own media server (coolify:own-sfu) is not applied yet`
|
|
1647
|
+
: `${org.slug}'s calls Matrix stack is blocked, not planned: the platform's shared calls SFU is off`,
|
|
1395
1648
|
};
|
|
1396
1649
|
}
|
|
1397
1650
|
// #248: the two images this stack's own apply would run, checked the same
|
|
@@ -1440,6 +1693,13 @@ function planCallsService(org, byKey, claimed, callsReady, recordedIds, callsBlo
|
|
|
1440
1693
|
if (org.calls.enabled && !byKey.get(key) && !callsReady) {
|
|
1441
1694
|
return { step: null, blocked: null };
|
|
1442
1695
|
}
|
|
1696
|
+
// An own org's service names its own media server, so it waits for it even when already provisioned.
|
|
1697
|
+
if (org.calls.enabled && ownsServices(org) && !callsReady) {
|
|
1698
|
+
return {
|
|
1699
|
+
step: null,
|
|
1700
|
+
blocked: `${org.slug}'s calls service is blocked, not planned: its own media server (coolify:own-sfu) is not applied yet`,
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1443
1703
|
// The introspection secret is shown once. One minted but never written to
|
|
1444
1704
|
// the app is lost, so this reports it and mints no second (platform rule 1).
|
|
1445
1705
|
const undelivered = recordedIds[CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY];
|
|
@@ -1667,13 +1927,21 @@ function notReadyReason(service, keyName, missingKeyRef, noKeyReader) {
|
|
|
1667
1927
|
* Files organisation already exists -- the old origin is left allowed
|
|
1668
1928
|
* rather than silently dropped.
|
|
1669
1929
|
*/
|
|
1670
|
-
function planFilesOrganisation(org, byKey, claimed, filesReady, missingKeyRef, noKeyReader = false
|
|
1930
|
+
function planFilesOrganisation(org, byKey, claimed, filesReady, missingKeyRef, noKeyReader = false,
|
|
1931
|
+
/** An own org whose files host (`coolify:own-files:<org.id>`) is not recorded yet. */
|
|
1932
|
+
ownFilesPending = false) {
|
|
1671
1933
|
const key = resourceKey('files', 'organisation', org.id);
|
|
1672
1934
|
const live = byKey.get(key);
|
|
1673
1935
|
if (live) {
|
|
1674
1936
|
claimed.add(key);
|
|
1675
1937
|
return { step: null, blocked: null };
|
|
1676
1938
|
}
|
|
1939
|
+
if (ownFilesPending) {
|
|
1940
|
+
return {
|
|
1941
|
+
step: null,
|
|
1942
|
+
blocked: `${org.slug}'s Files organisation is blocked, not planned: its own files host is not applied yet`,
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1677
1945
|
if (!filesReady) {
|
|
1678
1946
|
return {
|
|
1679
1947
|
step: null,
|
|
@@ -2046,14 +2314,32 @@ function planAgoraClient(org, byKey, claimed) {
|
|
|
2046
2314
|
* message naming each missing value, which `computePlanDetail` lists with the
|
|
2047
2315
|
* plan's non-step items, so every other step still applies.
|
|
2048
2316
|
*/
|
|
2049
|
-
function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
2317
|
+
function planAgora(org, byKey, claimed, recordedIds, reading, isPlatformOrg, issuerSwitch) {
|
|
2050
2318
|
const key = resourceKey('coolify', 'agora', org.id);
|
|
2051
2319
|
const live = byKey.get(key);
|
|
2052
2320
|
if (live)
|
|
2053
2321
|
claimed.add(key);
|
|
2322
|
+
const ownSignIn = ownsServices(org) && !isPlatformOrg;
|
|
2323
|
+
if (ownSignIn && !ownAuthApplied(org, recordedIds)) {
|
|
2324
|
+
return {
|
|
2325
|
+
step: null,
|
|
2326
|
+
blocked: `${org.slug}'s Agora is blocked, not planned: its own sign-in service is not applied yet`,
|
|
2327
|
+
issuerNote: null,
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
// The files host records its internal URL only while it applies, and Agora's
|
|
2331
|
+
// env takes its files upstream from it: planned in the same pass, the first
|
|
2332
|
+
// deploy would start without one. The company's files host exists only with a domain.
|
|
2333
|
+
if (ownSignIn && org.domain && !recordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY]) {
|
|
2334
|
+
return {
|
|
2335
|
+
step: null,
|
|
2336
|
+
blocked: `${org.slug}'s Agora is blocked, not planned: its own files host is not applied yet`,
|
|
2337
|
+
issuerNote: null,
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2054
2340
|
const error = 'error' in reading ? reading.error : null;
|
|
2055
|
-
const
|
|
2056
|
-
const missing = agoraHostMissing(
|
|
2341
|
+
const platformHost = withRecordedMessagingProject('error' in reading ? NO_AGORA_HOST_VALUES : reading, recordedIds);
|
|
2342
|
+
const missing = agoraHostMissing(platformHost);
|
|
2057
2343
|
if (missing.length > 0) {
|
|
2058
2344
|
// A fixed text, never the port's own message: this line is part of the
|
|
2059
2345
|
// stale-plan comparison, and an error that varies per call would refuse
|
|
@@ -2062,8 +2348,41 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
2062
2348
|
return {
|
|
2063
2349
|
step: null,
|
|
2064
2350
|
blocked: `${org.slug}'s Agora is blocked, not planned: the host supplies no ${missing.join(', ')}${why}`,
|
|
2351
|
+
issuerNote: null,
|
|
2065
2352
|
};
|
|
2066
2353
|
}
|
|
2354
|
+
// Agora signs in at the company's own login host, and refuses any issuer off
|
|
2355
|
+
// its own address's domain. Only this value changes: the calls token env,
|
|
2356
|
+
// chat and Boule keep reading the platform issuer from `reading`, and Agora
|
|
2357
|
+
// always plans. The company host is written only once the login host is
|
|
2358
|
+
// applied with Agora's client AND a live read said it serves discovery as
|
|
2359
|
+
// that issuer. An Agora already on the company host keeps it while the host
|
|
2360
|
+
// stays applied: a later failing read never flips it back (no oscillation).
|
|
2361
|
+
let host = platformHost;
|
|
2362
|
+
let issuerNote = null;
|
|
2363
|
+
const choice = agoraIssuerHostFor(org, isPlatformOrg);
|
|
2364
|
+
if (choice.mismatch) {
|
|
2365
|
+
issuerNote = `${org.slug}'s Agora cannot sign in: its login host ${loginHostnameFor(org)} is not on its Agora address ${agoraHostnameFor(org)}; the login host must be on the org's domain`;
|
|
2366
|
+
}
|
|
2367
|
+
else if (choice.host) {
|
|
2368
|
+
const loginHost = choice.host;
|
|
2369
|
+
const target = `https://${loginHost}`;
|
|
2370
|
+
const applied = loginHostServesAgora(loginHost, org, recordedIds);
|
|
2371
|
+
if (applied && (liveAgoraIssuer(live) === target || issuerSwitch.servesDiscovery)) {
|
|
2372
|
+
host = { ...platformHost, issuer: target };
|
|
2373
|
+
}
|
|
2374
|
+
else if (ownSignIn) {
|
|
2375
|
+
// No platform issuer to fall back on: Agora waits for the company's own.
|
|
2376
|
+
return {
|
|
2377
|
+
step: null,
|
|
2378
|
+
blocked: `${org.slug}'s Agora is blocked, not planned: ${target} does not answer OIDC discovery yet`,
|
|
2379
|
+
issuerNote: null,
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
else {
|
|
2383
|
+
issuerNote = `${org.slug}'s Agora keeps the platform issuer; the switch to ${target} is blocked, not planned, until ${loginHost} is applied with Agora's client and answers OIDC discovery`;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2067
2386
|
// The executor writes exactly these host values, so what the plan showed
|
|
2068
2387
|
// is what gets applied; the ids only the apply itself records are read at
|
|
2069
2388
|
// run time.
|
|
@@ -2084,6 +2403,7 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
2084
2403
|
},
|
|
2085
2404
|
},
|
|
2086
2405
|
blocked: null,
|
|
2406
|
+
issuerNote,
|
|
2087
2407
|
};
|
|
2088
2408
|
}
|
|
2089
2409
|
const liveEnv = (live.config.env ?? {});
|
|
@@ -2111,8 +2431,9 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
2111
2431
|
// actually serving this exact `env`; while it disagrees, the step stays
|
|
2112
2432
|
// planned no matter what Coolify's env store already shows.
|
|
2113
2433
|
const activated = recordedIds[AGORA_ACTIVATED_ENV_SCOPE_KEY] === agoraEnvRevision(env);
|
|
2114
|
-
if (drift.length === 0 && activated && !portDrift)
|
|
2115
|
-
return { step: null, blocked: null };
|
|
2434
|
+
if (drift.length === 0 && activated && !portDrift) {
|
|
2435
|
+
return { step: null, blocked: null, issuerNote };
|
|
2436
|
+
}
|
|
2116
2437
|
const reasons = [
|
|
2117
2438
|
...(drift.length > 0 ? [`${drift.join(', ')} missing or different`] : []),
|
|
2118
2439
|
...(portDrift ? ["ports_exposes wrong -- Traefik can't reach it"] : []),
|
|
@@ -2129,8 +2450,22 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
2129
2450
|
},
|
|
2130
2451
|
},
|
|
2131
2452
|
blocked: null,
|
|
2453
|
+
issuerNote,
|
|
2132
2454
|
};
|
|
2133
2455
|
}
|
|
2456
|
+
/** The issuer a live Agora's `AGORA_CONFIG` names, or null when it has none or is not JSON. */
|
|
2457
|
+
function liveAgoraIssuer(live) {
|
|
2458
|
+
const raw = (live?.config.env ?? {}).AGORA_CONFIG;
|
|
2459
|
+
if (raw === undefined)
|
|
2460
|
+
return null;
|
|
2461
|
+
try {
|
|
2462
|
+
const issuer = JSON.parse(raw)?.issuer;
|
|
2463
|
+
return typeof issuer === 'string' ? issuer : null;
|
|
2464
|
+
}
|
|
2465
|
+
catch {
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2134
2469
|
/**
|
|
2135
2470
|
* #161 (Boule part): every org gets one fixed ZITADEL web client named
|
|
2136
2471
|
* `boule`, so its Boule (https://github.com/wtfalch/boule) can sign anyone in.
|
|
@@ -2237,6 +2572,7 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
2237
2572
|
? `${org.slug}'s Boule mail is blocked, not planned: ${mailBlockedReason}`
|
|
2238
2573
|
: null;
|
|
2239
2574
|
const mailOn = mailWanted && !mailBlocked;
|
|
2575
|
+
const platformOwner = isPlatformOwnOrg(org, readiness.platformOwnerOrgId);
|
|
2240
2576
|
const action = {
|
|
2241
2577
|
system: 'coolify',
|
|
2242
2578
|
kind: 'boule',
|
|
@@ -2249,6 +2585,8 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
2249
2585
|
platformApex: readiness.platformApex,
|
|
2250
2586
|
platformLettermintProjectId: readiness.platformLettermintProjectId,
|
|
2251
2587
|
mailBlocked: mailBlocked !== null,
|
|
2588
|
+
// Only the platform's own org's Boule gets the peer password for Archon.
|
|
2589
|
+
platformOwner,
|
|
2252
2590
|
},
|
|
2253
2591
|
};
|
|
2254
2592
|
if (!live) {
|
|
@@ -2269,6 +2607,15 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
2269
2607
|
bouleMailTokenDrift(liveEnv[BOULE_MAIL_TOKEN_ENV], live.config.appMailToken)) {
|
|
2270
2608
|
drift.push(BOULE_MAIL_TOKEN_ENV);
|
|
2271
2609
|
}
|
|
2610
|
+
// The peer password is secret, so only its absence is compared.
|
|
2611
|
+
if (platformOwner && !liveEnv[BOULE_PEER_PASSWORD_ENV])
|
|
2612
|
+
drift.push(BOULE_PEER_PASSWORD_ENV);
|
|
2613
|
+
if (platformOwner && !liveEnv[BOULE_BRIDGE_SECRET_ENV])
|
|
2614
|
+
drift.push(BOULE_BRIDGE_SECRET_ENV);
|
|
2615
|
+
// Secrets written but Boule not restarted since: plan the step so it restarts, minting nothing.
|
|
2616
|
+
if (platformOwner && !recordedIds[BOULE_SECRETS_RESTARTED_SCOPE_KEY]) {
|
|
2617
|
+
drift.push('restart after the peer password and bridge secret');
|
|
2618
|
+
}
|
|
2272
2619
|
if (drift.length === 0)
|
|
2273
2620
|
return { step: null, blocked: null, mailBlocked };
|
|
2274
2621
|
return {
|
|
@@ -2755,6 +3102,16 @@ function planJobsRegistration(org, recordedIds, jobsAvailable, missingKeyRef = n
|
|
|
2755
3102
|
// The host says whether its executor has a scheduler to register with. A
|
|
2756
3103
|
// fixed text: it is part of the stale-plan comparison.
|
|
2757
3104
|
if (!jobsAvailable) {
|
|
3105
|
+
if (ownsServices(org)) {
|
|
3106
|
+
return {
|
|
3107
|
+
step: null,
|
|
3108
|
+
blocked: `${org.slug}'s jobs registration is blocked, not planned: ${addressMissing
|
|
3109
|
+
? `${org.slug}'s own jobs scheduler has not recorded its internal address yet; apply its own jobs step first`
|
|
3110
|
+
: missingKeyRef
|
|
3111
|
+
? `${org.slug}'s own jobs admin token ${missingKeyRef} is not held yet`
|
|
3112
|
+
: `${org.slug}'s own jobs scheduler is not applied yet`}`,
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
2758
3115
|
return {
|
|
2759
3116
|
step: null,
|
|
2760
3117
|
blocked: addressMissing
|