@wtfalch/foundry 0.45.1 → 0.46.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/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/org.d.ts +37 -0
- package/dist/org.js +135 -10
- package/dist/plan/agora.d.ts +14 -0
- package/dist/plan/agora.js +21 -0
- package/dist/plan/backup-gate.d.ts +15 -0
- package/dist/plan/backup-gate.js +38 -0
- package/dist/plan/boule-owners-http.d.ts +12 -0
- package/dist/plan/boule-owners-http.js +59 -0
- package/dist/plan/boule.d.ts +38 -2
- package/dist/plan/boule.js +72 -1
- package/dist/plan/company-backups.d.ts +7 -1
- package/dist/plan/company-backups.js +42 -2
- package/dist/plan/cookie-domain.d.ts +41 -0
- package/dist/plan/cookie-domain.js +71 -0
- package/dist/plan/desktop-auth.d.ts +58 -0
- package/dist/plan/desktop-auth.js +144 -0
- package/dist/plan/desktop-callback.d.ts +26 -0
- package/dist/plan/desktop-callback.js +59 -0
- package/dist/plan/diff.d.ts +13 -4
- package/dist/plan/diff.js +272 -84
- package/dist/plan/executor.d.ts +136 -6
- package/dist/plan/executor.js +725 -98
- package/dist/plan/health-gaps.js +5 -0
- package/dist/plan/index.d.ts +3 -0
- package/dist/plan/index.js +2 -0
- package/dist/plan/job-app.d.ts +23 -0
- package/dist/plan/job-app.js +41 -0
- package/dist/plan/live.d.ts +6 -0
- package/dist/plan/live.js +16 -0
- package/dist/plan/login-host.d.ts +28 -0
- package/dist/plan/login-host.js +36 -0
- package/dist/plan/org-names.js +9 -1
- package/dist/plan/org-postgres-job.d.ts +22 -0
- package/dist/plan/org-postgres-job.js +91 -0
- package/dist/plan/org-postgres-migrate.d.ts +115 -0
- package/dist/plan/org-postgres-migrate.js +466 -0
- package/dist/plan/org-postgres.d.ts +90 -0
- package/dist/plan/org-postgres.js +293 -0
- package/dist/plan/plan-apply.d.ts +30 -2
- package/dist/plan/plan-apply.js +104 -7
- package/dist/plan/platform-diff.d.ts +45 -0
- package/dist/plan/platform-diff.js +159 -4
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-org.js +4 -0
- package/dist/plan/platform-providers.d.ts +12 -2
- package/dist/plan/platform-providers.js +279 -9
- package/dist/plan/post-logout-uris.d.ts +22 -0
- package/dist/plan/post-logout-uris.js +43 -0
- package/dist/plan/providers-readers.d.ts +10 -1
- package/dist/plan/providers-readers.js +41 -1
- package/dist/plan/sign-in-bindings.d.ts +4 -0
- package/dist/plan/sign-in-bindings.js +10 -0
- package/dist/plan/sign-in-org.d.ts +19 -6
- package/dist/plan/sign-in-org.js +35 -10
- package/dist/plan/sign-in.d.ts +12 -5
- package/dist/plan/sign-in.js +12 -5
- package/dist/plan/signin-db-move.js +10 -34
- package/package.json +5 -5
package/dist/plan/diff.js
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
|
-
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, heldEnvRef, ownerMailboxRecorded, ownerMailboxes, ownsServices, } from '../org.js';
|
|
2
|
+
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, heldEnvRef, ownerMailboxRecorded, ownerMailboxes, ownerNameProblem, ownsServices, } from '../org.js';
|
|
3
3
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
4
4
|
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';
|
|
5
5
|
import { planBlueGreen } from './blue-green.js';
|
|
6
|
-
import { BOULE_ARCHON_CREDENTIAL_ENV, BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_CUSTOMERS_CREDENTIAL_ENV, BOULE_MAIL_TOKEN_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, CUSTOMERS_SIGN_IN_SCOPE_KEY, CUSTOMERS_SIGN_UP_SCOPE_KEY, archonCredentialRenewDue, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, customersOrgIdOf, desiredBouleEnv, staleCustomersEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
6
|
+
import { BOULE_ARCHON_CREDENTIAL_ENV, BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_CUSTOMERS_CREDENTIAL_ENV, BOULE_MAIL_TOKEN_ENV, BOULE_OWNER_SEED_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, CUSTOMERS_SIGN_IN_SCOPE_KEY, CUSTOMERS_SIGN_UP_SCOPE_KEY, archonCredentialRenewDue, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, customersOrgIdOf, desiredBouleEnv, planBouleOwners, staleCustomersEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
7
7
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
8
8
|
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
9
9
|
import { planCompanyBackups } from './company-backups.js';
|
|
10
|
+
import { COOKIE_DOMAIN_WAITING_REASONS, LOGIN_COOKIE_DOMAIN_SCOPE_KEY, MIN_AUTH_FOR_CLIENT_ORGS, authAtLeast, cookieDomainAllowed, cookieDomainWaitingLine, } from './cookie-domain.js';
|
|
11
|
+
import { DESKTOP_AUTH_HOME, planDesktopAuth } from './desktop-auth.js';
|
|
12
|
+
import { planDesktopCallbacks } from './desktop-callback.js';
|
|
10
13
|
import { onPlatformNetwork } from './destinations.js';
|
|
11
14
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
12
15
|
import { planEdgeReservedLabels, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
13
16
|
import { planFilesKeys } from './entry-files-key.js';
|
|
14
17
|
import { hasWildcardGate, planGates } from './gates.js';
|
|
15
18
|
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
19
|
+
import { APP_GONE_KIND } from './live.js';
|
|
16
20
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
17
|
-
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
21
|
+
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, agoraIssuerHostFor, clientOrganizationsState, companyLoginTunnelHost, customersClientOrganizations, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
18
22
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
19
23
|
import { mailStoreSwitchChange } from './mail-store.js';
|
|
20
24
|
import { planMcp } from './mcp.js';
|
|
21
25
|
import { hasAdoptedResources, networkMoveNeeded, planNetworkMove } from './network-move.js';
|
|
26
|
+
import { planOrgPostgresMigration } from './org-postgres-migrate.js';
|
|
27
|
+
import { legacyUnmigrated, orgPostgresUndeliveredScopeKey, planOrgPostgresSteps, serviceOnServer, } from './org-postgres.js';
|
|
22
28
|
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, OWN_SIGNIN_DB_MOVE_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, planOwnSfu, planSigninDbMove, } from './own-diff.js';
|
|
23
29
|
import { ownDnsHostnames } from './own-dns.js';
|
|
24
30
|
import { ownHostnames } from './own-hostnames.js';
|
|
25
31
|
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, appSecretAvailability, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
26
32
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
27
33
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
34
|
+
import { companyAppPostLogoutUris, postLogoutUpdateOwed, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
28
35
|
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryRoutes, entryRoutesMarker, } from './proxy-labels.js';
|
|
29
36
|
import { planRemoteDatabase } from './remote-database.js';
|
|
30
37
|
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';
|
|
@@ -234,14 +241,24 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
234
241
|
// #267: after the ZITADEL org step, before any app's own issuer
|
|
235
242
|
// application. Apps created later in this run are not in the binding yet;
|
|
236
243
|
// the next run replans this step and repoints them.
|
|
237
|
-
const loginHost = planLoginHost(org, recordedIds,
|
|
244
|
+
const loginHost = planLoginHost(org, recordedIds, {
|
|
245
|
+
authReady: platformReadiness.authReady,
|
|
246
|
+
issuerPlanned: issuer.step !== null,
|
|
247
|
+
bindingsWired: options.loginHostBindings ?? false,
|
|
248
|
+
signInLoginReady: platformReadiness.signInLoginReady ?? false,
|
|
249
|
+
gates: {
|
|
250
|
+
authGitRef: platformReadiness.serviceGitRefs?.auth ?? '',
|
|
251
|
+
bindKeyReady: platformReadiness.signInBindKeyReady ?? false,
|
|
252
|
+
clientOrganizationsPort: options.loginHostClientOrganizations ?? false,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
238
255
|
if (loginHost.step)
|
|
239
256
|
plannedSteps.push(loginHost.step);
|
|
240
|
-
// #
|
|
241
|
-
//
|
|
242
|
-
// `OrgPostgresPlan`, never `plannedSteps` ({@link OrgPostgresPlan}'s own
|
|
243
|
-
// doc comment says why).
|
|
257
|
+
// #444: the org's one shared Postgres server and its databases. They need
|
|
258
|
+
// nothing else planned here; the report stays on `OrgPostgresPlan` too.
|
|
244
259
|
const orgPostgres = planOrgPostgres(org, recordedIds);
|
|
260
|
+
const orgPostgresSteps = planOrgPostgresSteps(org, orgPostgres);
|
|
261
|
+
const orgPostgresMigration = planOrgPostgresMigration(org, recordedIds, orgPostgres.legacyUnmigrated);
|
|
245
262
|
// Planned ahead of everything that needs the zone live (#238); its own step
|
|
246
263
|
// is pushed just below, before mail.
|
|
247
264
|
const zone = planZone(org, byKey, claimed);
|
|
@@ -254,7 +271,8 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
254
271
|
// #555: steps that write DNS records, which go through edge.
|
|
255
272
|
const dnsBlocked = [];
|
|
256
273
|
// #553: a ZITADEL app provision whose app id is recorded needs the typed confirmation.
|
|
257
|
-
const
|
|
274
|
+
const appBlocked = [];
|
|
275
|
+
const guardApp = (step) => guardAppProvision(step, org, recordedIds, byKey, claimed, appBlocked);
|
|
258
276
|
// #567: before the first step that reads the zone (mail, the tunnel, the
|
|
259
277
|
// rate limit), so a domain with no zone yet gets it provisioned first.
|
|
260
278
|
const zoneReason = zone.step ? edgeNeeded(platformReadiness, ['zones']) : null;
|
|
@@ -444,7 +462,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
444
462
|
// string into the app's environment once the database exists -- it can
|
|
445
463
|
// only do that if the database step already ran and recorded the
|
|
446
464
|
// database's resource id.
|
|
447
|
-
const database = planDatabase(org, byKey, claimed);
|
|
465
|
+
const database = planDatabase(org, byKey, claimed, recordedIds);
|
|
448
466
|
if (database)
|
|
449
467
|
plannedSteps.push(database);
|
|
450
468
|
const authIssuer = 'error' in agoraHost ? null : agoraHost.issuer;
|
|
@@ -548,6 +566,18 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
548
566
|
const boule = planBoule(org, byKey, claimed, recordedIds, authIssuer, options.otherSlugs ?? [], options.archonUrl ?? null, platformReadiness, bouleCredentials.wants);
|
|
549
567
|
if (boule.step)
|
|
550
568
|
plannedSteps.push(boule.step);
|
|
569
|
+
// Boule owners, after the Boule step: needs the app deployed and the seeding secret in its env.
|
|
570
|
+
const bouleOwners = planBouleOwners(org, recordedIds, (byKey.get(resourceKey('coolify', 'boule', org.id))?.config.env ?? null));
|
|
571
|
+
if (bouleOwners.seed.length > 0) {
|
|
572
|
+
plannedSteps.push({
|
|
573
|
+
action: { system: 'coolify', kind: 'boule-owners', key: org.id, desired: {} },
|
|
574
|
+
step: {
|
|
575
|
+
description: `seed ${org.slug}'s declared owners into its Boule`,
|
|
576
|
+
destroys: false,
|
|
577
|
+
migrates: false,
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
}
|
|
551
581
|
// After the apps and Boule -- an edge key needs its app recorded or planned,
|
|
552
582
|
// the same dependency `planEntryAiKeys`/`planFilesKeys` have.
|
|
553
583
|
const entryEdgeKeys = planEntryEdgeKeys(org, recordedIds, plannedSteps, platformReadiness, (options.now ?? Date.now)());
|
|
@@ -557,6 +587,17 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
557
587
|
const bouleBroker = planBouleBrokerEntry(org, recordedIds, plannedSteps, options.broker ?? false);
|
|
558
588
|
if (bouleBroker.step)
|
|
559
589
|
plannedSteps.push(bouleBroker.step);
|
|
590
|
+
// #440: Agora's desktop sign-in backend, after Agora and Boule: its readiness probe goes through Agora's proxy, and
|
|
591
|
+
// Agora writes the upstream only once the service ran healthy (so a first apply needs a second to turn it on).
|
|
592
|
+
const desktopAuth = planDesktopAuth(org, recordedIds, {
|
|
593
|
+
host: agora.host ?? null,
|
|
594
|
+
liveAgoraEnv: agora.liveEnv ?? null,
|
|
595
|
+
homePlanned: orgPostgresSteps.some((s) => s.action.kind === 'org-postgres-databases' &&
|
|
596
|
+
(s.action.desired.homes ?? []).includes(DESKTOP_AUTH_HOME)),
|
|
597
|
+
});
|
|
598
|
+
claimed.add(resourceKey('coolify', 'desktop-auth', org.id));
|
|
599
|
+
if (desktopAuth.step)
|
|
600
|
+
plannedSteps.push(desktopAuth.step);
|
|
560
601
|
// An own org's scheduler, before its registration below.
|
|
561
602
|
const ownJobs = planOwnJobs({
|
|
562
603
|
org,
|
|
@@ -661,6 +702,15 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
661
702
|
const idTokenUserinfo = planIdTokenUserinfo(org, live, recordedIds, plannedSteps, platformReadiness.authReady, options.zitadelIdTokenUserinfo ?? false);
|
|
662
703
|
if (idTokenUserinfo)
|
|
663
704
|
plannedSteps.push(idTokenUserinfo);
|
|
705
|
+
const postLogoutUris = planPostLogoutUris(org, live, recordedIds, plannedSteps, platformReadiness.authReady, options.zitadelPostLogoutUris ?? false);
|
|
706
|
+
if (postLogoutUris)
|
|
707
|
+
plannedSteps.push(postLogoutUris);
|
|
708
|
+
const desktopCallbacks = planDesktopCallbacks(org, live, recordedIds, plannedSteps, platformReadiness.authReady, options.zitadelRedirectUris ?? false);
|
|
709
|
+
if (desktopCallbacks)
|
|
710
|
+
plannedSteps.push(desktopCallbacks);
|
|
711
|
+
// #444, last: a Postgres step that fails must not stop the zone, mail, app, Agora and Boule steps of a live company.
|
|
712
|
+
plannedSteps.push(...orgPostgresSteps);
|
|
713
|
+
plannedSteps.push(...orgPostgresMigration.steps);
|
|
664
714
|
// #10 (resolved): "foundry manages only what the declared org names";
|
|
665
715
|
// anything left unclaimed is listed, never proposed for deletion.
|
|
666
716
|
// A resource foundry made for the org is not a finding: only a resource
|
|
@@ -682,6 +732,16 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
682
732
|
.map((r) => ({
|
|
683
733
|
description: `${r.system} ${r.kind} "${liveResourceName(r)}" is live but not named by the declared org`,
|
|
684
734
|
}));
|
|
735
|
+
// A password the job set but nothing delivered: the step is still planned, it sets and delivers a new one.
|
|
736
|
+
for (const home of orgPostgres.missingSchemas) {
|
|
737
|
+
if (recordedIds[orgPostgresUndeliveredScopeKey(home.schema)]) {
|
|
738
|
+
unmanaged.push({
|
|
739
|
+
description: `${org.slug}'s ${home.schema} database password was set but not delivered; the next apply sets and delivers a new one`,
|
|
740
|
+
key: `org-postgres-undelivered:${org.slug}:${home.schema}`,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
unmanaged.push(...orgPostgresMigration.unmanaged);
|
|
685
745
|
// A blocked Agora is reported here, not planned: a step the executor must
|
|
686
746
|
// refuse would halt every step after it. This is the plan's one non-step
|
|
687
747
|
// item list, so it rides along with the unmanaged items.
|
|
@@ -689,8 +749,14 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
689
749
|
unmanaged.push({ description: agora.blocked, key: 'coolify:agora' });
|
|
690
750
|
if (agora.issuerNote)
|
|
691
751
|
unmanaged.push({ description: agora.issuerNote, key: 'coolify:agora' });
|
|
752
|
+
if (desktopAuth.blocked) {
|
|
753
|
+
unmanaged.push({ description: desktopAuth.blocked, key: 'coolify:desktop-auth' });
|
|
754
|
+
}
|
|
692
755
|
if (boule.blocked)
|
|
693
756
|
unmanaged.push({ description: boule.blocked, key: 'coolify:boule' });
|
|
757
|
+
for (const line of bouleOwners.blocked) {
|
|
758
|
+
unmanaged.push({ description: line, key: 'coolify:boule-owners' });
|
|
759
|
+
}
|
|
694
760
|
for (const description of bouleCredentials.blocked) {
|
|
695
761
|
unmanaged.push({ description, key: 'coolify:boule:credentials' });
|
|
696
762
|
}
|
|
@@ -762,6 +828,8 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
762
828
|
if (loginHost.blocked) {
|
|
763
829
|
unmanaged.push({ description: loginHost.blocked, key: 'zitadel:login-host' });
|
|
764
830
|
}
|
|
831
|
+
for (const note of loginHost.notes)
|
|
832
|
+
unmanaged.push({ description: note, key: 'zitadel:login-host' });
|
|
765
833
|
if (mail.blocked)
|
|
766
834
|
unmanaged.push({ description: mail.blocked, key: 'coolify:mail' });
|
|
767
835
|
// #162: reported separately from `mail.blocked` -- missing an owner
|
|
@@ -793,6 +861,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
793
861
|
unmanaged.push({ description: zone.blocked, key: 'cloudflare:zone' });
|
|
794
862
|
unmanaged.push(...zoneBlocked);
|
|
795
863
|
unmanaged.push(...dnsBlocked);
|
|
864
|
+
unmanaged.push(...appBlocked);
|
|
796
865
|
// A legacy email-list Access app is no longer planned or removed: an operator deletes it by hand.
|
|
797
866
|
const legacyAccessKey = resourceKey('cloudflare', 'access', appStepKey(org, APP_ENTRY_KEY));
|
|
798
867
|
if (recordedIds[legacyAccessKey]) {
|
|
@@ -848,6 +917,7 @@ const NEEDS_OWN_ZITADEL = new Set([
|
|
|
848
917
|
'zitadel:app',
|
|
849
918
|
'zitadel:customers',
|
|
850
919
|
'zitadel:id-token-userinfo',
|
|
920
|
+
'zitadel:desktop-callback',
|
|
851
921
|
'zitadel:login-host',
|
|
852
922
|
'coolify:chat',
|
|
853
923
|
'coolify:calls-service',
|
|
@@ -857,17 +927,19 @@ const NEEDS_OWN_ZITADEL = new Set([
|
|
|
857
927
|
'files:organisation',
|
|
858
928
|
]);
|
|
859
929
|
/**
|
|
860
|
-
* #553: a plain ZITADEL app provision (not a `recreate`) is planned only
|
|
861
|
-
* the live listing lacks the app by name. If an app id is recorded for
|
|
862
|
-
* the listing may simply have missed a live app
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
930
|
+
* #553, #557: a plain ZITADEL app provision (not a `recreate`) is planned only
|
|
931
|
+
* when the live listing lacks the app by name. If an app id is recorded for
|
|
932
|
+
* it, the listing may simply have missed a live app. The replacement is
|
|
933
|
+
* offered only when the reader proved the app gone with a direct read by app
|
|
934
|
+
* id (a live `zitadel:app-gone:<name>` resource, see
|
|
935
|
+
* `ProvidersZitadelReader`); without that proof the step is blocked and
|
|
936
|
+
* nothing is planned. With the proof, the way out is the destroying
|
|
937
|
+
* `recreate` path: the step is planned as a replacement that asks for the
|
|
938
|
+
* typed confirmation, then apply deletes the recorded id (a 404 is fine) and
|
|
939
|
+
* creates a new app. (A failed recreate clears the recorded id, so it is not
|
|
940
|
+
* caught here.)
|
|
869
941
|
*/
|
|
870
|
-
function guardAppProvision(step, org, recordedIds) {
|
|
942
|
+
function guardAppProvision(step, org, recordedIds, byKey, claimed, blocked) {
|
|
871
943
|
if (!step || step.action.system !== 'zitadel' || step.action.kind !== 'app')
|
|
872
944
|
return step;
|
|
873
945
|
if (step.action.desired.recreate === true)
|
|
@@ -875,10 +947,19 @@ function guardAppProvision(step, org, recordedIds) {
|
|
|
875
947
|
const recorded = recordedIds[`zitadel:app:${step.action.key}`];
|
|
876
948
|
if (!recorded)
|
|
877
949
|
return step;
|
|
950
|
+
const goneKey = resourceKey('zitadel', APP_GONE_KIND, step.action.key);
|
|
951
|
+
if (!byKey.has(goneKey)) {
|
|
952
|
+
blocked.push({
|
|
953
|
+
description: `${org.slug}'s ZITADEL app '${step.action.key}' is blocked, not planned: recorded app ${recorded} is not in ZITADEL's listing, and a direct read of it did not answer not-found, so it may be live (a listing can miss a live app)`,
|
|
954
|
+
key: `zitadel:app:${step.action.key}`,
|
|
955
|
+
});
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
claimed.add(goneKey);
|
|
878
959
|
return {
|
|
879
960
|
action: { ...step.action, desired: { ...step.action.desired, recreate: true } },
|
|
880
961
|
step: {
|
|
881
|
-
description: `replace ZITADEL app '${step.action.key}' for ${org.slug}: recorded app ${recorded} is not in ZITADEL's listing
|
|
962
|
+
description: `replace ZITADEL app '${step.action.key}' for ${org.slug}: recorded app ${recorded} is not in ZITADEL's listing and a direct read answered not-found`,
|
|
882
963
|
destroys: true,
|
|
883
964
|
migrates: false,
|
|
884
965
|
},
|
|
@@ -1113,6 +1194,14 @@ function planOwners(org, recordedIds, authReady, authIssuer, ownerInviteMail, re
|
|
|
1113
1194
|
});
|
|
1114
1195
|
return;
|
|
1115
1196
|
}
|
|
1197
|
+
const nameProblem = ownerNameProblem(mailboxAddress, owner.givenName, owner.familyName);
|
|
1198
|
+
if (nameProblem) {
|
|
1199
|
+
blocked.push({
|
|
1200
|
+
description: `${org.slug}'s owner ${position} is blocked, not planned: ${nameProblem}`,
|
|
1201
|
+
key: `zitadel:owner:${position}`,
|
|
1202
|
+
});
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1116
1205
|
if (!ownerInviteMail) {
|
|
1117
1206
|
blocked.push({
|
|
1118
1207
|
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`,
|
|
@@ -1149,48 +1238,55 @@ function planOwners(org, recordedIds, authReady, authIssuer, ownerInviteMail, re
|
|
|
1149
1238
|
export function isPlatformOwnOrg(org, platformOwnerOrgId) {
|
|
1150
1239
|
return Boolean(platformOwnerOrgId && org.id === platformOwnerOrgId);
|
|
1151
1240
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1241
|
+
function planLoginHost(org, recordedIds, options) {
|
|
1242
|
+
const gates = options.gates ?? {
|
|
1243
|
+
authGitRef: '',
|
|
1244
|
+
bindKeyReady: false,
|
|
1245
|
+
clientOrganizationsPort: false,
|
|
1246
|
+
};
|
|
1247
|
+
const core = planLoginHostCore(org, recordedIds, {
|
|
1248
|
+
authReady: options.authReady,
|
|
1249
|
+
issuerPlanned: options.issuerPlanned,
|
|
1250
|
+
bindingsWired: options.bindingsWired,
|
|
1251
|
+
signInLoginReady: options.signInLoginReady ?? false,
|
|
1252
|
+
gates,
|
|
1253
|
+
});
|
|
1254
|
+
return { ...core, notes: loginHostNotes(org, recordedIds, gates) };
|
|
1255
|
+
}
|
|
1256
|
+
/** Fixed lines for what the login host cannot yet do: the organisation check (577) and the cookie domain (795). */
|
|
1257
|
+
function loginHostNotes(org, recordedIds, gates) {
|
|
1258
|
+
const notes = [];
|
|
1259
|
+
const mapped = Object.keys(customersClientOrganizations(org, recordedIds)).length > 0;
|
|
1260
|
+
if (mapped && !authAtLeast(gates.authGitRef, MIN_AUTH_FOR_CLIENT_ORGS)) {
|
|
1261
|
+
notes.push(`${org.slug}: the shared login host does not yet check customers clients' organisation: auth <${MIN_AUTH_FOR_CLIENT_ORGS}`);
|
|
1262
|
+
}
|
|
1263
|
+
else if (mapped && !ownsServices(org) && !gates.clientOrganizationsPort) {
|
|
1264
|
+
notes.push(`${org.slug}: the shared login host does not yet check customers clients' organisation: the host cannot write the map`);
|
|
1265
|
+
}
|
|
1266
|
+
const cookie = cookieDomainAllowed({
|
|
1267
|
+
org,
|
|
1268
|
+
recorded: recordedIds,
|
|
1269
|
+
authGitRef: gates.authGitRef,
|
|
1270
|
+
bindKeyReady: gates.bindKeyReady,
|
|
1271
|
+
});
|
|
1272
|
+
if (cookie.domain === null && COOKIE_DOMAIN_WAITING_REASONS.has(cookie.reason)) {
|
|
1273
|
+
notes.push(cookieDomainWaitingLine(org.slug, cookie.reason));
|
|
1274
|
+
}
|
|
1275
|
+
return notes;
|
|
1276
|
+
}
|
|
1277
|
+
/** True when the recorded organisation map differs from the wanted one and a port can write it. */
|
|
1278
|
+
function clientOrganizationsOwed(org, recordedIds, writable) {
|
|
1279
|
+
return (writable &&
|
|
1280
|
+
clientOrganizationsState(customersClientOrganizations(org, recordedIds)) !==
|
|
1281
|
+
(recordedIds[LOGIN_CLIENT_ORGS_SCOPE_KEY] ?? ''));
|
|
1282
|
+
}
|
|
1283
|
+
function planLoginHostCore(org, recordedIds, options) {
|
|
1284
|
+
const { authReady, issuerPlanned, bindingsWired, signInLoginReady, gates } = options;
|
|
1189
1285
|
const none = { step: null, blocked: null };
|
|
1190
1286
|
// An own org's instance has no shared login host to bind: its issuer is the sign-in service's own.
|
|
1191
1287
|
// Only in-app sign-in plans a step for it: the app hosts, through its own in-app login app.
|
|
1192
1288
|
if (ownsServices(org))
|
|
1193
|
-
return planOwnLoginHost(org, recordedIds);
|
|
1289
|
+
return planOwnLoginHost(org, recordedIds, gates);
|
|
1194
1290
|
const key = resourceKey('zitadel', 'login-host', org.id);
|
|
1195
1291
|
const host = loginHostnameFor(org);
|
|
1196
1292
|
// In-app sign-in: each app host gets its own binding, trusted domain and base URI. Until the second
|
|
@@ -1200,6 +1296,23 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1200
1296
|
const inAppWaiting = wantedApps.length > 0 && !signInLoginReady
|
|
1201
1297
|
? `${org.slug}'s in-app sign-in waits for the login app that serves ${SIGN_IN_BASE_PATH} (${SIGN_IN_LOGIN_INTERNAL_NAME}), which the platform has not applied`
|
|
1202
1298
|
: null;
|
|
1299
|
+
const targets = inAppTargets(apps, recordedIds);
|
|
1300
|
+
const authReleased = authAtLeast(gates.authGitRef, MIN_AUTH_FOR_CLIENT_ORGS);
|
|
1301
|
+
const cookie = cookieDomainAllowed({
|
|
1302
|
+
org,
|
|
1303
|
+
recorded: recordedIds,
|
|
1304
|
+
authGitRef: gates.authGitRef,
|
|
1305
|
+
bindKeyReady: gates.bindKeyReady,
|
|
1306
|
+
});
|
|
1307
|
+
const cookieDomain = targets.active !== '' && cookie.domain !== null ? cookie.domain : '';
|
|
1308
|
+
const settled = recordedIds[key] === host &&
|
|
1309
|
+
(recordedIds[LOGIN_COOKIE_DOMAIN_SCOPE_KEY] ?? '') === cookieDomain &&
|
|
1310
|
+
!clientOrganizationsOwed(org, recordedIds, gates.clientOrganizationsPort) &&
|
|
1311
|
+
recordedIds[LOGIN_BINDING_SCOPE_KEY] ===
|
|
1312
|
+
loginBindingState(host, loginHostClientIds(org, recordedIds, inAppStatePairs(targets.active).map((pair) => pair.clientId))) &&
|
|
1313
|
+
(recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1314
|
+
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active;
|
|
1315
|
+
const owed = pendingRemovalHosts(recordedIds);
|
|
1203
1316
|
function stepFor() {
|
|
1204
1317
|
return {
|
|
1205
1318
|
step: {
|
|
@@ -1207,14 +1320,20 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1207
1320
|
system: 'zitadel',
|
|
1208
1321
|
kind: 'login-host',
|
|
1209
1322
|
key: org.id,
|
|
1210
|
-
desired:
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1323
|
+
desired: {
|
|
1324
|
+
...(apps.length > 0 ||
|
|
1325
|
+
recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ||
|
|
1326
|
+
recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY]
|
|
1327
|
+
? { host, apps }
|
|
1328
|
+
: { host }),
|
|
1329
|
+
...(authReleased ? { authReleased: true } : {}),
|
|
1330
|
+
...(cookieDomain ? { cookieDomain } : {}),
|
|
1331
|
+
},
|
|
1215
1332
|
},
|
|
1216
1333
|
step: {
|
|
1217
|
-
description:
|
|
1334
|
+
description: settled && owed.length > 0
|
|
1335
|
+
? `retire ${org.slug}'s old login host ${owed.join(', ')} and record ${host}`
|
|
1336
|
+
: `set ${org.slug}'s login host to ${host}`,
|
|
1218
1337
|
destroys: false,
|
|
1219
1338
|
migrates: false,
|
|
1220
1339
|
},
|
|
@@ -1228,13 +1347,7 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1228
1347
|
// `zitadelLoginHost` always advances the recorded host to the current one,
|
|
1229
1348
|
// so a pending removal is the only way to know one is still owed, and
|
|
1230
1349
|
// nothing else here would replan the step to retry it.
|
|
1231
|
-
|
|
1232
|
-
if (recordedIds[key] === host &&
|
|
1233
|
-
recordedIds[LOGIN_BINDING_SCOPE_KEY] ===
|
|
1234
|
-
loginBindingState(host, loginHostClientIds(org, recordedIds, inAppStatePairs(targets.active).map((pair) => pair.clientId))) &&
|
|
1235
|
-
(recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1236
|
-
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
|
|
1237
|
-
pendingRemovalHosts(recordedIds).length === 0) {
|
|
1350
|
+
if (settled && owed.length === 0) {
|
|
1238
1351
|
return { step: null, blocked: inAppWaiting };
|
|
1239
1352
|
}
|
|
1240
1353
|
// #177: nothing to add a trusted domain to yet -- `planIssuer` already
|
|
@@ -1254,10 +1367,11 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1254
1367
|
* applied and that login app is recorded; until then one blocked line says what it waits for.
|
|
1255
1368
|
* Archon's host is not here: the platform's Archon step owns it.
|
|
1256
1369
|
*/
|
|
1257
|
-
function planOwnLoginHost(org, recordedIds) {
|
|
1370
|
+
function planOwnLoginHost(org, recordedIds, gates) {
|
|
1258
1371
|
const none = { step: null, blocked: null };
|
|
1372
|
+
const mapOwed = clientOrganizationsOwed(org, recordedIds, true);
|
|
1259
1373
|
// Nothing wanted and nothing owed.
|
|
1260
|
-
if (!inAppSignIn(org) && !recordedIds[SIGN_IN_IN_APP_SCOPE_KEY])
|
|
1374
|
+
if (!inAppSignIn(org) && !recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] && !mapOwed)
|
|
1261
1375
|
return none;
|
|
1262
1376
|
// The company's own sign-in is reported on its own line (`coolify:own-auth`).
|
|
1263
1377
|
if (!ownAuthApplied(org, recordedIds))
|
|
@@ -1273,19 +1387,31 @@ function planOwnLoginHost(org, recordedIds) {
|
|
|
1273
1387
|
const targets = inAppTargets(apps, recordedIds);
|
|
1274
1388
|
if ((recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1275
1389
|
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
|
|
1390
|
+
!mapOwed &&
|
|
1276
1391
|
pendingRemovalHosts(recordedIds).length === 0) {
|
|
1277
1392
|
return { step: null, blocked: waiting };
|
|
1278
1393
|
}
|
|
1279
1394
|
// Nothing to trust a domain in, or to bind in, before the org exists in the company's ZITADEL.
|
|
1280
|
-
if (!recordedIds.zitadel_org)
|
|
1281
|
-
return {
|
|
1395
|
+
if (!recordedIds.zitadel_org) {
|
|
1396
|
+
return {
|
|
1397
|
+
step: null,
|
|
1398
|
+
blocked: waiting ??
|
|
1399
|
+
`${org.slug}'s in-app sign-in waits for the company's ZITADEL organisation, which is not recorded yet`,
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1282
1402
|
return {
|
|
1283
1403
|
step: {
|
|
1284
1404
|
action: {
|
|
1285
1405
|
system: 'zitadel',
|
|
1286
1406
|
kind: 'login-host',
|
|
1287
1407
|
key: org.id,
|
|
1288
|
-
desired: {
|
|
1408
|
+
desired: {
|
|
1409
|
+
host,
|
|
1410
|
+
apps,
|
|
1411
|
+
...(authAtLeast(gates.authGitRef, MIN_AUTH_FOR_CLIENT_ORGS)
|
|
1412
|
+
? { authReleased: true }
|
|
1413
|
+
: {}),
|
|
1414
|
+
},
|
|
1289
1415
|
},
|
|
1290
1416
|
step: {
|
|
1291
1417
|
description: `set ${org.slug}'s app hosts to sign in on their own addresses`,
|
|
@@ -1335,6 +1461,46 @@ function planIdTokenUserinfo(org, live, recordedIds, plannedSteps, authReady, wi
|
|
|
1335
1461
|
},
|
|
1336
1462
|
};
|
|
1337
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Every live `@wtfalch/auth` web app (not Agora, which signs out locally) holds
|
|
1466
|
+
* its root and `/auth/signed-out` as post-logout addresses. One
|
|
1467
|
+
* `zitadel:post-logout-uris` step names the apps whose marker
|
|
1468
|
+
* ({@link zitadelAppPostLogoutAppliedScopeKey}: id and set) differs from what
|
|
1469
|
+
* the declared org wants, so a client made with only the root, or a host
|
|
1470
|
+
* change, is updated once and the next plan is clean. Plans nothing when the
|
|
1471
|
+
* host's ZITADEL client lacks the method, before the org and project are
|
|
1472
|
+
* recorded, or while the platform's sign-in is not ready.
|
|
1473
|
+
*/
|
|
1474
|
+
function planPostLogoutUris(org, live, recordedIds, plannedSteps, authReady, wired) {
|
|
1475
|
+
if (!wired || !authReady || !recordedIds.zitadel_org || !recordedIds.zitadel_project)
|
|
1476
|
+
return null;
|
|
1477
|
+
const apps = live
|
|
1478
|
+
.filter((r) => r.system === 'zitadel' && r.kind === 'app' && r.config.appType === 'web')
|
|
1479
|
+
.map((r) => r.key)
|
|
1480
|
+
.filter((appKey) => appKey !== 'agora')
|
|
1481
|
+
// An app this plan recreates gets its marker when it is made.
|
|
1482
|
+
.filter((appKey) => !plannedSteps.some((s) => s.action.system === 'zitadel' &&
|
|
1483
|
+
s.action.kind === 'app' &&
|
|
1484
|
+
s.action.key === appKey &&
|
|
1485
|
+
s.action.desired.recreate === true))
|
|
1486
|
+
.filter((appKey) => {
|
|
1487
|
+
const appId = recordedIds[`zitadel:app:${appKey}`];
|
|
1488
|
+
const uris = companyAppPostLogoutUris(org, appKey);
|
|
1489
|
+
return (Boolean(appId) &&
|
|
1490
|
+
uris.length > 0 &&
|
|
1491
|
+
postLogoutUpdateOwed(recordedIds[zitadelAppPostLogoutAppliedScopeKey(appKey)], appId, uris));
|
|
1492
|
+
});
|
|
1493
|
+
if (apps.length === 0)
|
|
1494
|
+
return null;
|
|
1495
|
+
return {
|
|
1496
|
+
action: { system: 'zitadel', kind: 'post-logout-uris', key: org.id, desired: { apps } },
|
|
1497
|
+
step: {
|
|
1498
|
+
description: `update the post-logout addresses of ${org.slug}'s sign-in apps (${apps.join(', ')})`,
|
|
1499
|
+
destroys: false,
|
|
1500
|
+
migrates: false,
|
|
1501
|
+
},
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1338
1504
|
/** Why a login-host step cannot run yet, or `null` when nothing is missing. */
|
|
1339
1505
|
function loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) {
|
|
1340
1506
|
if (!bindingsWired) {
|
|
@@ -1374,6 +1540,7 @@ function orgServiceSchemaEnabled(org, schema) {
|
|
|
1374
1540
|
return org.calls.enabled;
|
|
1375
1541
|
if (schema.schema === 'app')
|
|
1376
1542
|
return org.hosting.entries.some(entryHasDatabase);
|
|
1543
|
+
// Agora always plans (planAgora): desktop-auth shares its gate, so it is on like Boule.
|
|
1377
1544
|
return true;
|
|
1378
1545
|
}
|
|
1379
1546
|
/**
|
|
@@ -1395,6 +1562,7 @@ function planOrgPostgres(org, recordedIds) {
|
|
|
1395
1562
|
return {
|
|
1396
1563
|
serverMissing: !recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY],
|
|
1397
1564
|
missingSchemas: ORG_SERVICE_SCHEMAS.filter((schema) => orgServiceSchemaEnabled(org, schema) && !recordedIds[schema.scopeKey]),
|
|
1565
|
+
legacyUnmigrated: ORG_SERVICE_SCHEMAS.filter((schema) => orgServiceSchemaEnabled(org, schema) && legacyUnmigrated(org, schema, recordedIds)),
|
|
1398
1566
|
};
|
|
1399
1567
|
}
|
|
1400
1568
|
/** #230: restated from `executor.ts`'s `MAIL_ROLE_MAILBOXES_SCOPE_KEY`, same rule: comma-joined local parts already created. */
|
|
@@ -2566,6 +2734,7 @@ function planAgora(org, byKey, claimed, recordedIds, reading, issuerSwitch) {
|
|
|
2566
2734
|
},
|
|
2567
2735
|
blocked: null,
|
|
2568
2736
|
issuerNote,
|
|
2737
|
+
host,
|
|
2569
2738
|
};
|
|
2570
2739
|
}
|
|
2571
2740
|
const liveEnv = (live.config.env ?? {});
|
|
@@ -2594,7 +2763,7 @@ function planAgora(org, byKey, claimed, recordedIds, reading, issuerSwitch) {
|
|
|
2594
2763
|
// planned no matter what Coolify's env store already shows.
|
|
2595
2764
|
const activated = recordedIds[AGORA_ACTIVATED_ENV_SCOPE_KEY] === agoraEnvRevision(env);
|
|
2596
2765
|
if (drift.length === 0 && activated && !portDrift) {
|
|
2597
|
-
return { step: null, blocked: null, issuerNote };
|
|
2766
|
+
return { step: null, blocked: null, issuerNote, host, liveEnv };
|
|
2598
2767
|
}
|
|
2599
2768
|
const reasons = [
|
|
2600
2769
|
...(drift.length > 0 ? [`${drift.join(', ')} missing or different`] : []),
|
|
@@ -2613,6 +2782,8 @@ function planAgora(org, byKey, claimed, recordedIds, reading, issuerSwitch) {
|
|
|
2613
2782
|
},
|
|
2614
2783
|
blocked: null,
|
|
2615
2784
|
issuerNote,
|
|
2785
|
+
host,
|
|
2786
|
+
liveEnv,
|
|
2616
2787
|
};
|
|
2617
2788
|
}
|
|
2618
2789
|
/** The issuer a live Agora's `AGORA_CONFIG` names, or null when it has none or is not JSON. */
|
|
@@ -2829,6 +3000,8 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
2829
3000
|
drift.push(BOULE_PEER_PASSWORD_ENV);
|
|
2830
3001
|
if (platformOwner && !liveEnv[BOULE_BRIDGE_SECRET_ENV])
|
|
2831
3002
|
drift.push(BOULE_BRIDGE_SECRET_ENV);
|
|
3003
|
+
if (!liveEnv[BOULE_OWNER_SEED_SECRET_ENV])
|
|
3004
|
+
drift.push(BOULE_OWNER_SEED_SECRET_ENV);
|
|
2832
3005
|
// Secrets written but Boule not restarted since: plan the step so it restarts, minting nothing.
|
|
2833
3006
|
if (platformOwner && !recordedIds[BOULE_SECRETS_RESTARTED_SCOPE_KEY]) {
|
|
2834
3007
|
drift.push('restart after the peer password and bridge secret');
|
|
@@ -3145,8 +3318,10 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
3145
3318
|
// names (`envNames` absent) reads as compliant, like `hasLogVolume`.
|
|
3146
3319
|
const envNames = live.config.envNames;
|
|
3147
3320
|
const dbLive = byKey.get(resourceKey('coolify', 'database', org.id));
|
|
3148
|
-
const
|
|
3149
|
-
|
|
3321
|
+
const appHome = ORG_SERVICE_SCHEMAS.find((home) => home.schema === 'app');
|
|
3322
|
+
const dbOnServer = appHome !== undefined && serviceOnServer(org, appHome, recordedIds);
|
|
3323
|
+
const dbReady = entry.database && (dbOnServer || Boolean(recordedIds[`coolify:database:${org.id}`]));
|
|
3324
|
+
if (!dbOnServer && dbReady && dbLive?.config.hasInternalUrl === false) {
|
|
3150
3325
|
// Rule 2: the push cannot succeed yet, so report it rather than replan
|
|
3151
3326
|
// forever.
|
|
3152
3327
|
blocked.push({
|
|
@@ -3157,7 +3332,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
3157
3332
|
else if (dbReady &&
|
|
3158
3333
|
// Only a read that saw the URL replans: an unread database (a failed
|
|
3159
3334
|
// read) would plan a step that pushes nothing, forever.
|
|
3160
|
-
dbLive?.config.hasInternalUrl === true &&
|
|
3335
|
+
(dbOnServer || dbLive?.config.hasInternalUrl === true) &&
|
|
3161
3336
|
((Array.isArray(envNames) && !envNames.includes('DATABASE_URL_OWNER')) ||
|
|
3162
3337
|
// A crash between the push and the restart leaves the env right and no
|
|
3163
3338
|
// marker: replan once more to restart and set it.
|
|
@@ -3450,7 +3625,19 @@ function planJobsRegistration(org, recordedIds, jobsAvailable, missingKeyRef = n
|
|
|
3450
3625
|
* `hosting.enabled` before this can be true. Same park-not-delete shape as
|
|
3451
3626
|
* {@link planApp}.
|
|
3452
3627
|
*/
|
|
3453
|
-
function planDatabase(org, byKey, claimed) {
|
|
3628
|
+
function planDatabase(org, byKey, claimed, recordedIds) {
|
|
3629
|
+
// #444: only a company whose app database is still its own Coolify database
|
|
3630
|
+
// plans one. A new company, and one that has moved, has it on its Postgres
|
|
3631
|
+
// server: recorded, or provisioned by a step earlier in this plan.
|
|
3632
|
+
// A Worker app's database is reached through the Coolify database's id (its Hyperdrive route
|
|
3633
|
+
// and tunnel), so a Worker app entry with a database always plans that database.
|
|
3634
|
+
const workerAppDatabase = org.hosting.entries.some((entry) => entry.workerApp?.database);
|
|
3635
|
+
const appHome = ORG_SERVICE_SCHEMAS.find((home) => home.schema === 'app');
|
|
3636
|
+
const legacy = appHome?.legacyKey(org);
|
|
3637
|
+
if (!workerAppDatabase &&
|
|
3638
|
+
(!appHome || !legacy || !recordedIds[legacy] || serviceOnServer(org, appHome, recordedIds))) {
|
|
3639
|
+
return null;
|
|
3640
|
+
}
|
|
3454
3641
|
return planHostingToggle('coolify', 'database', org.hosting.entries.some(entryHasDatabase), org, byKey, claimed, () => `provision a database for ${org.slug}'s app`, () => `park ${org.slug}'s database (data is retained, not deleted -- #7 corrections 1)`);
|
|
3455
3642
|
}
|
|
3456
3643
|
function sortedNames(names) {
|
|
@@ -3874,7 +4061,7 @@ function planBouleBrokerEntry(org, recordedIds, plannedSteps, brokerWired) {
|
|
|
3874
4061
|
}
|
|
3875
4062
|
/**
|
|
3876
4063
|
* Plan gap A9: an org's customers sign in through a second ZITADEL org with
|
|
3877
|
-
* self-registration
|
|
4064
|
+
* self-registration open only when `customers.signUp` is declared (off by default), granted the org's app project. Planned once an entry
|
|
3878
4065
|
* signs in its customers (`uses.auth.users: 'customers'`).
|
|
3879
4066
|
* Idempotent on the recorded customers org id, like `planOwners`. Once
|
|
3880
4067
|
* recorded, the live project grant to that org is claimed so it is not
|
|
@@ -4038,7 +4225,8 @@ function accessRuleDescription(org, rule) {
|
|
|
4038
4225
|
* time this step's own action runs.
|
|
4039
4226
|
*/
|
|
4040
4227
|
function planRateLimit(org, recordedIds) {
|
|
4041
|
-
|
|
4228
|
+
// In-app sign-in serves `/sign-in/` on the company's zone whatever else is hosted there, so it is limited too.
|
|
4229
|
+
if (!(org.hosting.usesDomain || org.site.enabled || inAppSignIn(org)) || !org.domain)
|
|
4042
4230
|
return null;
|
|
4043
4231
|
// Recorded, and the rule is the variant the switch asks for (the in-app one also covers POSTs under `/sign-in/`).
|
|
4044
4232
|
if (recordedIds[resourceKey('cloudflare', 'ratelimit', org.id)] &&
|