@wtfalch/foundry 0.60.0 → 0.62.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/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +7 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/native-build.d.ts +70 -0
- package/dist/native-build.js +92 -0
- package/dist/org.d.ts +184 -24
- package/dist/org.js +444 -49
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +60 -8
- package/dist/plan/blue-green.js +115 -10
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +15 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +552 -104
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-access.js +1 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +231 -19
- package/dist/plan/executor.js +1648 -129
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +7 -4
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +15 -2
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +13 -2
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +12 -3
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +20 -7
- package/dist/store/postgres.js +25 -2
- package/dist/store/schema.d.ts +73 -1
- package/dist/store/schema.js +10 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +19 -2
package/dist/plan/diff.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { VERSIONS
|
|
2
|
-
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, heldEnvRef, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownerNameProblem, ownsServices, } from '../org.js';
|
|
1
|
+
import { VERSIONS } from '../bootstrap/versions.js';
|
|
2
|
+
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, appsOn, companyGithubFor, heldEnvRef, kosmosOn, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownerNameProblem, ownsServices, } from '../org.js';
|
|
3
3
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
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
|
-
import {
|
|
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, entryClientIdScopeKey, withRecordedMessagingProject, } from './agora.js';
|
|
5
|
+
import { planAppsDbRoute } from './apps-db-route.js';
|
|
6
|
+
import { planAppsZones } from './apps-zones.js';
|
|
7
|
+
import { bgKey, planBlueGreen, } from './blue-green.js';
|
|
6
8
|
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_ROOM_SCOPE_KEY, CUSTOMERS_SIGN_IN_SCOPE_KEY, CUSTOMERS_SIGN_UP_SCOPE_KEY, archonCredentialRenewDue, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, customersOrgIdOf, customersRoomValue, desiredBouleEnv, planBouleOwners, staleCustomersEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
7
|
-
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
9
|
+
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, entryMcpProjectScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
10
|
+
import { bouleRegistrarDrift, planBrokerRegistrar } from './broker-registrar.js';
|
|
8
11
|
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
9
12
|
import { planCompanyBackups } from './company-backups.js';
|
|
10
13
|
import { COOKIE_DOMAIN_WAITING_REASONS, LOGIN_COOKIE_DOMAIN_SCOPE_KEY, MIN_AUTH_FOR_CLIENT_ORGS, authAtLeast, cookieDomainAllowed, cookieDomainWaitingLine, } from './cookie-domain.js';
|
|
@@ -12,34 +15,49 @@ import { DESKTOP_AUTH_HOME, planDesktopAuth } from './desktop-auth.js';
|
|
|
12
15
|
import { planDesktopCallbacks } from './desktop-callback.js';
|
|
13
16
|
import { onPlatformNetwork } from './destinations.js';
|
|
14
17
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
15
|
-
import {
|
|
18
|
+
import { planEntryAudit } from './entry-audit.js';
|
|
19
|
+
import { CUSTOMER_GRANTS_ENV, CUSTOMER_GRANTS_WRITTEN_SCOPE_KEY, customerGrantsEntries, customerGrantsShapeMatches, customerGrantsToDrop, wantedCustomerGrants, } from './entry-customer-grants.js';
|
|
20
|
+
import { planEdgeReservedLabels, planEdgeSlug, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
16
21
|
import { planFilesKeys } from './entry-files-key.js';
|
|
17
|
-
import {
|
|
22
|
+
import { planEntryJobs } from './entry-jobs.js';
|
|
23
|
+
import { planEntryRepoKeys } from './entry-repo-key.js';
|
|
24
|
+
import { BROKER_WORKSPACES_DATABASE_ENV, workspacesDatabaseRef } from './entry-workspaces.js';
|
|
25
|
+
import { gateIdRecordKey, hasWildcardGate, planGates } from './gates.js';
|
|
26
|
+
import { githubSourceResourceKey, planGithubSource } from './github-source.js';
|
|
18
27
|
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
28
|
+
import { planKeysRegistrar } from './keys-registrar.js';
|
|
29
|
+
import { desiredKosmosEnv, kosmosEnvDrift } from './kosmos.js';
|
|
19
30
|
import { APP_GONE_KIND } from './live.js';
|
|
20
31
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
21
32
|
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, agoraIssuerHostFor, clientOrganizationsState, companyLoginTunnelHost, customersClientOrganizations, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, ownCustomersAppState, pendingRemovalHosts, } from './login-host.js';
|
|
33
|
+
import { planMachineBypasses } from './machine-paths.js';
|
|
22
34
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
23
35
|
import { mailStoreSwitchChange } from './mail-store.js';
|
|
24
36
|
import { planMcp } from './mcp.js';
|
|
25
37
|
import { hasAdoptedResources, networkMoveNeeded, planNetworkMove } from './network-move.js';
|
|
38
|
+
import { planAppsAdmin } from './org-apps-admin.js';
|
|
26
39
|
import { planOrgPostgresMigration } from './org-postgres-migrate.js';
|
|
27
40
|
import { legacyUnmigrated, orgPostgresUndeliveredScopeKey, planOrgPostgresSteps, serviceOnServer, } from './org-postgres.js';
|
|
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';
|
|
41
|
+
import { OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_JOBS_ROUTE_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, OWN_SIGNIN_DB_MOVE_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnJobsRoute, planOwnMail, planOwnSfu, planSigninDbMove, } from './own-diff.js';
|
|
29
42
|
import { ownDnsHostnames } from './own-dns.js';
|
|
30
43
|
import { ownHostnames } from './own-hostnames.js';
|
|
44
|
+
import { platformMailMxHost } from './platform-diff.js';
|
|
31
45
|
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_AUTH_MCP_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSecretAvailability, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
32
46
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
33
47
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
34
48
|
import { companyAppPostLogoutUris, postLogoutUpdateOwed, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
35
|
-
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryRoutes, entryRoutesMarker, } from './proxy-labels.js';
|
|
49
|
+
import { CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY, CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, appLabelsAppliedScopeKey, entryAppsHostnames, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryRoutes, entryRoutesMarker, } from './proxy-labels.js';
|
|
36
50
|
import { planRemoteDatabase } from './remote-database.js';
|
|
37
51
|
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';
|
|
38
|
-
import { SIGN_IN_BASE_PATH, SIGN_IN_IMAGE_TAG_SUFFIX, SIGN_IN_IN_APP_SCOPE_KEY, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_LOGIN_INTERNAL_NAME, SIGN_IN_RATE_LIMIT_SCOPE_KEY, SIGN_IN_TRUSTED_SCOPE_KEY, inAppSignIn, inAppStatePairs, inAppTargets, } from './sign-in.js';
|
|
52
|
+
import { SIGN_IN_BASE_PATH, SIGN_IN_IMAGE_TAG_SUFFIX, SIGN_IN_IN_APP_SCOPE_KEY, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_LOGIN_INTERNAL_NAME, SIGN_IN_RATE_LIMIT_SCOPE_KEY, SIGN_IN_TRUSTED_SCOPE_KEY, inAppSignIn, inAppStatePairs, inAppTargets, signInLoginNames, } from './sign-in.js';
|
|
39
53
|
import { withStepSystem } from './step-system.js';
|
|
40
54
|
import { WORKER_APP_DB_BLOCKED_KEY, WORKER_APP_DB_LEGACY_NOTE_KEY, WORKER_APP_DB_SCOPE_KEYS, planWorkerAppDb, } from './worker-app-db.js';
|
|
41
55
|
import { brokerKeyDelivered, planWorkerApps } from './worker-app.js';
|
|
42
56
|
import { planWorkers } from './worker.js';
|
|
57
|
+
import { testCustomersOrgKey, withDarkWorkspaceSignIn, workspaceClientKey, } from './workspace-client.js';
|
|
58
|
+
import { planTestCustomers, planWorkspaceBrokerEntries, planWorkspaceClients, workspaceSignInMarker, } from './workspace-sign-in.js';
|
|
59
|
+
import { workspaceWorkerParent } from './workspace-worker.js';
|
|
60
|
+
import { planWorkspaces, withWorkspaceWorkers, withoutDarkEntries, workspaceGateKey, wsKey, } from './workspace.js';
|
|
43
61
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
44
62
|
export function toPlan(detail) {
|
|
45
63
|
return {
|
|
@@ -84,7 +102,17 @@ function madeByFoundry(org, r, recordedIds, recordedValues, platformNames) {
|
|
|
84
102
|
if (r.system === 'coolify') {
|
|
85
103
|
if (r.kind === 'environment')
|
|
86
104
|
return true;
|
|
87
|
-
|
|
105
|
+
if (r.kind !== 'application')
|
|
106
|
+
return false;
|
|
107
|
+
// The in-app login app of the org's own sign-in stack; its id is not recorded, so its name says it.
|
|
108
|
+
const login = `${org.slug}-auth-login`;
|
|
109
|
+
const inAppLogin = signInLoginNames({
|
|
110
|
+
names: { authLogin: login },
|
|
111
|
+
internal: { authLogin: login },
|
|
112
|
+
});
|
|
113
|
+
if (r.config.name === inAppLogin.name)
|
|
114
|
+
return Boolean(recordedIds[`coolify:own-auth:${org.id}`]);
|
|
115
|
+
return r.config.name === chatInternalNames(org.slug).mas;
|
|
88
116
|
}
|
|
89
117
|
if (r.system !== 'cloudflare' || r.kind !== 'dns_record')
|
|
90
118
|
return false;
|
|
@@ -122,6 +150,7 @@ const OWN_MARKER_KINDS = new Set([
|
|
|
122
150
|
'own-files',
|
|
123
151
|
'own-files-r2',
|
|
124
152
|
'own-jobs',
|
|
153
|
+
'own-jobs-route',
|
|
125
154
|
'own-sfu',
|
|
126
155
|
]);
|
|
127
156
|
/** The kinds a provider lists a resource under before a recorded `system:kind:key` maps it to foundry's own. */
|
|
@@ -197,7 +226,10 @@ function liveResourceName(r) {
|
|
|
197
226
|
* Company Factory end to end", which wires real readers and executors
|
|
198
227
|
* against this seam.
|
|
199
228
|
*/
|
|
200
|
-
export function computePlanDetail(
|
|
229
|
+
export function computePlanDetail(declaredOrg, live, recordedIds = {}, agoraHost = NO_AGORA_HOST_VALUES, jobsAvailable = false, platformReadiness = DEFAULT_ORG_PLATFORM_READINESS, options = {}) {
|
|
230
|
+
// An entry that is not live has no app, address or Worker of its own: only its workspace exists (the
|
|
231
|
+
// workspace steps, the gate and the tunnel read `declaredOrg`).
|
|
232
|
+
const org = withoutDarkEntries(declaredOrg);
|
|
201
233
|
const byKey = new Map(live.map((r) => [resourceKey(r.system, r.kind, r.key), r]));
|
|
202
234
|
const claimed = new Set();
|
|
203
235
|
const plannedSteps = [];
|
|
@@ -243,7 +275,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
243
275
|
// #267: after the ZITADEL org step, before any app's own issuer
|
|
244
276
|
// application. Apps created later in this run are not in the binding yet;
|
|
245
277
|
// the next run replans this step and repoints them.
|
|
246
|
-
const loginHost = planLoginHost(org, recordedIds, {
|
|
278
|
+
const loginHost = planLoginHost(withDarkWorkspaceSignIn(org, declaredOrg), recordedIds, {
|
|
247
279
|
authReady: platformReadiness.authReady,
|
|
248
280
|
issuerPlanned: issuer.step !== null,
|
|
249
281
|
bindingsWired: options.loginHostBindings ?? false,
|
|
@@ -320,7 +352,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
320
352
|
const chat = planChat(org, byKey, claimed, recordedIds, agoraHost, issuer.step !== null);
|
|
321
353
|
if (chat.step)
|
|
322
354
|
plannedSteps.push(chat.step);
|
|
323
|
-
const calls = planCalls(org, byKey, claimed
|
|
355
|
+
const calls = planCalls(org, byKey, claimed);
|
|
324
356
|
if (calls.step)
|
|
325
357
|
plannedSteps.push(calls.step);
|
|
326
358
|
// An own org's media server, before the calls service that reads its addresses.
|
|
@@ -423,9 +455,14 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
423
455
|
const customers = planCustomers(org, live, claimed, recordedIds, options.zitadelOrgMetadata ?? false);
|
|
424
456
|
if (customers.step)
|
|
425
457
|
plannedSteps.push(customers.step);
|
|
458
|
+
// The workspaces' own customers, before the clients and the workspace steps that read the organisation's id.
|
|
459
|
+
const testCustomers = planTestCustomers(declaredOrg, live, claimed, recordedIds, options.zitadelOrgMetadata ?? false);
|
|
460
|
+
if (testCustomers.step)
|
|
461
|
+
plannedSteps.push(testCustomers.step);
|
|
426
462
|
for (const step of [
|
|
427
463
|
...planIssuerApps(org, byKey, claimed),
|
|
428
464
|
...planEntryClients(org, byKey, claimed),
|
|
465
|
+
...planWorkspaceClients(declaredOrg, byKey, claimed),
|
|
429
466
|
]) {
|
|
430
467
|
const guarded = guardApp(step);
|
|
431
468
|
if (guarded)
|
|
@@ -433,14 +470,16 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
433
470
|
}
|
|
434
471
|
// Auth ADR 0002: the MCP project and the JWT app, after the app they change
|
|
435
472
|
// and before the broker entry and the Worker that read the project id.
|
|
436
|
-
const mcp = planMcp(org, recordedIds, plannedSteps, options.zitadelMcp ?? false);
|
|
473
|
+
const mcp = planMcp(org, recordedIds, plannedSteps, options.zitadelMcp ?? false, declaredOrg);
|
|
437
474
|
plannedSteps.push(...mcp.steps);
|
|
438
475
|
// The Access gate before the database, app, tunnel and zone (#84): on a
|
|
439
476
|
// first apply the hostname is gated before anything answers on it.
|
|
440
477
|
// An Access gate that cannot be planned must not leave the hostname it
|
|
441
478
|
// guards routed: the tunnel (and its DNS) that exposes it waits too.
|
|
442
479
|
let accessGateBlocked = false;
|
|
443
|
-
const access = planAccess(org, recordedIds, platformReadiness.accessProviderId ?? null, options.platformHostnames ?? []);
|
|
480
|
+
const access = planAccess(org, recordedIds, platformReadiness.accessProviderId ?? null, options.platformHostnames ?? [], declaredOrg);
|
|
481
|
+
for (const note of access.notes)
|
|
482
|
+
dnsBlocked.push({ description: note, key: 'cloudflare:access' });
|
|
444
483
|
if (access.blocked) {
|
|
445
484
|
accessGateBlocked = true;
|
|
446
485
|
dnsBlocked.push({
|
|
@@ -477,11 +516,32 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
477
516
|
});
|
|
478
517
|
if (workerAppDb.step)
|
|
479
518
|
plannedSteps.push(workerAppDb.step);
|
|
519
|
+
// Before the apps: an app built from the company's own account is created through this source.
|
|
520
|
+
const githubSource = planGithubSource(org, recordedIds, platformReadiness.githubAppKeys);
|
|
521
|
+
if (githubSource.step)
|
|
522
|
+
plannedSteps.push(githubSource.step);
|
|
480
523
|
const authIssuer = 'error' in agoraHost ? null : agoraHost.issuer;
|
|
481
524
|
const app = planApps(org, byKey, claimed, recordedIds, plannedSteps, authIssuer, platformReadiness);
|
|
482
525
|
plannedSteps.push(...app.steps);
|
|
526
|
+
// The broker's workspaces database, before any broker entry that needs it: made once, when none is held or set.
|
|
527
|
+
const workspacesDatabasePlanned = declaredOrg.hosting.enabled &&
|
|
528
|
+
declaredOrg.hosting.entries.some((e) => e.uses?.auth?.workspaces) &&
|
|
529
|
+
(options.broker ?? false) &&
|
|
530
|
+
!(options.brokerWorkspacesDatabase ?? false) &&
|
|
531
|
+
(options.provisionWorkspacesDatabase ?? false);
|
|
532
|
+
if (workspacesDatabasePlanned) {
|
|
533
|
+
plannedSteps.push({
|
|
534
|
+
action: { system: 'coolify', kind: 'workspaces-database', key: org.id, desired: {} },
|
|
535
|
+
step: {
|
|
536
|
+
description: `make the database of ${org.slug}'s broker for customer workspaces and hold its URL at ${workspacesDatabaseRef(org)}`,
|
|
537
|
+
destroys: false,
|
|
538
|
+
migrates: false,
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
const brokerWorkspacesDatabase = (options.brokerWorkspacesDatabase ?? false) || workspacesDatabasePlanned;
|
|
483
543
|
// After the apps: the broker entry's key is minted into each app's env.
|
|
484
|
-
const brokerEntries = planBrokerEntries(org, recordedIds, plannedSteps, options.broker ?? false);
|
|
544
|
+
const brokerEntries = planBrokerEntries(org, recordedIds, plannedSteps, options.broker ?? false, brokerWorkspacesDatabase);
|
|
485
545
|
plannedSteps.push(...brokerEntries.steps);
|
|
486
546
|
// F6a: after the apps and the org's own ai key, which it mints below.
|
|
487
547
|
const entryAiKeys = planEntryAiKeys(org, recordedIds, plannedSteps, platformReadiness, (options.now ?? Date.now)());
|
|
@@ -495,6 +555,10 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
495
555
|
const edgeReservedLabels = planEdgeReservedLabels(org, recordedIds, platformReadiness, options.platformHostnames ?? []);
|
|
496
556
|
if (edgeReservedLabels.step)
|
|
497
557
|
plannedSteps.push(edgeReservedLabels.step);
|
|
558
|
+
// F3: the slug edge keys a company's platform hostnames on, next to the company's own edge key.
|
|
559
|
+
const edgeSlugStep = planEdgeSlug(org, recordedIds, platformReadiness);
|
|
560
|
+
if (edgeSlugStep)
|
|
561
|
+
plannedSteps.push(edgeSlugStep);
|
|
498
562
|
// Blue/green (`blue-green.ts`): the apps and balancers before the tunnel routes to them, a second
|
|
499
563
|
// server's connector after the tunnel exists.
|
|
500
564
|
const blueGreen = planBlueGreen({
|
|
@@ -509,7 +573,49 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
509
573
|
p.action.key === appStepKey(org, entryKey)),
|
|
510
574
|
});
|
|
511
575
|
plannedSteps.push(...blueGreen.steps.filter((x) => x.phase === 'before').map(blueGreenDraft()));
|
|
512
|
-
|
|
576
|
+
// A workspace is made only behind its gate: recorded already, or planned earlier in this run.
|
|
577
|
+
const workspaceGateReady = (gateKey) => Boolean(recordedIds[gateIdRecordKey(org.id, gateKey)]) ||
|
|
578
|
+
plannedSteps.some((p) => p.action.system === 'cloudflare' &&
|
|
579
|
+
p.action.kind === 'gate' &&
|
|
580
|
+
p.action.key === appStepKey(org, gateKey));
|
|
581
|
+
const stepPlanned = (system, kind, key) => plannedSteps.some((p) => p.action.system === system &&
|
|
582
|
+
p.action.kind === kind &&
|
|
583
|
+
(key === undefined || p.action.key === key));
|
|
584
|
+
// A workspace's sign-in is ready when its values are recorded, or the steps that record them are planned.
|
|
585
|
+
const workspaceSignInWanted = (entry) => {
|
|
586
|
+
const marker = workspaceSignInMarker(declaredOrg, entry, recordedIds, authIssuer);
|
|
587
|
+
if (marker)
|
|
588
|
+
return marker;
|
|
589
|
+
const customers = entry.uses?.auth?.users === 'customers';
|
|
590
|
+
const orgKnown = customers
|
|
591
|
+
? Boolean(recordedIds[testCustomersOrgKey(org.id)]) ||
|
|
592
|
+
stepPlanned('zitadel', 'customers-test')
|
|
593
|
+
: Boolean(recordedIds.zitadel_org);
|
|
594
|
+
const clientKnown = Boolean(recordedIds[entryClientIdScopeKey(workspaceClientKey(entry.key))]) ||
|
|
595
|
+
stepPlanned('zitadel', 'app', workspaceClientKey(entry.key));
|
|
596
|
+
const mcpKnown = !entry.uses?.auth?.mcp ||
|
|
597
|
+
Boolean(recordedIds[entryMcpProjectScopeKey(workspaceClientKey(entry.key))]) ||
|
|
598
|
+
stepPlanned('zitadel', 'mcp-project', `${org.id}:${workspaceClientKey(entry.key)}`);
|
|
599
|
+
return orgKnown && clientKnown && mcpKnown && authIssuer ? 'pending' : null;
|
|
600
|
+
};
|
|
601
|
+
// Blue/green's cousin: each server workspace is a stage with a host of its own, behind its gate.
|
|
602
|
+
const workspaces = planWorkspaces({
|
|
603
|
+
org: declaredOrg,
|
|
604
|
+
recordedIds,
|
|
605
|
+
servers: options.blueGreenServers,
|
|
606
|
+
gateReady: workspaceGateReady,
|
|
607
|
+
signInWanted: workspaceSignInWanted,
|
|
608
|
+
authIssuer,
|
|
609
|
+
...(options.stageSinkDown ? { stageSinkDown: true } : {}),
|
|
610
|
+
});
|
|
611
|
+
plannedSteps.push(...workspaces.steps);
|
|
612
|
+
// After the workspace step has put the stage's own key in its env.
|
|
613
|
+
const workspaceBrokerEntries = planWorkspaceBrokerEntries(declaredOrg, recordedIds, plannedSteps, options.broker ?? false, brokerWorkspacesDatabase, (entry) => recordedIds[bgKey.stageState(entry.key)] === 'ok' ||
|
|
614
|
+
plannedSteps.some((p) => p.action.system === 'coolify' &&
|
|
615
|
+
p.action.kind === 'workspace' &&
|
|
616
|
+
p.action.desired.entry === entry.key));
|
|
617
|
+
plannedSteps.push(...workspaceBrokerEntries.steps);
|
|
618
|
+
const tunnel = planTunnel(declaredOrg, byKey, claimed, recordedIds, platformReadiness.platformApex);
|
|
513
619
|
// #238: a tunnel that is being provisioned waits for an active zone; parking
|
|
514
620
|
// one never does.
|
|
515
621
|
// #555: provisioning writes the tunnel config, the zone lookup and DNS; a
|
|
@@ -577,10 +683,45 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
577
683
|
archon: options.bouleArchonCredential === true,
|
|
578
684
|
directory: options.customersDirectory === true,
|
|
579
685
|
}, platformReadiness.canHoldSecrets === true, (options.now ?? Date.now)());
|
|
580
|
-
|
|
686
|
+
// F13: Boule's `CUSTOMER_GRANTS`, from the entries that declare `uses.customerGrants`. Every reason it
|
|
687
|
+
// says nothing yet is a plan line, never silence.
|
|
688
|
+
const customerGrants = wantedCustomerGrants(org, recordedIds, {
|
|
689
|
+
live: byKey.has(resourceKey('coolify', 'boule', org.id)),
|
|
690
|
+
});
|
|
691
|
+
const customerGrantsNotes = [...customerGrants.notes];
|
|
692
|
+
let customerGrantsShape = null;
|
|
693
|
+
if (customerGrants.shape) {
|
|
694
|
+
if (bouleCredentials.wants.customersDirectory) {
|
|
695
|
+
customerGrantsShape = customerGrants.shape;
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
customerGrantsNotes.push(`${org.slug}'s customer grants are not planned: they need the customers directory credential, which is blocked above`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// T3: a company with customers gets a registrar key at the sign-in broker; Boule is given it.
|
|
702
|
+
const registrar = planBrokerRegistrar(org, recordedIds, plannedSteps, {
|
|
703
|
+
brokerWired: options.broker ?? false,
|
|
704
|
+
canHoldSecrets: platformReadiness.canHoldSecrets === true,
|
|
705
|
+
issuerKnown: Boolean(authIssuer),
|
|
706
|
+
});
|
|
707
|
+
const boule = planBoule(org, byKey, claimed, recordedIds, authIssuer, options.otherSlugs ?? [], options.archonUrl ?? null, platformReadiness, {
|
|
708
|
+
...bouleCredentials.wants,
|
|
709
|
+
...(customerGrantsShape ? { customerGrants: true } : {}),
|
|
710
|
+
...(registrar.deliver ? { registrar: true } : {}),
|
|
711
|
+
...(registrar.keepBouleEnv ? { registrarKept: true } : {}),
|
|
712
|
+
}, customerGrantsShape);
|
|
581
713
|
if (boule.step)
|
|
582
714
|
plannedSteps.push(boule.step);
|
|
583
|
-
//
|
|
715
|
+
// foundry#949: the company's kosmos, only once it turns it on. Its fixed ZITADEL client first, the same
|
|
716
|
+
// reason Boule's and Agora's do: `coolifyKosmos` reads this step's recorded client id.
|
|
717
|
+
const kosmosClient = planKosmosClient(org, byKey, claimed);
|
|
718
|
+
const kosmosGuarded = guardApp(kosmosClient);
|
|
719
|
+
if (kosmosGuarded)
|
|
720
|
+
plannedSteps.push(kosmosGuarded);
|
|
721
|
+
const kosmos = planKosmos(org, byKey, claimed, recordedIds, authIssuer, options.otherSlugs ?? []);
|
|
722
|
+
if (kosmos.step)
|
|
723
|
+
plannedSteps.push(kosmos.step);
|
|
724
|
+
// Boule's, Agora's and kosmos's blue/green: after their own steps, which make the app it copies.
|
|
584
725
|
plannedSteps.push(...blueGreen.steps.filter((x) => x.phase === 'last').map(blueGreenDraft()));
|
|
585
726
|
// Boule owners, after the Boule step: needs the app deployed and the seeding secret in its env.
|
|
586
727
|
const bouleOwners = planBouleOwners(org, recordedIds, (byKey.get(resourceKey('coolify', 'boule', org.id))?.config.env ?? null));
|
|
@@ -598,11 +739,26 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
598
739
|
// the same dependency `planEntryAiKeys`/`planFilesKeys` have.
|
|
599
740
|
const entryEdgeKeys = planEntryEdgeKeys(org, recordedIds, plannedSteps, platformReadiness, (options.now ?? Date.now)());
|
|
600
741
|
plannedSteps.push(...entryEdgeKeys.steps);
|
|
742
|
+
// F7: the signed-records registrar key, after Boule like the edge key.
|
|
743
|
+
const keysRegistrar = planKeysRegistrar(org, recordedIds, plannedSteps, { ...(options.keysRegistrarUrl !== undefined ? { url: options.keysRegistrarUrl } : {}) }, (options.now ?? Date.now)());
|
|
744
|
+
plannedSteps.push(...keysRegistrar.steps);
|
|
745
|
+
// A company on shared services registers on the scheduler the host supplies (the owner company's); a company
|
|
746
|
+
// that runs its own services never does, only on its own.
|
|
747
|
+
const schedulerUsable = (jobsAvailable && !ownsServices(org)) || platformReadiness.jobsReady;
|
|
748
|
+
// A `uses.jobs` entry registers with the scheduler after its app, whose env the step writes.
|
|
749
|
+
const entryJobs = planEntryJobs(org, recordedIds, plannedSteps, platformReadiness, schedulerUsable);
|
|
750
|
+
plannedSteps.push(...entryJobs.steps);
|
|
751
|
+
// F12: a deploy key for an entry's own repo, after its app (whose env the step writes).
|
|
752
|
+
const entryRepoKeys = planEntryRepoKeys(org, recordedIds, plannedSteps, platformReadiness);
|
|
753
|
+
plannedSteps.push(...entryRepoKeys.steps);
|
|
601
754
|
// Boule's own broker entry, after its app: the entry needs the AUTH_APP_KEY
|
|
602
755
|
// that step puts in the app's env.
|
|
603
756
|
const bouleBroker = planBouleBrokerEntry(org, recordedIds, plannedSteps, options.broker ?? false);
|
|
604
757
|
if (bouleBroker.step)
|
|
605
758
|
plannedSteps.push(bouleBroker.step);
|
|
759
|
+
// The company's registrar record in the broker, after Boule's own broker entry.
|
|
760
|
+
if (registrar.step)
|
|
761
|
+
plannedSteps.push(registrar.step);
|
|
606
762
|
// #440: Agora's desktop sign-in backend, after Agora and Boule: its readiness probe goes through Agora's proxy, and
|
|
607
763
|
// Agora writes the upstream only once the service ran healthy (so a first apply needs a second to turn it on).
|
|
608
764
|
const desktopAuth = planDesktopAuth(org, recordedIds, {
|
|
@@ -622,12 +778,20 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
622
778
|
});
|
|
623
779
|
if (ownJobs.step)
|
|
624
780
|
plannedSteps.push(ownJobs.step);
|
|
781
|
+
const ownJobsRoute = planOwnJobsRoute({
|
|
782
|
+
org,
|
|
783
|
+
recordedIds,
|
|
784
|
+
readiness: platformReadiness,
|
|
785
|
+
ownServices: options.ownServices ?? false,
|
|
786
|
+
zoneActive: ownZoneActive,
|
|
787
|
+
schedulerPlanned: ownJobs.step !== null,
|
|
788
|
+
});
|
|
789
|
+
if (ownJobsRoute.step)
|
|
790
|
+
plannedSteps.push(ownJobsRoute.step);
|
|
625
791
|
// #173: register the org's own app with the platform's jobs scheduler.
|
|
626
792
|
// After Agora, so a failing registration can never stop Agora from
|
|
627
793
|
// provisioning (`runSteps` stops at the first failing step).
|
|
628
|
-
const jobsRegistration = planJobsRegistration(org, recordedIds,
|
|
629
|
-
// An own org never registers with the shared scheduler the host supplies.
|
|
630
|
-
(jobsAvailable && !ownsServices(org)) || platformReadiness.jobsReady, platformReadiness.missingKeys.jobs, platformReadiness.noKeyReader, platformReadiness.jobsUrlMissing ?? false);
|
|
794
|
+
const jobsRegistration = planJobsRegistration(org, recordedIds, schedulerUsable, platformReadiness.missingKeys.jobs, platformReadiness.noKeyReader, platformReadiness.jobsUrlMissing ?? false);
|
|
631
795
|
if (jobsRegistration.step)
|
|
632
796
|
plannedSteps.push(jobsRegistration.step);
|
|
633
797
|
const remoteDatabase = planRemoteDatabase(org, options.remoteDatabase, recordedIds);
|
|
@@ -660,7 +824,8 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
660
824
|
}
|
|
661
825
|
// ADR 0002: Worker apps, after the Workers for the same reason: a route, record
|
|
662
826
|
// or script name that is not ours throws at apply and `runSteps` stops there.
|
|
663
|
-
const
|
|
827
|
+
const workerOrg = withWorkspaceWorkers(org, declaredOrg);
|
|
828
|
+
const workerApps = planWorkerApps(workerOrg, recordedIds, {
|
|
664
829
|
available: options.workerApps ?? false,
|
|
665
830
|
...(options.workerAppKeysUrl !== undefined ? { keysUrl: options.workerAppKeysUrl } : {}),
|
|
666
831
|
...(options.workerAppRegistryAccountId !== undefined
|
|
@@ -683,7 +848,10 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
683
848
|
});
|
|
684
849
|
for (const step of workerApps.steps) {
|
|
685
850
|
const stepEntry = step.action.key.slice(org.id.length + 1);
|
|
686
|
-
|
|
851
|
+
// A workspace Worker lives in the platform's apps zone, not the org's own.
|
|
852
|
+
const wsEntry = workerOrg.hosting.entries.find((e) => e.key === stepEntry);
|
|
853
|
+
const wsParent = wsEntry ? workspaceWorkerParent(wsEntry) : null;
|
|
854
|
+
const ownZone = wsParent === null && org.hosting.usesDomain && Boolean(org.domain);
|
|
687
855
|
const apply = step.action.desired.mode === 'apply';
|
|
688
856
|
// Retire never looks a zone up; a deploy needs the org's zone, or the shared oikos.host zone.
|
|
689
857
|
const hasDatabase = Boolean(org.hosting.entries.find((e) => e.key === stepEntry)?.workerApp?.database);
|
|
@@ -691,10 +859,16 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
691
859
|
? null
|
|
692
860
|
: hasDatabase
|
|
693
861
|
? edgeNeeded(platformReadiness, ['zones', 'dns', 'access', 'tunnels'])
|
|
694
|
-
: ownZone
|
|
862
|
+
: ownZone || wsParent !== null
|
|
695
863
|
? edgeNeeded(platformReadiness, ['zones', 'dns'])
|
|
696
864
|
: edgeNeeded(platformReadiness, ['zones']);
|
|
697
|
-
if (
|
|
865
|
+
if (apply && wsParent !== null && !workspaceGateReady(workspaceGateKey(wsParent))) {
|
|
866
|
+
appBlocked.push({
|
|
867
|
+
description: `${org.slug}'s ${wsParent} workspace Worker is blocked, not planned: its Access gate is not recorded or planned yet`,
|
|
868
|
+
key: step.action.key,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
else if (reason) {
|
|
698
872
|
dnsBlocked.push(edgeBlockedItem(org, `${stepEntry} Worker app`, step.action.key, reason));
|
|
699
873
|
}
|
|
700
874
|
else if (apply && ownZone && zoneWaiting) {
|
|
@@ -703,7 +877,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
703
877
|
key: step.action.key,
|
|
704
878
|
});
|
|
705
879
|
}
|
|
706
|
-
else if (apply && !ownZone && !(options.oikosZoneReady ?? false)) {
|
|
880
|
+
else if (apply && wsParent === null && !ownZone && !(options.oikosZoneReady ?? false)) {
|
|
707
881
|
zoneBlocked.push({
|
|
708
882
|
description: `${org.slug}'s ${stepEntry} Worker app is blocked, not planned: the shared oikos.host zone is not ready yet`,
|
|
709
883
|
key: step.action.key,
|
|
@@ -713,11 +887,14 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
713
887
|
plannedSteps.push(step);
|
|
714
888
|
}
|
|
715
889
|
}
|
|
890
|
+
// `uses.audit`: registers the entry's ledger for platform sealing, after its Worker app.
|
|
891
|
+
const entryAudit = planEntryAudit(org, recordedIds, plannedSteps, platformReadiness);
|
|
892
|
+
plannedSteps.push(...entryAudit.steps);
|
|
716
893
|
// Last: a repair that fails must not stop any step after it (`runSteps` stops at the first failure).
|
|
717
894
|
const idTokenUserinfo = planIdTokenUserinfo(org, live, recordedIds, plannedSteps, platformReadiness.authReady, options.zitadelIdTokenUserinfo ?? false);
|
|
718
895
|
if (idTokenUserinfo)
|
|
719
896
|
plannedSteps.push(idTokenUserinfo);
|
|
720
|
-
const postLogoutUris = planPostLogoutUris(
|
|
897
|
+
const postLogoutUris = planPostLogoutUris(declaredOrg, live, recordedIds, plannedSteps, platformReadiness.authReady, options.zitadelPostLogoutUris ?? false);
|
|
721
898
|
if (postLogoutUris)
|
|
722
899
|
plannedSteps.push(postLogoutUris);
|
|
723
900
|
const desktopCallbacks = planDesktopCallbacks(org, live, recordedIds, plannedSteps, platformReadiness.authReady, options.zitadelRedirectUris ?? false);
|
|
@@ -725,6 +902,24 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
725
902
|
plannedSteps.push(desktopCallbacks);
|
|
726
903
|
// #444, last: a Postgres step that fails must not stop the zone, mail, app, Agora and Boule steps of a live company.
|
|
727
904
|
plannedSteps.push(...orgPostgresSteps);
|
|
905
|
+
// F4 (#925): with apps on, the apps admin role on that server and the route Hyperdrive reaches it by.
|
|
906
|
+
const appsAdmin = planAppsAdmin(org, recordedIds, plannedSteps, {
|
|
907
|
+
canHoldSecrets: platformReadiness.canHoldSecrets === true,
|
|
908
|
+
postgresPlanned: orgPostgresSteps.some((p) => p.action.system === 'coolify' && p.action.kind === 'org-postgres'),
|
|
909
|
+
});
|
|
910
|
+
plannedSteps.push(...appsAdmin.steps);
|
|
911
|
+
const appsDbRoute = planAppsDbRoute(org, recordedIds, plannedSteps, {
|
|
912
|
+
accessReady: edgeNeeded(platformReadiness, ['access', 'tunnels', 'dns', 'zones']) === null,
|
|
913
|
+
canHoldSecrets: platformReadiness.canHoldSecrets === true,
|
|
914
|
+
tunnelReady: Boolean(recordedIds[`cloudflare:tunnel:${org.id}`]) ||
|
|
915
|
+
plannedSteps.some((s) => s.action.system === 'cloudflare' && s.action.kind === 'tunnel'),
|
|
916
|
+
});
|
|
917
|
+
plannedSteps.push(...appsDbRoute.steps);
|
|
918
|
+
// FZ: the company slug and the zone ids, in the same Boule env.
|
|
919
|
+
const appsZones = planAppsZones(org, recordedIds, plannedSteps, {
|
|
920
|
+
zonesReady: edgeNeeded(platformReadiness, ['zones']) === null,
|
|
921
|
+
});
|
|
922
|
+
plannedSteps.push(...appsZones.steps);
|
|
728
923
|
// After the org-postgres steps: the desktop sign-in service lives on a home they may create in this same plan.
|
|
729
924
|
if (desktopAuth.step)
|
|
730
925
|
plannedSteps.push(desktopAuth.step);
|
|
@@ -742,7 +937,12 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
742
937
|
for (const id of platformReadiness.platformZitadelIds ?? [])
|
|
743
938
|
recordedValues.add(id);
|
|
744
939
|
}
|
|
745
|
-
const platformNames = new Set([
|
|
940
|
+
const platformNames = new Set([
|
|
941
|
+
...(options.platformHostnames ?? []),
|
|
942
|
+
platformReadiness.mailHost?.mailHost ?? '',
|
|
943
|
+
// The shared mail relay's MX host, which the platform's relay step writes.
|
|
944
|
+
platformReadiness.platformApex ? platformMailMxHost(platformReadiness.platformApex) : '',
|
|
945
|
+
].map((h) => h.toLowerCase()));
|
|
746
946
|
platformNames.delete('');
|
|
747
947
|
const unmanaged = live
|
|
748
948
|
.filter((r) => !claimed.has(resourceKey(r.system, r.kind, r.key)))
|
|
@@ -778,14 +978,23 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
778
978
|
for (const description of bouleCredentials.blocked) {
|
|
779
979
|
unmanaged.push({ description, key: 'coolify:boule:credentials' });
|
|
780
980
|
}
|
|
981
|
+
customerGrantsNotes.forEach((description, index) => {
|
|
982
|
+
unmanaged.push({ description, key: `coolify:boule:customer-grants:${index}` });
|
|
983
|
+
});
|
|
984
|
+
registrar.blocked.forEach((description, index) => {
|
|
985
|
+
unmanaged.push({ description, key: `coolify:broker-registrar:${index}` });
|
|
986
|
+
});
|
|
781
987
|
if (boule.mailBlocked) {
|
|
782
988
|
unmanaged.push({ description: boule.mailBlocked, key: 'coolify:boule:mail' });
|
|
783
989
|
}
|
|
990
|
+
if (kosmos.blocked)
|
|
991
|
+
unmanaged.push({ description: kosmos.blocked, key: 'coolify:kosmos' });
|
|
784
992
|
// Same for an ai key minted but never delivered: planning a new mint would
|
|
785
993
|
// orphan a second live key.
|
|
786
994
|
if (aiKey.blocked)
|
|
787
995
|
unmanaged.push({ description: aiKey.blocked, key: 'ai:key' });
|
|
788
996
|
unmanaged.push(...blueGreen.unmanaged);
|
|
997
|
+
unmanaged.push(...workspaces.unmanaged);
|
|
789
998
|
unmanaged.push(...filesKeys.blocked);
|
|
790
999
|
unmanaged.push(...entryAiKeys.blocked);
|
|
791
1000
|
unmanaged.push(...edgeWorkers.blocked);
|
|
@@ -798,7 +1007,15 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
798
1007
|
}
|
|
799
1008
|
unmanaged.push(...mcp.blocked);
|
|
800
1009
|
unmanaged.push(...customers.blocked);
|
|
1010
|
+
unmanaged.push(...testCustomers.blocked);
|
|
801
1011
|
unmanaged.push(...entryEdgeKeys.blocked);
|
|
1012
|
+
unmanaged.push(...keysRegistrar.blocked);
|
|
1013
|
+
unmanaged.push(...appsAdmin.blocked);
|
|
1014
|
+
unmanaged.push(...appsDbRoute.blocked);
|
|
1015
|
+
unmanaged.push(...appsZones.blocked);
|
|
1016
|
+
unmanaged.push(...entryJobs.blocked);
|
|
1017
|
+
unmanaged.push(...entryAudit.blocked);
|
|
1018
|
+
unmanaged.push(...entryRepoKeys.blocked);
|
|
802
1019
|
unmanaged.push(...edgeReservedLabels.blocked);
|
|
803
1020
|
if (ownAuth.blocked)
|
|
804
1021
|
unmanaged.push({ description: ownAuth.blocked, key: OWN_AUTH_BLOCKED_KEY });
|
|
@@ -807,6 +1024,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
807
1024
|
}
|
|
808
1025
|
if (ownJobs.blocked)
|
|
809
1026
|
unmanaged.push({ description: ownJobs.blocked, key: OWN_JOBS_BLOCKED_KEY });
|
|
1027
|
+
if (ownJobsRoute.blocked) {
|
|
1028
|
+
unmanaged.push({ description: ownJobsRoute.blocked, key: OWN_JOBS_ROUTE_BLOCKED_KEY });
|
|
1029
|
+
}
|
|
810
1030
|
if (ownMail.blocked)
|
|
811
1031
|
unmanaged.push({ description: ownMail.blocked, key: OWN_MAIL_BLOCKED_KEY });
|
|
812
1032
|
if (ownSfu.blocked)
|
|
@@ -895,8 +1115,10 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
895
1115
|
});
|
|
896
1116
|
}
|
|
897
1117
|
// #243: an appEnv source nothing will record is reported, not silently skipped.
|
|
1118
|
+
unmanaged.push(...githubSource.blocked);
|
|
898
1119
|
unmanaged.push(...app.blocked);
|
|
899
1120
|
unmanaged.push(...brokerEntries.blocked);
|
|
1121
|
+
unmanaged.push(...workspaceBrokerEntries.blocked);
|
|
900
1122
|
unmanaged.push(...bouleBroker.blocked);
|
|
901
1123
|
// F6: last, so every database step above runs before the backup of its database.
|
|
902
1124
|
const backups = planCompanyBackups(org, recordedIds, options.backupStorageUuid);
|
|
@@ -940,6 +1162,7 @@ const NEEDS_OWN_ZITADEL = new Set([
|
|
|
940
1162
|
'zitadel:owner',
|
|
941
1163
|
'zitadel:app',
|
|
942
1164
|
'zitadel:customers',
|
|
1165
|
+
'zitadel:customers-test',
|
|
943
1166
|
'zitadel:id-token-userinfo',
|
|
944
1167
|
'zitadel:desktop-callback',
|
|
945
1168
|
'zitadel:login-host',
|
|
@@ -947,7 +1170,10 @@ const NEEDS_OWN_ZITADEL = new Set([
|
|
|
947
1170
|
'coolify:calls-service',
|
|
948
1171
|
'coolify:calls-chat',
|
|
949
1172
|
'coolify:broker-entry',
|
|
1173
|
+
'coolify:workspace-broker-entry',
|
|
1174
|
+
'coolify:workspaces-database',
|
|
950
1175
|
'coolify:boule-broker-entry',
|
|
1176
|
+
'coolify:broker-registrar',
|
|
951
1177
|
'files:organisation',
|
|
952
1178
|
]);
|
|
953
1179
|
/**
|
|
@@ -1555,8 +1781,8 @@ function loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned)
|
|
|
1555
1781
|
* actually need it -- CMS's, chat's and calls' entries follow their own
|
|
1556
1782
|
* on/off switch, the same ones this file already gates their own
|
|
1557
1783
|
* `coolify:*` steps on (`org.ts`'s `SiteConfig.enabled`, `ChatConfig.enabled`,
|
|
1558
|
-
* `CallsConfig.enabled`);
|
|
1559
|
-
* "every org gets one" rule; the app schema follows whether any
|
|
1784
|
+
* `CallsConfig.enabled`); kosmos follows its own switch too (`KosmosApp.enabled`, foundry#949); Boule's is
|
|
1785
|
+
* unconditional, matching `org.ts`'s own "every org gets one" rule; the app schema follows whether any
|
|
1560
1786
|
* `hosting.entries` entry declares `database: true` (`coolifyDatabase`'s own
|
|
1561
1787
|
* gate) rather than `hosting.enabled` alone, since a hosting org with no
|
|
1562
1788
|
* database-bearing entry has nothing for this schema to hold.
|
|
@@ -1564,6 +1790,8 @@ function loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned)
|
|
|
1564
1790
|
function orgServiceSchemaEnabled(org, schema) {
|
|
1565
1791
|
if (schema.schema === 'cms')
|
|
1566
1792
|
return org.site.enabled;
|
|
1793
|
+
if (schema.schema === 'kosmos')
|
|
1794
|
+
return kosmosOn(org);
|
|
1567
1795
|
if (schema.schema.startsWith('chat_'))
|
|
1568
1796
|
return org.chat.enabled;
|
|
1569
1797
|
if (schema.schema.startsWith('calls_'))
|
|
@@ -1983,75 +2211,47 @@ function planChat(org, byKey, claimed, recordedIds, reading, issuerPlanned) {
|
|
|
1983
2211
|
* matrix-authentication-service, entirely separate from chat's
|
|
1984
2212
|
* (`plan/calls-matrix.ts`'s own top doc comment: a call guest must never gain
|
|
1985
2213
|
* chat membership). Unlike chat, off by default: `org.ts`'s
|
|
1986
|
-
* `CallsConfig.enabled` doc comment.
|
|
1987
|
-
* {@link planApp}/{@link planDatabase}/{@link planTunnel} below
|
|
1988
|
-
* (`planHostingToggle`, #7 corrections 1) -- turning it off parks Synapse and
|
|
1989
|
-
* MAS, never either database (`executor.ts`'s `coolifyCalls`).
|
|
2214
|
+
* `CallsConfig.enabled` doc comment.
|
|
1990
2215
|
*
|
|
1991
|
-
* #
|
|
1992
|
-
*
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
2216
|
+
* #430: calls run transit-only now (calls#93's `CALLS_ENCRYPTION=transit`),
|
|
2217
|
+
* so the calls service no longer talks to a per-org Matrix stack and this
|
|
2218
|
+
* never provisions a new one, nor re-provisions a parked one -- the live
|
|
2219
|
+
* resource is still claimed, so it is never reported unmanaged. An org
|
|
2220
|
+
* whose stack is already live and enabled is left exactly as it is;
|
|
2221
|
+
* removing it is a separate, human-confirmed step. Turning `calls.enabled`
|
|
2222
|
+
* off still parks an already-live stack unchanged (`executor.ts`'s
|
|
2223
|
+
* `coolifyCalls`, #7 corrections 1): data is retained, not deleted.
|
|
1997
2224
|
*/
|
|
1998
|
-
function planCalls(org, byKey, claimed
|
|
2225
|
+
function planCalls(org, byKey, claimed) {
|
|
1999
2226
|
const key = resourceKey('coolify', 'calls', org.id);
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
// #248: the two images this stack's own apply would run, checked the same
|
|
2009
|
-
// way the platform's SFU checks LiveKit/cloudflared and ZITADEL checks its
|
|
2010
|
-
// own tag -- a re-apply of the same declaration must not silently run a
|
|
2011
|
-
// different version with no diff shown anywhere.
|
|
2012
|
-
if (org.calls.enabled && !byKey.get(key)) {
|
|
2013
|
-
const synapseBlocked = floatingTagBlocked('Synapse', synapseTag ?? VERSIONS.synapse, 'v1.161.0');
|
|
2014
|
-
if (synapseBlocked) {
|
|
2015
|
-
return {
|
|
2016
|
-
step: null,
|
|
2017
|
-
blocked: `${org.slug}'s calls Matrix stack is blocked, not planned: ${synapseBlocked}`,
|
|
2018
|
-
};
|
|
2019
|
-
}
|
|
2020
|
-
const masBlocked = floatingTagBlocked('matrix-authentication-service', VERSIONS.mas, '1.24.0');
|
|
2021
|
-
if (masBlocked) {
|
|
2022
|
-
return {
|
|
2023
|
-
step: null,
|
|
2024
|
-
blocked: `${org.slug}'s calls Matrix stack is blocked, not planned: ${masBlocked}`,
|
|
2025
|
-
};
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2227
|
+
const live = byKey.get(key);
|
|
2228
|
+
if (live)
|
|
2229
|
+
claimed.add(key);
|
|
2230
|
+
const liveEnabled = live !== undefined && live.config.enabled !== false;
|
|
2231
|
+
if (org.calls.enabled && !liveEnabled)
|
|
2232
|
+
return { step: null, blocked: null };
|
|
2028
2233
|
const toggle = planHostingToggle('coolify', 'calls', org.calls.enabled, org, byKey, claimed, () => `provision ${org.slug}'s calls Matrix stack (Synapse + matrix-authentication-service)`, () => `park ${org.slug}'s calls Matrix stack (stops serving; data is retained, not deleted -- #7 corrections 1)`);
|
|
2029
|
-
// A held Synapse tag rides in the provisioning step; a parking step carries none.
|
|
2030
|
-
if (toggle && synapseTag && toggle.action.desired.enabled === true) {
|
|
2031
|
-
toggle.action.desired = { ...toggle.action.desired, synapseTag };
|
|
2032
|
-
}
|
|
2033
2234
|
return { step: toggle, blocked: null };
|
|
2034
2235
|
}
|
|
2035
2236
|
/**
|
|
2036
2237
|
* The org's own calls service (#145, part 2 of #128) -- `wtfalch/calls`'s
|
|
2037
|
-
* `packages/service`,
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2041
|
-
* authenticate a guest against, and a Matrix stack alone answers no browser,
|
|
2042
|
-
* so the two are declared as one on/off switch even though they are two
|
|
2043
|
-
* planner steps. Same on/off, park-not-delete shape as {@link planCalls}
|
|
2044
|
-
* (`planHostingToggle`, #7 corrections 1) -- turning it off parks the app,
|
|
2045
|
-
* never its database (`executor.ts`'s `coolifyCallsService`).
|
|
2238
|
+
* `packages/service`, wired to the shared LiveKit SFU (foundry#122/#127).
|
|
2239
|
+
* Same on/off shape as {@link planCalls} (`planHostingToggle`, #7
|
|
2240
|
+
* corrections 1) -- turning it off parks the app, never its database
|
|
2241
|
+
* (`executor.ts`'s `coolifyCallsService`).
|
|
2046
2242
|
*
|
|
2047
|
-
* #
|
|
2048
|
-
* stack
|
|
2049
|
-
*
|
|
2243
|
+
* #430: calls run transit-only (calls#93), so this no longer rides behind
|
|
2244
|
+
* the Matrix stack {@link planCalls} used to provision -- there is none to
|
|
2245
|
+
* ride behind any more -- and reports the shared SFU as missing itself
|
|
2246
|
+
* (#177) rather than relying on `planCalls`'s now-removed report.
|
|
2050
2247
|
*/
|
|
2051
2248
|
function planCallsService(org, byKey, claimed, callsReady, recordedIds, callsBlocked, tokenHost) {
|
|
2052
2249
|
const key = resourceKey('coolify', 'calls-service', org.id);
|
|
2053
|
-
if (org.calls.enabled && !byKey.get(key) && !callsReady) {
|
|
2054
|
-
return {
|
|
2250
|
+
if (org.calls.enabled && !byKey.get(key) && !callsReady && !ownsServices(org)) {
|
|
2251
|
+
return {
|
|
2252
|
+
step: null,
|
|
2253
|
+
blocked: `${org.slug}'s calls service is blocked, not planned: the platform owner's calls SFU (the platform's) is off`,
|
|
2254
|
+
};
|
|
2055
2255
|
}
|
|
2056
2256
|
// An own org's service names its own media server, so it waits for it even when already provisioned.
|
|
2057
2257
|
if (org.calls.enabled && ownsServices(org) && !callsReady) {
|
|
@@ -2867,6 +3067,137 @@ function planBouleClient(org, byKey, claimed) {
|
|
|
2867
3067
|
},
|
|
2868
3068
|
};
|
|
2869
3069
|
}
|
|
3070
|
+
/**
|
|
3071
|
+
* foundry#949: the fixed ZITADEL web client `kosmos`, once the company turns kosmos on. The same shape as
|
|
3072
|
+
* {@link planBouleClient} and {@link planAgoraClient}; `kosmosOn(org)` is false before any client is planned.
|
|
3073
|
+
*/
|
|
3074
|
+
function planKosmosClient(org, byKey, claimed) {
|
|
3075
|
+
if (!kosmosOn(org))
|
|
3076
|
+
return null;
|
|
3077
|
+
const key = resourceKey('zitadel', 'app', 'kosmos');
|
|
3078
|
+
const live = byKey.get(key);
|
|
3079
|
+
if (live)
|
|
3080
|
+
claimed.add(key);
|
|
3081
|
+
const desired = { appType: 'web' };
|
|
3082
|
+
if (!live) {
|
|
3083
|
+
return {
|
|
3084
|
+
action: { system: 'zitadel', kind: 'app', key: 'kosmos', desired },
|
|
3085
|
+
step: {
|
|
3086
|
+
description: `provision the web ZITADEL app 'kosmos' for ${org.slug}'s kosmos sign-in`,
|
|
3087
|
+
destroys: false,
|
|
3088
|
+
migrates: false,
|
|
3089
|
+
},
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
if (live.config.appType === 'web')
|
|
3093
|
+
return null;
|
|
3094
|
+
return {
|
|
3095
|
+
action: {
|
|
3096
|
+
system: 'zitadel',
|
|
3097
|
+
kind: 'app',
|
|
3098
|
+
key: 'kosmos',
|
|
3099
|
+
desired: { ...desired, recreate: true },
|
|
3100
|
+
},
|
|
3101
|
+
step: {
|
|
3102
|
+
description: `recreate ZITADEL app 'kosmos' for ${org.slug} as web (was ${String(live.config.appType)})`,
|
|
3103
|
+
destroys: true,
|
|
3104
|
+
migrates: false,
|
|
3105
|
+
},
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* foundry#949: the org's own kosmos, a per-org singleton keyed by the org id under its own `kosmos` kind, built
|
|
3110
|
+
* from https://github.com/wtfalch/kosmos's root Dockerfile (`executor.ts`'s `coolifyKosmos`), with a database of
|
|
3111
|
+
* its own. Unlike Boule and Agora, kosmos is set up only once the company turns it on (`kosmosOn(org)`); while
|
|
3112
|
+
* it is off, an org that never had it plans nothing, and a live app is parked (stopped, database kept), the
|
|
3113
|
+
* same rule a switched-off service always follows here.
|
|
3114
|
+
*
|
|
3115
|
+
* Once live, a step is planned again whenever the live app's sign-in env lacks a desired key or holds a
|
|
3116
|
+
* different value (`kosmos.ts`'s `desiredKosmosEnv`, the function the executor writes from). The only host
|
|
3117
|
+
* value is the issuer address (the one Agora and Boule sign in at); without it kosmos is blocked, not planned,
|
|
3118
|
+
* so every other step still applies.
|
|
3119
|
+
*/
|
|
3120
|
+
function planKosmos(org, byKey, claimed, recordedIds, issuer, otherSlugs) {
|
|
3121
|
+
if (!kosmosOn(org)) {
|
|
3122
|
+
// Switched off: park a live app (stop it, keep the database), like CMS and calls. A stopped app is
|
|
3123
|
+
// already parked, so the plan reaches empty.
|
|
3124
|
+
const parked = planHostingToggle('coolify', 'kosmos', false, org, byKey, claimed, () => `provision ${org.slug}'s kosmos`, () => `park ${org.slug}'s kosmos (stops serving; its database is retained, not deleted)`, org.id, (live) => !/^(exited|stopped)/.test(String(live.config.status ?? '')));
|
|
3125
|
+
return { step: parked, blocked: null };
|
|
3126
|
+
}
|
|
3127
|
+
const key = resourceKey('coolify', 'kosmos', org.id);
|
|
3128
|
+
const live = byKey.get(key);
|
|
3129
|
+
if (live)
|
|
3130
|
+
claimed.add(key);
|
|
3131
|
+
// Never truncated: Postgres cuts an identifier at 63 bytes, and a DNS label
|
|
3132
|
+
// over 63 is refused by Coolify and Cloudflare -- the same check `planBoule` makes.
|
|
3133
|
+
const tooLong = [
|
|
3134
|
+
[`${org.slug}-kosmos`, 'the kosmos host label and app name'],
|
|
3135
|
+
[`${org.slug}_kosmos_db_rt`, 'the kosmos database role'],
|
|
3136
|
+
].filter(([name]) => name.length > 63);
|
|
3137
|
+
if (tooLong.length > 0) {
|
|
3138
|
+
return {
|
|
3139
|
+
step: null,
|
|
3140
|
+
blocked: `${org.slug}'s kosmos is blocked, not planned: ${tooLong.map(([n, what]) => `${what} '${n}' is over 63 characters`).join(', ')}; shorten the slug`,
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
if (otherSlugs.includes(`${org.slug}-kosmos`)) {
|
|
3144
|
+
return {
|
|
3145
|
+
step: null,
|
|
3146
|
+
blocked: `${org.slug}'s kosmos is blocked, not planned: another declared org has the slug '${org.slug}-kosmos', which is this kosmos's hostname`,
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
if (!issuer) {
|
|
3150
|
+
return {
|
|
3151
|
+
step: null,
|
|
3152
|
+
blocked: `${org.slug}'s kosmos is blocked, not planned: the host supplies no issuer`,
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
const action = {
|
|
3156
|
+
system: 'coolify',
|
|
3157
|
+
kind: 'kosmos',
|
|
3158
|
+
key: org.id,
|
|
3159
|
+
desired: { issuer },
|
|
3160
|
+
};
|
|
3161
|
+
if (!live) {
|
|
3162
|
+
return {
|
|
3163
|
+
step: {
|
|
3164
|
+
action,
|
|
3165
|
+
step: { description: `provision ${org.slug}'s kosmos`, destroys: false, migrates: false },
|
|
3166
|
+
},
|
|
3167
|
+
blocked: null,
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
const liveEnv = (live.config.env ?? {});
|
|
3171
|
+
const { env } = desiredKosmosEnv(org, recordedIds, issuer);
|
|
3172
|
+
const drift = kosmosEnvDrift(env, liveEnv);
|
|
3173
|
+
// Switched back on while the app is still stopped: the step starts it (`coolifyKosmos` starts a stopped app).
|
|
3174
|
+
if (/^(exited|stopped)/.test(String(live.config.status ?? ''))) {
|
|
3175
|
+
return {
|
|
3176
|
+
step: {
|
|
3177
|
+
action,
|
|
3178
|
+
step: {
|
|
3179
|
+
description: `start ${org.slug}'s kosmos again (it was parked)`,
|
|
3180
|
+
destroys: false,
|
|
3181
|
+
migrates: false,
|
|
3182
|
+
},
|
|
3183
|
+
},
|
|
3184
|
+
blocked: null,
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
if (drift.length === 0)
|
|
3188
|
+
return { step: null, blocked: null };
|
|
3189
|
+
return {
|
|
3190
|
+
step: {
|
|
3191
|
+
action,
|
|
3192
|
+
step: {
|
|
3193
|
+
description: `update ${org.slug}'s kosmos config (${drift.join(', ')} missing or different)`,
|
|
3194
|
+
destroys: false,
|
|
3195
|
+
migrates: false,
|
|
3196
|
+
},
|
|
3197
|
+
},
|
|
3198
|
+
blocked: null,
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
2870
3201
|
/**
|
|
2871
3202
|
* C3: which credentials the org's Boule is given, and why one is not. Archon's credential goes to every
|
|
2872
3203
|
* Boule the host can mint it for; the customers directory credential only while `customers.enabled`. Each
|
|
@@ -2919,7 +3250,7 @@ function planBouleCredentials(org, recordedIds, host, canHoldSecrets, nowMs) {
|
|
|
2919
3250
|
* address (the one Agora signs in at); without it the Boule is blocked, not
|
|
2920
3251
|
* planned, so every other step still applies.
|
|
2921
3252
|
*/
|
|
2922
|
-
function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonUrl, readiness, credentials) {
|
|
3253
|
+
function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonUrl, readiness, credentials, customerGrantsShape = null) {
|
|
2923
3254
|
const key = resourceKey('coolify', 'boule', org.id);
|
|
2924
3255
|
const live = byKey.get(key);
|
|
2925
3256
|
if (live)
|
|
@@ -2991,6 +3322,9 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
2991
3322
|
platformOwner,
|
|
2992
3323
|
archonCredential: credentials.archonCredential === true,
|
|
2993
3324
|
customersDirectory: credentials.customersDirectory === true,
|
|
3325
|
+
// Only an org that declares the flag carries the key, so every other plan is unchanged.
|
|
3326
|
+
...(credentials.customerGrants === true ? { customerGrants: true } : {}),
|
|
3327
|
+
...(credentials.registrar === true ? { registrar: true } : {}),
|
|
2994
3328
|
},
|
|
2995
3329
|
};
|
|
2996
3330
|
if (!live) {
|
|
@@ -3020,6 +3354,26 @@ function planBoule(org, byKey, claimed, recordedIds, issuer, otherSlugs, archonU
|
|
|
3020
3354
|
if (!drift.includes(key))
|
|
3021
3355
|
drift.push(key);
|
|
3022
3356
|
}
|
|
3357
|
+
// F13: the grants entry says what the flagged entries say (credentials by presence only). Taken away only
|
|
3358
|
+
// once no entry declares the flag and foundry wrote it; held credentials of dropped entries are forgotten.
|
|
3359
|
+
if (customerGrantsShape) {
|
|
3360
|
+
if (!customerGrantsShapeMatches(liveEnv[CUSTOMER_GRANTS_ENV], customerGrantsShape)) {
|
|
3361
|
+
drift.push(CUSTOMER_GRANTS_ENV);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
else if (customerGrantsEntries(org).length === 0 &&
|
|
3365
|
+
recordedIds[CUSTOMER_GRANTS_WRITTEN_SCOPE_KEY] &&
|
|
3366
|
+
liveEnv[CUSTOMER_GRANTS_ENV] !== undefined) {
|
|
3367
|
+
drift.push(CUSTOMER_GRANTS_ENV);
|
|
3368
|
+
}
|
|
3369
|
+
if (customerGrantsToDrop(org, recordedIds).length > 0 && !drift.includes(CUSTOMER_GRANTS_ENV)) {
|
|
3370
|
+
drift.push(CUSTOMER_GRANTS_ENV);
|
|
3371
|
+
}
|
|
3372
|
+
// T3: the registrar key by presence and by which mint Boule got; taken away once customers are off.
|
|
3373
|
+
for (const key of bouleRegistrarDrift(org, recordedIds, liveEnv, credentials.registrar === true, credentials.registrarKept === true)) {
|
|
3374
|
+
if (!drift.includes(key))
|
|
3375
|
+
drift.push(key);
|
|
3376
|
+
}
|
|
3023
3377
|
// The token is compared, never planned with its value.
|
|
3024
3378
|
if (mailOn &&
|
|
3025
3379
|
bouleMailTokenDrift(liveEnv[BOULE_MAIL_TOKEN_ENV], live.config.appMailToken)) {
|
|
@@ -3191,7 +3545,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
3191
3545
|
// right now, and gating this on `mailHost` let a platform-apex org with an
|
|
3192
3546
|
// already-recorded domain take the unprotected path the moment platform
|
|
3193
3547
|
// mail went back to off.
|
|
3194
|
-
if (entryKey === mainEntry(org.hosting)?.key) {
|
|
3548
|
+
if (entryKey === mainEntry(org.hosting)?.key || entry?.uses?.mail) {
|
|
3195
3549
|
const mailDomain = recordedIds[MAIL_DOMAIN_SCOPE_KEY];
|
|
3196
3550
|
const platformApex = readiness.platformApex;
|
|
3197
3551
|
const platformLettermintProjectId = readiness.platformLettermintProjectId;
|
|
@@ -3519,11 +3873,53 @@ function planApps(org, byKey, claimed, recordedIds, plannedSteps, authIssuer, re
|
|
|
3519
3873
|
if (org.hosting.entries.some((e) => e.key === key && e.workerApp))
|
|
3520
3874
|
continue;
|
|
3521
3875
|
const app = planApp(org, key, byKey, claimed, recordedIds, plannedSteps, authIssuer, readiness);
|
|
3876
|
+
// An app built from the company's own account is created through its GitHub source: with no source
|
|
3877
|
+
// recorded or planned, a new app waits (blocked, not planned) instead of failing at create.
|
|
3878
|
+
const sourceEntry = org.hosting.entries.find((e) => e.key === key);
|
|
3879
|
+
if (app.step &&
|
|
3880
|
+
app.step.action.desired.enabled !== false &&
|
|
3881
|
+
sourceEntry &&
|
|
3882
|
+
companyGithubFor(org.hosting, sourceEntry.repo) &&
|
|
3883
|
+
!recordedIds[githubSourceResourceKey(org.id)] &&
|
|
3884
|
+
!recordedIds[`coolify:app:${appStepKey(org, key)}`] &&
|
|
3885
|
+
!plannedSteps.some((p) => p.action.system === 'coolify' && p.action.kind === 'github-source')) {
|
|
3886
|
+
blocked.push({
|
|
3887
|
+
description: `${org.slug}'s app for entry '${key}' is blocked, not planned: it is built from the company's GitHub account and its GitHub source is not made yet`,
|
|
3888
|
+
key: 'github-source',
|
|
3889
|
+
});
|
|
3890
|
+
blocked.push(...app.blocked);
|
|
3891
|
+
continue;
|
|
3892
|
+
}
|
|
3893
|
+
// The entry's workspaces key is made and held by this step; a store that cannot put leaves the step failing every run.
|
|
3894
|
+
if (app.step &&
|
|
3895
|
+
app.step.action.desired.enabled !== false &&
|
|
3896
|
+
org.hosting.entries.some((e) => e.key === key && e.uses?.auth?.workspaces) &&
|
|
3897
|
+
readiness.canHoldSecrets !== true) {
|
|
3898
|
+
blocked.push({
|
|
3899
|
+
description: `${org.slug}'s app for entry '${key}' is blocked, not planned: its workspaces key needs a held-key store that can put, and the host has none`,
|
|
3900
|
+
key: `coolify:app:${key}:workspaces-key`,
|
|
3901
|
+
});
|
|
3902
|
+
blocked.push(...app.blocked);
|
|
3903
|
+
continue;
|
|
3904
|
+
}
|
|
3905
|
+
// The entry's customer-grants credential is made and held by this step; a store that cannot put leaves it failing every run.
|
|
3906
|
+
if (app.step &&
|
|
3907
|
+
app.step.action.desired.enabled !== false &&
|
|
3908
|
+
org.hosting.entries.some((e) => e.key === key && e.uses?.customerGrants) &&
|
|
3909
|
+
readiness.canHoldSecrets !== true) {
|
|
3910
|
+
blocked.push({
|
|
3911
|
+
description: `${org.slug}'s app for entry '${key}' is blocked, not planned: its customer grants credential needs a held-key store that can put, and the host has none`,
|
|
3912
|
+
key: `coolify:app:${key}:customer-grants`,
|
|
3913
|
+
});
|
|
3914
|
+
blocked.push(...app.blocked);
|
|
3915
|
+
continue;
|
|
3916
|
+
}
|
|
3522
3917
|
// The executor pushes the issuer the plan read, the way Agora gets its host values.
|
|
3523
3918
|
// The same availability the plan's marker used: the executor records its
|
|
3524
3919
|
// marker from this, never from its own port, so the two cannot disagree.
|
|
3525
3920
|
const usesSecrets = org.hosting.entries.some((e) => e.key === key &&
|
|
3526
|
-
|
|
3921
|
+
(e.uses?.customerGrants === true ||
|
|
3922
|
+
Object.values(e.env).some((v) => typeof v !== 'string' && (v.from === 'sharedSecret' || v.from === 'heldSecret'))));
|
|
3527
3923
|
if (usesSecrets && app.step && app.step.action.desired.enabled !== false) {
|
|
3528
3924
|
const entry = org.hosting.entries.find((e) => e.key === key);
|
|
3529
3925
|
app.step.action.desired = {
|
|
@@ -3538,15 +3934,16 @@ function planApps(org, byKey, claimed, recordedIds, plannedSteps, authIssuer, re
|
|
|
3538
3934
|
if (signsIn)
|
|
3539
3935
|
app.step.action.desired = { ...app.step.action.desired, authIssuer };
|
|
3540
3936
|
}
|
|
3541
|
-
// #491: only the `app` entry's
|
|
3542
|
-
// (`executor.ts`'s own
|
|
3937
|
+
// #491: only the `app` entry's step and an entry with `uses.mail` run
|
|
3938
|
+
// `ensureAppLettermintMail` (`executor.ts`'s own gate). Unlike
|
|
3543
3939
|
// `secretSources`/`authIssuer` above, these two are set unconditionally
|
|
3544
3940
|
// -- `null` when no platform is declared, never omitted -- because
|
|
3545
3941
|
// `executor.ts` treats the key being *absent* as a stale plan predating
|
|
3546
3942
|
// this field and refuses to run (review round 2, finding 3): an omitted
|
|
3547
3943
|
// key here would read identically to a stale plan and silently fall
|
|
3548
3944
|
// back to the unprotected pre-#491 path instead.
|
|
3549
|
-
|
|
3945
|
+
const sendsMail = key === mainKey || org.hosting.entries.some((e) => e.key === key && e.uses?.mail);
|
|
3946
|
+
if (sendsMail && app.step && app.step.action.desired.enabled !== false) {
|
|
3550
3947
|
app.step.action.desired = {
|
|
3551
3948
|
...app.step.action.desired,
|
|
3552
3949
|
platformApex: readiness.platformApex,
|
|
@@ -3558,17 +3955,53 @@ function planApps(org, byKey, claimed, recordedIds, plannedSteps, authIssuer, re
|
|
|
3558
3955
|
// edge: blocked, not planned, while edge is not applied.
|
|
3559
3956
|
const appReason = edgeNeeded(readiness, ['zones', 'dns']);
|
|
3560
3957
|
if (appReason &&
|
|
3561
|
-
|
|
3958
|
+
sendsMail &&
|
|
3562
3959
|
app.step &&
|
|
3563
3960
|
app.step.action.desired.enabled !== false &&
|
|
3564
3961
|
recordedIds[MAIL_DOMAIN_SCOPE_KEY]) {
|
|
3565
|
-
blocked.push(edgeBlockedItem(org, 'app'
|
|
3962
|
+
blocked.push(edgeBlockedItem(org, key === mainKey ? 'app' : `${key} app`, key === mainKey ? 'coolify:app' : `coolify:app:${key}`, appReason));
|
|
3566
3963
|
blocked.push(...app.blocked);
|
|
3567
3964
|
continue;
|
|
3568
3965
|
}
|
|
3966
|
+
// A workspace entry's live app is made from the commit its workspace runs: none is made before one is known.
|
|
3967
|
+
const goLive = org.hosting.entries.find((e) => e.key === key && e.workspace !== undefined && e.workerApp === undefined);
|
|
3968
|
+
if (goLive &&
|
|
3969
|
+
app.step &&
|
|
3970
|
+
app.step.action.desired.enabled !== false &&
|
|
3971
|
+
!recordedIds[`coolify:app:${appStepKey(org, key)}`]) {
|
|
3972
|
+
const commit = goLive.liveCommit ?? recordedIds[wsKey.commit(key)];
|
|
3973
|
+
if (!commit) {
|
|
3974
|
+
blocked.push({
|
|
3975
|
+
description: `${org.slug}'s app for entry '${key}' is blocked, not planned: its workspace has not deployed a commit yet, and the live app is made from that commit`,
|
|
3976
|
+
key: `workspace:${key}:golive`,
|
|
3977
|
+
});
|
|
3978
|
+
blocked.push(...app.blocked);
|
|
3979
|
+
continue;
|
|
3980
|
+
}
|
|
3981
|
+
app.step.action.desired = { ...app.step.action.desired, workspaceCommit: commit };
|
|
3982
|
+
app.step.step.description = `${app.step.step.description} from the commit its workspace runs (${commit.slice(0, 12)})`;
|
|
3983
|
+
}
|
|
3569
3984
|
if (app.step)
|
|
3570
3985
|
steps.push(app.step);
|
|
3571
3986
|
blocked.push(...app.blocked);
|
|
3987
|
+
// A promote: the live app exists and runs another commit than the entry's `liveCommit`.
|
|
3988
|
+
if (goLive?.liveCommit !== undefined &&
|
|
3989
|
+
recordedIds[`coolify:app:${appStepKey(org, key)}`] &&
|
|
3990
|
+
recordedIds[wsKey.liveCommit(key)] !== goLive.liveCommit) {
|
|
3991
|
+
steps.push({
|
|
3992
|
+
action: {
|
|
3993
|
+
system: 'coolify',
|
|
3994
|
+
kind: 'promote',
|
|
3995
|
+
key: appStepKey(org, key),
|
|
3996
|
+
desired: { entry: key, commit: goLive.liveCommit },
|
|
3997
|
+
},
|
|
3998
|
+
step: {
|
|
3999
|
+
description: `put ${org.slug}'s live app for entry '${key}' on commit ${goLive.liveCommit.slice(0, 12)} and deploy it`,
|
|
4000
|
+
destroys: false,
|
|
4001
|
+
migrates: false,
|
|
4002
|
+
},
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
3572
4005
|
}
|
|
3573
4006
|
return { steps, blocked };
|
|
3574
4007
|
}
|
|
@@ -3713,6 +4146,8 @@ function planTunnel(org, byKey, claimed, recordedIds, platformApex) {
|
|
|
3713
4146
|
(org.site.enabled && Boolean(org.domain)) ||
|
|
3714
4147
|
(org.calls.enabled && org.calls.usesDomain && Boolean(org.domain)) ||
|
|
3715
4148
|
Boolean(org.domain) ||
|
|
4149
|
+
// F4 (#925): apps on, the tunnel carries the route to the company's Postgres server for Hyperdrive.
|
|
4150
|
+
appsOn(org) ||
|
|
3716
4151
|
// A company without a domain still gets a tunnel of its own for an entry at `<slug>.oikos.host`.
|
|
3717
4152
|
entryRoutes(org, recordedIds).length > 0;
|
|
3718
4153
|
const toggled = planHostingToggle('cloudflare', 'tunnel', desired, org, byKey, claimed, () => `provision a Cloudflare tunnel for ${org.slug}'s app`, () => `park ${org.slug}'s Cloudflare tunnel (configuration is retained, not deleted -- #7 corrections 1)`, org.id,
|
|
@@ -3764,10 +4199,13 @@ function planTunnel(org, byKey, claimed, recordedIds, platformApex) {
|
|
|
3764
4199
|
sortedNames(tunnelDnsHostnames(org, org.domain, loginHost));
|
|
3765
4200
|
// The entry names in the platform's oikos.host zone are recorded apart from the company's own zone.
|
|
3766
4201
|
const oikosStale = (recordedIds[CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY] ?? '') !==
|
|
3767
|
-
entryOikosHostnames(org).join(',');
|
|
4202
|
+
entryOikosHostnames(org, recordedIds).join(',');
|
|
4203
|
+
const appsStale = (recordedIds[CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY] ?? '') !==
|
|
4204
|
+
entryAppsHostnames(org, recordedIds).join(',');
|
|
3768
4205
|
const dnsStale = (org.domain !== null && recordedIds[CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY] !== org.domain) ||
|
|
3769
4206
|
edgeChanged ||
|
|
3770
|
-
oikosStale
|
|
4207
|
+
oikosStale ||
|
|
4208
|
+
appsStale;
|
|
3771
4209
|
// An entry's hostname goes straight to its container, so a changed port, key or name, or an ingress
|
|
3772
4210
|
// written before entries were routed that way, needs the ingress written again. Nothing recorded
|
|
3773
4211
|
// equals no routes.
|
|
@@ -3962,7 +4400,7 @@ function planEntryClients(org, byKey, claimed) {
|
|
|
3962
4400
|
* record blocks it (rule 2). A foreign broker entry the last write found
|
|
3963
4401
|
* invalid is listed as a warning: the broker drops it, foundry keeps it.
|
|
3964
4402
|
*/
|
|
3965
|
-
export function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired) {
|
|
4403
|
+
export function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired, brokerWorkspacesDatabase) {
|
|
3966
4404
|
const steps = [];
|
|
3967
4405
|
const blocked = [];
|
|
3968
4406
|
for (const entry of org.hosting.entries) {
|
|
@@ -3985,6 +4423,14 @@ export function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired) {
|
|
|
3985
4423
|
});
|
|
3986
4424
|
continue;
|
|
3987
4425
|
}
|
|
4426
|
+
// The broker does not start once an entry declares workspaces without its database, and then no one signs in.
|
|
4427
|
+
if (auth.workspaces && !brokerWorkspacesDatabase) {
|
|
4428
|
+
blocked.push({
|
|
4429
|
+
description: `${at} is blocked, not planned: the broker has no ${BROKER_WORKSPACES_DATABASE_ENV}; hold the connection string of its workspaces database at ${workspacesDatabaseRef(org)}, then apply again`,
|
|
4430
|
+
key,
|
|
4431
|
+
});
|
|
4432
|
+
continue;
|
|
4433
|
+
}
|
|
3988
4434
|
// Auth ADR 0002: a broker given an `mcp` entry before it holds its database and secret exits at boot.
|
|
3989
4435
|
// A company's own sign-in step sets them and records it; a shared org's broker is the platform's, whose
|
|
3990
4436
|
// marker this plan cannot see, so `writeBrokerEntry` refuses there until the platform step has run.
|
|
@@ -4205,10 +4651,12 @@ const signInMetadataBlocked = (org) => ({
|
|
|
4205
4651
|
* gates (`planGates`) come first. Under a `'*'` gate, a bypass on each name
|
|
4206
4652
|
* foundry owns comes before them, so no owned name is ever gated.
|
|
4207
4653
|
*/
|
|
4208
|
-
function planAccess(org, recordedIds, providerId, platformHostnames) {
|
|
4209
|
-
const { steps: gates, blocked } = planGates(org, recordedIds, providerId);
|
|
4654
|
+
function planAccess(org, recordedIds, providerId, platformHostnames, declaredOrg = org) {
|
|
4655
|
+
const { steps: gates, blocked } = planGates(org, recordedIds, providerId, declaredOrg);
|
|
4656
|
+
// F11: a machine path of a gated entry is let through its gate, and a removed one is closed again.
|
|
4657
|
+
const machine = planMachineBypasses(org, recordedIds, declaredOrg);
|
|
4210
4658
|
if (!org.hosting.enabled)
|
|
4211
|
-
return { steps: gates, blocked };
|
|
4659
|
+
return { steps: [...gates, ...machine.steps], blocked, notes: machine.notes };
|
|
4212
4660
|
const owned = (hasWildcardGate(org) ? [...lockedHostnames(org, platformHostnames)].sort() : [])
|
|
4213
4661
|
.filter((hostname) => !recordedIds[resourceKey('cloudflare', 'access', appStepKey(org, hostname))])
|
|
4214
4662
|
.map((hostname) => ({
|
|
@@ -4239,7 +4687,7 @@ function planAccess(org, recordedIds, providerId, platformHostnames) {
|
|
|
4239
4687
|
migrates: false,
|
|
4240
4688
|
},
|
|
4241
4689
|
}));
|
|
4242
|
-
return { steps: [...owned, ...gates, ...rules], blocked };
|
|
4690
|
+
return { steps: [...owned, ...gates, ...machine.steps, ...rules], blocked, notes: machine.notes };
|
|
4243
4691
|
}
|
|
4244
4692
|
/** The plan description for one `hosting.access` rule, by kind (#246). */
|
|
4245
4693
|
function accessRuleDescription(org, rule) {
|