@wtfalch/foundry 0.61.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +3 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/modules/sign-in.js +1 -1
- package/dist/native-build.d.ts +50 -0
- package/dist/native-build.js +72 -0
- package/dist/org.d.ts +22 -3
- package/dist/org.js +25 -2
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/archon-blue-green.d.ts +5 -0
- package/dist/plan/archon-blue-green.js +2 -0
- package/dist/plan/blue-green-apply.d.ts +5 -0
- package/dist/plan/blue-green-apply.js +197 -22
- package/dist/plan/blue-green.d.ts +80 -9
- package/dist/plan/blue-green.js +115 -14
- package/dist/plan/boule.d.ts +1 -1
- package/dist/plan/boule.js +2 -0
- package/dist/plan/broker-entries.d.ts +9 -0
- package/dist/plan/broker-entries.js +29 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +4 -1
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +184 -63
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +8 -0
- package/dist/plan/executor.d.ts +111 -19
- package/dist/plan/executor.js +578 -72
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/jobs-scheduler-http.d.ts +2 -0
- package/dist/plan/jobs-scheduler-http.js +11 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/locked-hostnames.js +5 -2
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -1
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-limits.d.ts +12 -0
- package/dist/plan/own-limits.js +2 -0
- package/dist/plan/plan-apply.d.ts +51 -1
- package/dist/plan/plan-apply.js +298 -25
- package/dist/plan/platform-diff.d.ts +6 -0
- package/dist/plan/platform-diff.js +74 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +162 -1
- package/dist/plan/platform-registry-keys.d.ts +58 -0
- package/dist/plan/platform-registry-keys.js +69 -0
- package/dist/plan/platform-registry.d.ts +67 -0
- package/dist/plan/platform-registry.js +298 -0
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +5 -2
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/release-host.d.ts +31 -2
- package/dist/plan/release-host.js +114 -6
- package/dist/plan/stage-copy-host.d.ts +104 -0
- package/dist/plan/stage-copy-host.js +259 -0
- package/dist/plan/stage-copy.js +44 -0
- package/dist/plan/stage-env.d.ts +9 -0
- package/dist/plan/stage-env.js +38 -0
- package/dist/plan/stage-guards.d.ts +57 -1
- package/dist/plan/stage-guards.js +106 -0
- package/dist/plan/stage-isolation.js +56 -0
- package/dist/plan/stage-services.d.ts +93 -0
- package/dist/plan/stage-services.js +153 -0
- package/dist/plan/stage-signin.d.ts +152 -0
- package/dist/plan/stage-signin.js +133 -0
- package/dist/plan/stage-sink.d.ts +2 -0
- package/dist/plan/stage-sink.js +6 -1
- package/dist/platform.d.ts +7 -1
- package/dist/platform.js +1 -0
- package/dist/release/decide.d.ts +12 -0
- package/dist/release/decide.js +37 -2
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +71 -1
- package/dist/release/run.js +92 -8
- package/dist/release/state.d.ts +14 -1
- package/dist/release/state.js +43 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/release/types.js +2 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +11 -2
- package/dist/store/postgres.js +21 -0
- package/dist/store/schema.d.ts +87 -0
- package/dist/store/schema.js +10 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
package/dist/plan/executor.js
CHANGED
|
@@ -5,30 +5,31 @@ import { EdgeApiError } from '@wtfalch/edge';
|
|
|
5
5
|
import { coolifyImageFor } from '../bootstrap/versions.js';
|
|
6
6
|
import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
7
7
|
import { resolveCallsSfuUrls } from '../calls-sfu.js';
|
|
8
|
-
import {
|
|
8
|
+
import { NATIVE_BUILD_KEY_BYTES, NATIVE_BUILD_KEY_ENV } from '../native-build.js';
|
|
9
|
+
import { AI_ENTRY_PERMISSIONS, APPS_DOMAIN, APP_ENTRY_KEY, FILES_ENTRY_PERMISSIONS, FULL_COMMIT_SHA, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, appsOn, companyGithubFor, filesCollectionEnvName, heldEnvRef, isLiveEntry, isMainEntry, kosmosOn, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownsServices, sharedSecretRef, } from '../org.js';
|
|
9
10
|
import { adoptedKeysUnreadable, parseAdoptSnapshot } from './adopt.js';
|
|
10
11
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, DESKTOP_AUTH_PORT, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatCallsApplied, chatHasUpstreams, chatIntrospectionAppName, desiredAgoraEnv, desktopAuthInternalName, entryClientIdScopeKey, } from './agora.js';
|
|
11
12
|
import { APPS_DATABASE_ADMIN_URL_ENV, applyAppsDbRoute, retireAppsDbRoute, } from './apps-db-route.js';
|
|
12
13
|
import { APPS_ZONES_APPLIED_SCOPE_KEY, applyAppsZones, retireAppsZones } from './apps-zones.js';
|
|
13
14
|
import { applyBlueGreenConnectors, applyBlueGreenServer, applyBlueGreenWeights, applyWorkspaceServer, } from './blue-green-apply.js';
|
|
14
|
-
import { actionsKeyName, bgKey, blueGreenSubjects, colorAppName, healthTokenName, liveAppNameFor, mintActionsKey, mintPinKey, pinKeyName, splitServers, } from './blue-green.js';
|
|
15
|
+
import { actionsKeyName, bgKey, blueGreenSubjects, colorAppName, healthTokenName, liveAppNameFor, mintActionsKey, mintPinKey, parseGuards, pinKeyName, splitServers, } from './blue-green.js';
|
|
15
16
|
import { BouleOwnersError, httpBouleOwners } from './boule-owners-http.js';
|
|
16
17
|
import { BOULE_ARCHON_CREDENTIAL_ENV, BOULE_ARCHON_PUSHED_AT_SCOPE_KEY, BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_CUSTOMERS_CREDENTIAL_ENV, BOULE_CUSTOMERS_ORG_ENV, BOULE_DATABASE_SCOPE_KEY, BOULE_DEPLOYED_SCOPE_KEY, BOULE_OWNER_SEED_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, BOULE_TEST_CUSTOMERS_ORG_ENV, CUSTOMERS_DIRECTORY_USER_SCOPE_KEY, CUSTOMERS_ROOM_METADATA_KEY, CUSTOMERS_ROOM_SCOPE_KEY, CUSTOMERS_SIGN_IN_METADATA_KEY, CUSTOMERS_SIGN_IN_SCOPE_KEY, CUSTOMERS_SIGN_UP_SCOPE_KEY, archonCredentialRenewDue, bouleArchonCredentialRef, bouleEnvDrift, bouleHostnameFor, bouleMailWanted, bouleOwnerRefusedScopeKey, bouleOwnerSeededScopeKey, bouleOwnerSeeds, customersDirectoryRef, customersOrgIdOf, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
17
|
-
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, entryMcp, entryMcpProjectScopeKey, mcpCallbackUri, wantedBrokerEntry, writeBrokerEntry, } from './broker-entries.js';
|
|
18
|
+
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, entryMcp, entryMcpProjectScopeKey, mcpCallbackUri, removeBrokerEntry, wantedBrokerEntry, writeBrokerEntry, } from './broker-entries.js';
|
|
18
19
|
import { AUTH_REGISTRAR_KEY_ENV, AUTH_REGISTRAR_URL_ENV, REGISTRAR_APPLIED_SCOPE_KEY, REGISTRAR_DELIVERED_SCOPE_KEY, REGISTRAR_DEPLOY_WAITS_SCOPE_KEY, REGISTRAR_MINTED_SCOPE_KEY, REGISTRAR_REMOVAL_HELD_SCOPE_KEY, appliedRegistrarOrganization, ensureRegistrarKey, registrarAppliedValue, registrarRef, registrarWanted, wantedRegistrar, writeRegistrar, } from './broker-registrar.js';
|
|
19
20
|
import { BROKER_WORKSPACES_DATABASE_SCOPE_KEY, canHoldWorkspacesDatabase, ensureBrokerWorkspacesDatabase, } from './broker-workspaces-database.js';
|
|
20
21
|
import { recordBuiltCommit } from './build-record.js';
|
|
21
22
|
import { CallError, withTimeout as withLabelledTimeout } from './call-timeout.js';
|
|
22
|
-
import { MAS_TAG, SYNAPSE_IMAGE, SYNAPSE_TAG, callsMasConfigYaml,
|
|
23
|
+
import { MAS_TAG, SYNAPSE_IMAGE, SYNAPSE_TAG, callsMasConfigYaml, callsSynapseHomeserverYaml, masDockerfile, mintCallsMatrixSecrets, parseCallsDbUrl, } from './calls-matrix.js';
|
|
23
24
|
import { CHAT_MAS_PORT, CHAT_REPO, CHAT_SERVICE_DOCKERFILE, CHAT_SERVICE_PORT, CHAT_SYNAPSE_DOCKERFILE, CHAT_SYNAPSE_PORT, assertChatServerName, chatInternalNames, chatMasConfigYaml, chatRekeyedInternalNames, chatServerName, chatServiceEnvs, chatSynapseHomeserverYaml, chatUpstreams, } from './chat-matrix.js';
|
|
24
25
|
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
25
|
-
import { BACKUP_POSTGRES_DATABASES_SCOPE_KEY, BACKUP_SCOPE_PREFIX, COMPANY_BACKUP_DATABASE_NAMES, backupScopeKey, } from './company-backups.js';
|
|
26
|
+
import { BACKUP_POSTGRES_DATABASES_SCOPE_KEY, BACKUP_SCOPE_PREFIX, COMPANY_BACKUP_DATABASE_NAMES, backupScopeKey, companyDatabaseKeys, } from './company-backups.js';
|
|
26
27
|
import { LOGIN_COOKIE_DOMAIN_SCOPE_KEY } from './cookie-domain.js';
|
|
27
28
|
import { previewsOn } from './coolify-previews.js';
|
|
28
29
|
import { CORE_FILES_COLLECTION_NAME, FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY, } from './core-files.js';
|
|
29
30
|
import { waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
|
|
30
31
|
import { DESKTOP_AUTH_APP_SCOPE_KEY, DESKTOP_AUTH_DEPLOYED_SCOPE_KEY, DESKTOP_AUTH_DOCKERFILE, DESKTOP_AUTH_HOME, DESKTOP_AUTH_ORIGIN_SCOPE_KEY, DESKTOP_AUTH_READY_PATH, DESKTOP_AUTH_READY_SCOPE_KEY, DESKTOP_AUTH_REDIRECT_SCOPE_KEY, DESKTOP_AUTH_REVISION_SCOPE_KEY, desktopAuthCallbackUri, desktopAuthEnv, desktopAuthInputs, desktopAuthRedirectUnsupported, desktopAuthRevision, } from './desktop-auth.js';
|
|
31
|
-
import { signInCallbackAppliedValue, signInRedirectUris, unionRedirectUris, zitadelAppDesktopCallbackAppliedScopeKey, } from './desktop-callback.js';
|
|
32
|
+
import { desktopCallbackUri, signInCallbackAppliedValue, signInRedirectUris, unionRedirectUris, zitadelAppDesktopCallbackAppliedScopeKey, } from './desktop-callback.js';
|
|
32
33
|
import { PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, onPlatformNetwork, placed, placedByScope, placedOnPlatform, } from './destinations.js';
|
|
33
34
|
import { DEFAULT_AI_LIMIT_MICROS } from './diff.js';
|
|
34
35
|
import { createEdgeAccess, isNotFound as isEdgeNotFound, } from './edge-access.js';
|
|
@@ -36,7 +37,7 @@ import { EDGE_DNS_WRITE_CONCURRENCY, createEdgeDns, limiter, } from './edge-dns.
|
|
|
36
37
|
import { createEdgeRateLimits, } from './edge-ratelimits.js';
|
|
37
38
|
import { createEdgeTunnels } from './edge-tunnels.js';
|
|
38
39
|
import { createEdgeZones } from './edge-zones.js';
|
|
39
|
-
import { AI_ENTRY_RENEW_WINDOW_DAYS, entryAiAppliedScopeKey, entryAiClampedScopeKey, entryAiExpiresScopeKey, entryAiPendingExpiresScopeKey, entryAiRetiredScopeKey, entryAiUndeliveredScopeKey, entryAiUnrevokedScopeKey, entryAiWebhookPendingScopeKey, entryAiWrittenScopeKey, parseIdList, } from './entry-ai-key.js';
|
|
40
|
+
import { AI_ENTRY_KEY_DAYS, AI_ENTRY_RENEW_WINDOW_DAYS, entryAiAppliedScopeKey, entryAiClampedScopeKey, entryAiExpiresScopeKey, entryAiPendingExpiresScopeKey, entryAiRetiredScopeKey, entryAiUndeliveredScopeKey, entryAiUnrevokedScopeKey, entryAiWebhookPendingScopeKey, entryAiWrittenScopeKey, parseIdList, } from './entry-ai-key.js';
|
|
40
41
|
import { applyEntryAudit } from './entry-audit.js';
|
|
41
42
|
import { CUSTOMER_GRANTS_CREDENTIAL_ENV, CUSTOMER_GRANTS_ENV, CUSTOMER_GRANTS_WRITTEN_SCOPE_KEY, customerGrantsEntries, customerGrantsJson, customerGrantsMintedScopeKey, customerGrantsRef, customerGrantsToDrop, ensureCustomerGrantsCredential, wantedCustomerGrants, } from './entry-customer-grants.js';
|
|
42
43
|
import { BOULE_EDGE_ENTRY_KEY, EDGE_ENTRY_KEY_DAYS, EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY, EDGE_SLUG_APPLIED_SCOPE_KEY, ENTRY_EDGE_KEY_DELIVERY_ATTEMPTS, bouleEdgeKeyWanted, edgeKeyAppRecordKey, edgeKeyDomain, entryEdgeMintedAtScopeKey, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
@@ -50,6 +51,7 @@ import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSett
|
|
|
50
51
|
import { PROXY_LABEL_TOKEN_REF } from './held-key-refs.js';
|
|
51
52
|
import { appHostname, cmsStudioHostnameFor, coolifyTunnelDomains, entryHostnames, siteHostnameFor, tunnelDnsHostnames, tunnelIngressHostnames, workspaceHost, } from './hostnames.js';
|
|
52
53
|
import { KEYS_REGISTRAR_DELIVERY_ATTEMPTS, KEYS_REGISTRAR_KEY_DAYS, KEYS_REGISTRAR_KEY_ENV, KEYS_REGISTRAR_MINTED_AT_SCOPE_KEY, KEYS_REGISTRAR_MINT_STARTED_SCOPE_KEY, KEYS_REGISTRAR_SCOPE_KEY, KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY, KEYS_REGISTRAR_URL_APPLIED_SCOPE_KEY, KEYS_REGISTRAR_URL_ENV, } from './keys-registrar.js';
|
|
54
|
+
import { KOSMOS_CLIENT_ID_SCOPE_KEY, KOSMOS_DATABASE_SCOPE_KEY, KOSMOS_DEPLOYED_SCOPE_KEY, desiredKosmosEnv, kosmosEnvDrift, kosmosHostnameFor, } from './kosmos.js';
|
|
53
55
|
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
54
56
|
import { machineBypassScopeKey } from './machine-paths.js';
|
|
55
57
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
@@ -68,7 +70,7 @@ import { ownSfuUrls, provisionSfuApp } from './own-sfu.js';
|
|
|
68
70
|
import { JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, } from './platform-diff.js';
|
|
69
71
|
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, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appFolderDiffers, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, orgDeclaresMcp, resolveAppEnv, serviceDbEnvAppliedScopeKey, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
70
72
|
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_AUTH_PROJECT, PLATFORM_MAIL_SERVER_SCOPE_KEY, defaultStalwartForInstance, filesDepsInvalid, filesShareDomains, provisionAuthStack, provisionDatabaseBackup, provisionFilesHost, provisionJobsHost, provisionMailServer, resolveOwnFilesKeys, } from './platform-providers.js';
|
|
71
|
-
import { companyAppPostLogoutUris, postLogoutAppliedValue, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
73
|
+
import { companyAppPostLogoutUris, postLogoutAppliedValue, postLogoutUrisFor, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
72
74
|
import { CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY, CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, ENTRY_CONTAINER_LABELS, appLabelsAppliedScopeKey, builtinRoutes, entryAppsHostnames, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryProxyLabels, entryRoutes, entryRoutesMarker, ownsAppsZoneName, ownsOikosName, routesWithoutToken, sameLabelSets, } from './proxy-labels.js';
|
|
73
75
|
import { createReleaseHost } from './release-host.js';
|
|
74
76
|
import { TUNNEL_HTTP_INGRESS_SCOPE_KEY, applyRemoteDatabase, mergeTcpIngress, recordedTcpRoutes, } from './remote-database.js';
|
|
@@ -76,7 +78,8 @@ import { editSignInBindings } from './sign-in-bindings.js';
|
|
|
76
78
|
import { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, closeSignInOrgRegistration, ensureSignInOrg, inviteOwnerWithLink, registerSignInDomainOrBlock, signInOrgDomain, } from './sign-in-org.js';
|
|
77
79
|
import { SIGN_IN_BASE_PATH, SIGN_IN_ENV_LIVE_SCOPE_PREFIX, SIGN_IN_IN_APP_SCOPE_KEY, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_RATE_LIMIT_DESCRIPTION, SIGN_IN_RATE_LIMIT_EXPRESSION, SIGN_IN_RATE_LIMIT_SCOPE_KEY, SIGN_IN_TRUSTED_SCOPE_KEY, SIGN_IN_UPSTREAM_ENV, inAppBindingFor, inAppSignIn, inAppStateHosts, inAppStatePairs, inAppTargets, signInEnvLiveHosts, } from './sign-in.js';
|
|
78
80
|
import { moveSigninDatabase } from './signin-db-move.js';
|
|
79
|
-
import {
|
|
81
|
+
import { buildStageCopyHostPorts, } from './stage-copy-host.js';
|
|
82
|
+
import { membersHash, stageGateEmails, stageGateKey, stageHostname, syncStageGateMembers, } from './stage-guards.js';
|
|
80
83
|
import { WORKER_APP_DB_SCOPE_KEYS, provisionWorkerAppDb, workerAppDbCaPem, workerAppDbOwnerUrl, } from './worker-app-db.js';
|
|
81
84
|
import { WORKER_APP_CALL_TIMEOUT_MS, applyWorkerApp, heldBrokerKey, teardownWorkerApp, withCallTimeout, } from './worker-app.js';
|
|
82
85
|
import { entryWorkerAppliedScopeKey, entryWorkerExclusionsScopeKey, entryWorkerRetiredScopeKey, entryWorkerRouteIdScopeKey, entryWorkerRoutePatternScopeKey, entryWorkerRoutePendingScopeKey, entryWorkerRouteZoneScopeKey, entryWorkerUploadedScopeKey, } from './worker.js';
|
|
@@ -392,6 +395,15 @@ export const BOULE_REPO = 'wtfalch/boule';
|
|
|
392
395
|
export const BOULE_APP_DOCKERFILE = '/Dockerfile';
|
|
393
396
|
/** Boule's own `EXPOSE 3000` (its doc comment just above) -- the port its health check (#543) probes. */
|
|
394
397
|
export const BOULE_PORT = 3000;
|
|
398
|
+
/**
|
|
399
|
+
* foundry#949: kosmos (https://github.com/wtfalch/kosmos), bootstrapped from the same `wtfalch/package-template`
|
|
400
|
+
* service shape Boule is, with its own Dockerfile at the repository root, serving on 3000 (`EXPOSE 3000`) and
|
|
401
|
+
* running its migrations through `apps/host/scripts/migrate.mjs` before it serves, the same shape Boule's
|
|
402
|
+
* `migrate-and-start.sh` follows.
|
|
403
|
+
*/
|
|
404
|
+
export const KOSMOS_REPO = 'wtfalch/kosmos';
|
|
405
|
+
export const KOSMOS_APP_DOCKERFILE = '/Dockerfile';
|
|
406
|
+
export const KOSMOS_PORT = 3000;
|
|
395
407
|
/**
|
|
396
408
|
* Every app foundry deploys keeps its own logs across a redeploy, so a
|
|
397
409
|
* database outage does not also erase the only other copy of the evidence
|
|
@@ -657,6 +669,14 @@ const CALLS_SYNAPSE_SCOPE_KEY = 'coolify_calls_synapse';
|
|
|
657
669
|
const CALLS_MAS_SCOPE_KEY = 'coolify_calls_mas';
|
|
658
670
|
const CALLS_SYNAPSE_DB_SCOPE_KEY = 'coolify_calls_synapse_database';
|
|
659
671
|
const CALLS_MAS_DB_SCOPE_KEY = 'coolify_calls_mas_database';
|
|
672
|
+
/** #430: the calls service's own Matrix/MAS env vars, from before calls#93 (transit-only). Removed from every org's calls service, never written again. */
|
|
673
|
+
const CALLS_MATRIX_ENV_KEYS = [
|
|
674
|
+
'MAS_URL',
|
|
675
|
+
'MAS_HOMESERVER',
|
|
676
|
+
'MAS_CLIENT_ID',
|
|
677
|
+
'MAS_CLIENT_SECRET',
|
|
678
|
+
'SYNAPSE_URL',
|
|
679
|
+
];
|
|
660
680
|
/**
|
|
661
681
|
* Synapse refuses to start against a database whose `LC_COLLATE`/`LC_CTYPE`
|
|
662
682
|
* is not `C` (https://element-hq.github.io/synapse/latest/postgres.html);
|
|
@@ -947,6 +967,8 @@ function limitProfileOf(action) {
|
|
|
947
967
|
case 'coolify:boule':
|
|
948
968
|
case 'coolify:boule-broker-entry':
|
|
949
969
|
return LIMITS.boule;
|
|
970
|
+
case 'coolify:kosmos':
|
|
971
|
+
return LIMITS.kosmos;
|
|
950
972
|
case 'coolify:broker-registrar':
|
|
951
973
|
return LIMITS.brokerEntry;
|
|
952
974
|
case 'coolify:keys-registrar':
|
|
@@ -1380,6 +1402,8 @@ export class FoundryStepExecutor {
|
|
|
1380
1402
|
return this.bouleOwners(org, context);
|
|
1381
1403
|
case 'coolify:boule-broker-entry':
|
|
1382
1404
|
return this.bouleBrokerEntry(org, context);
|
|
1405
|
+
case 'coolify:kosmos':
|
|
1406
|
+
return this.coolifyKosmos(action, org, context);
|
|
1383
1407
|
case 'coolify:broker-registrar':
|
|
1384
1408
|
return this.brokerRegistrar(org, context);
|
|
1385
1409
|
case 'coolify:keys-registrar':
|
|
@@ -2778,6 +2802,7 @@ export class FoundryStepExecutor {
|
|
|
2778
2802
|
const isCmsClient = action.key === 'cms' && org.site.enabled;
|
|
2779
2803
|
const isAgoraClient = action.key === 'agora';
|
|
2780
2804
|
const isBouleClient = action.key === 'boule';
|
|
2805
|
+
const isKosmosClient = action.key === 'kosmos' && kosmosOn(org);
|
|
2781
2806
|
const authEntry = org.hosting.entries.find((e) => e.key === action.key && e.uses?.auth);
|
|
2782
2807
|
// A workspace's own client `<key>-work`, signing in at the workspace host.
|
|
2783
2808
|
const workspaceEntry = authEntry ? undefined : workspaceAuthEntryOf(org, action.key);
|
|
@@ -2787,9 +2812,11 @@ export class FoundryStepExecutor {
|
|
|
2787
2812
|
? AGORA_CLIENT_ID_SCOPE_KEY
|
|
2788
2813
|
: isBouleClient
|
|
2789
2814
|
? BOULE_CLIENT_ID_SCOPE_KEY
|
|
2790
|
-
:
|
|
2791
|
-
?
|
|
2792
|
-
:
|
|
2815
|
+
: isKosmosClient
|
|
2816
|
+
? KOSMOS_CLIENT_ID_SCOPE_KEY
|
|
2817
|
+
: authEntry || workspaceEntry
|
|
2818
|
+
? entryClientIdScopeKey(action.key)
|
|
2819
|
+
: APP_CLIENT_ID_SCOPE_KEY;
|
|
2793
2820
|
const priorAppId = recorded[`zitadel:app:${action.key}`];
|
|
2794
2821
|
// Only a step the planner flagged as destroying (`recreate`, set with
|
|
2795
2822
|
// `destroys: true` so plan-apply asks for the typed confirmation) may
|
|
@@ -3140,8 +3167,33 @@ export class FoundryStepExecutor {
|
|
|
3140
3167
|
...(this.ports.runtime?.fetch ? { fetch: this.ports.runtime.fetch } : {}),
|
|
3141
3168
|
renew,
|
|
3142
3169
|
label: `coolify:release:${subject.id}`,
|
|
3170
|
+
...this.stageCopyFor(org, subject, context),
|
|
3143
3171
|
});
|
|
3144
3172
|
}
|
|
3173
|
+
/**
|
|
3174
|
+
* The copy's ports for one subject. Production's database is the org's (`coolify:database:<org id>`) for an
|
|
3175
|
+
* entry and Boule's own for Boule; Agora has none, so its copy is refused with `no-production-database`.
|
|
3176
|
+
*/
|
|
3177
|
+
stageCopyFor(org, subject, context) {
|
|
3178
|
+
const entryDatabase = companyDatabaseKeys(org);
|
|
3179
|
+
const ports = buildStageCopyHostPorts({
|
|
3180
|
+
host: this.ports.stageCopy,
|
|
3181
|
+
coolify: this.ports.coolify,
|
|
3182
|
+
context,
|
|
3183
|
+
org,
|
|
3184
|
+
secret: (value) => void this.handledSecrets.add(value),
|
|
3185
|
+
databaseKey: subject.builtin ?? `app.${subject.id}`,
|
|
3186
|
+
databaseScopeKey: subject.builtin === 'boule'
|
|
3187
|
+
? entryDatabase.boule
|
|
3188
|
+
: subject.builtin
|
|
3189
|
+
? null
|
|
3190
|
+
: entryDatabase.app,
|
|
3191
|
+
backupScopeKey: backupScopeKey(subject.builtin === 'boule' ? 'boule' : 'app'),
|
|
3192
|
+
projectScopeKey: 'coolify_project',
|
|
3193
|
+
serverScopeKey: COOLIFY_SERVER_SCOPE_KEY,
|
|
3194
|
+
});
|
|
3195
|
+
return ports ? { stageCopy: ports } : {};
|
|
3196
|
+
}
|
|
3145
3197
|
/** Whether the stage mail sink app is stopped or gone, read live. A failed read throws; the caller reads that as up. */
|
|
3146
3198
|
async stageSinkDown(appId) {
|
|
3147
3199
|
try {
|
|
@@ -3153,6 +3205,13 @@ export class FoundryStepExecutor {
|
|
|
3153
3205
|
throw error;
|
|
3154
3206
|
}
|
|
3155
3207
|
}
|
|
3208
|
+
/** Which stage-guard ports this executor has, for the planner (they decide the guard names in the applied hash). */
|
|
3209
|
+
stageGuardPorts() {
|
|
3210
|
+
return {
|
|
3211
|
+
probes: this.ports.stageProbes !== undefined,
|
|
3212
|
+
access: Boolean(this.ports.access || this.ports.edge),
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3156
3215
|
/** Every server Coolify lists with whether it is usable and reachable, for the planner. */
|
|
3157
3216
|
async blueGreenServers() {
|
|
3158
3217
|
return (await this.ports.coolify.listServers()).map((s) => ({
|
|
@@ -3272,7 +3331,8 @@ export class FoundryStepExecutor {
|
|
|
3272
3331
|
primaryBlueAppId,
|
|
3273
3332
|
logDir: LOG_DIR,
|
|
3274
3333
|
isRunning: isRunningOrStarting,
|
|
3275
|
-
stageGuards: this.stageGuards(org, subject, recorded(), action.desired.accessProviderId),
|
|
3334
|
+
stageGuards: this.stageGuards(org, subject, recorded(), action.desired.accessProviderId, context),
|
|
3335
|
+
guardInputs: parseGuards(action.desired.guards),
|
|
3276
3336
|
stageSink: { slug: org.slug },
|
|
3277
3337
|
};
|
|
3278
3338
|
if (action.kind === 'bluegreen-lb') {
|
|
@@ -3311,6 +3371,8 @@ export class FoundryStepExecutor {
|
|
|
3311
3371
|
const guards = this.ports.stageProbes ? { probes: this.ports.stageProbes } : {};
|
|
3312
3372
|
if (gateId)
|
|
3313
3373
|
guards.gate = { hostname: host, create: async () => ({ id: gateId }) };
|
|
3374
|
+
// A workspace signs in with its own client (`workspaceStageSignIn`), never stage sign-in's: that guard is "none".
|
|
3375
|
+
guards.signIn = { required: false };
|
|
3314
3376
|
const runtime = this.ports.runtime ?? {};
|
|
3315
3377
|
const authIssuer = action.desired.authIssuer ?? null;
|
|
3316
3378
|
const signIn = await this.workspaceStageSignIn(org, entry, authIssuer, context);
|
|
@@ -3579,8 +3641,12 @@ export class FoundryStepExecutor {
|
|
|
3579
3641
|
* the company's sign-in organisation, an owner, an Access port) and no `hosting.access` rule names the host: a
|
|
3580
3642
|
* bypass or service-token rule there would open stage, so no gate is made and the guard stays unrecorded.
|
|
3581
3643
|
*/
|
|
3582
|
-
stageGuards(org, subject, recorded, providerId) {
|
|
3644
|
+
stageGuards(org, subject, recorded, providerId, context) {
|
|
3583
3645
|
const guards = this.ports.stageProbes ? { probes: this.ports.stageProbes } : {};
|
|
3646
|
+
const signIn = this.stageSignInPort(org, subject, context);
|
|
3647
|
+
if (signIn)
|
|
3648
|
+
guards.signIn = signIn;
|
|
3649
|
+
guards.services = this.stageServicesPort(org, subject, context);
|
|
3584
3650
|
const claimValue = recorded[ZITADEL_ORG_SCOPE_KEY];
|
|
3585
3651
|
const emails = org.owners.map((owner) => owner.email);
|
|
3586
3652
|
if (!org.domain || typeof providerId !== 'string' || !providerId || !claimValue)
|
|
@@ -3592,19 +3658,285 @@ export class FoundryStepExecutor {
|
|
|
3592
3658
|
return guards;
|
|
3593
3659
|
guards.gate = {
|
|
3594
3660
|
hostname,
|
|
3595
|
-
create: () =>
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3661
|
+
create: async () => {
|
|
3662
|
+
const list = await stageGateEmails({
|
|
3663
|
+
label: `coolify:stage-gate:${subject.id}`,
|
|
3664
|
+
subjectId: subject.id,
|
|
3665
|
+
orgId: org.id,
|
|
3666
|
+
owners: emails,
|
|
3667
|
+
read: this.ports.releasePreviewMembers,
|
|
3668
|
+
});
|
|
3669
|
+
const created = await this.accessPort().createGate({
|
|
3670
|
+
organisationId: org.id,
|
|
3671
|
+
key: stageGateKey(subject.id),
|
|
3672
|
+
entry: subject.id,
|
|
3673
|
+
hostnames: [hostname],
|
|
3674
|
+
providerId,
|
|
3675
|
+
claimName: GATE_CLAIM_NAME,
|
|
3676
|
+
claimValue,
|
|
3677
|
+
emails: list.emails,
|
|
3678
|
+
});
|
|
3679
|
+
return list.members
|
|
3680
|
+
? { id: created.id, membersHash: membersHash(list.emails) }
|
|
3681
|
+
: { id: created.id };
|
|
3682
|
+
},
|
|
3605
3683
|
};
|
|
3606
3684
|
return guards;
|
|
3607
3685
|
}
|
|
3686
|
+
/**
|
|
3687
|
+
* One release tick for one subject: its recorded stage gate's list follows the people holding
|
|
3688
|
+
* `releases:preview`, owners always staying (`syncStageGateMembers`). Never throws.
|
|
3689
|
+
*/
|
|
3690
|
+
async syncStageGateMembers(org, subject, context) {
|
|
3691
|
+
await syncStageGateMembers({
|
|
3692
|
+
label: `coolify:stage-gate:${subject.id}`,
|
|
3693
|
+
subjectId: subject.id,
|
|
3694
|
+
orgId: org.id,
|
|
3695
|
+
owners: org.owners.map((owner) => owner.email),
|
|
3696
|
+
read: this.ports.releasePreviewMembers,
|
|
3697
|
+
recorded: context.recordedIds(),
|
|
3698
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
3699
|
+
persist: () => context.persist(),
|
|
3700
|
+
setPeople: async (gateId, list) => {
|
|
3701
|
+
const access = this.accessPort();
|
|
3702
|
+
if (!access.setGatePeople) {
|
|
3703
|
+
const error = new Error('the Access port cannot change a gate');
|
|
3704
|
+
error.name = 'NoGatePeoplePort';
|
|
3705
|
+
throw error;
|
|
3706
|
+
}
|
|
3707
|
+
await access.setGatePeople(gateId, list);
|
|
3708
|
+
},
|
|
3709
|
+
});
|
|
3710
|
+
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Stage's own files organisation, ai organisation and jobs project (`plan/stage-services.ts`), each named
|
|
3713
|
+
* for its subject (`<org id>-stage-<subject id>`, `<slug>-stage-<subject id>`: one per subject, as the recorded keys are) and built from the same ports production's steps use. A service whose port is
|
|
3714
|
+
* missing or not ready answers `available() === false`, and stage then records it absent and mints nothing.
|
|
3715
|
+
*/
|
|
3716
|
+
stageServicesPort(org, subject, context) {
|
|
3717
|
+
const entry = subject.builtin
|
|
3718
|
+
? undefined
|
|
3719
|
+
: org.hosting.entries.find((e) => e.key === subject.id);
|
|
3720
|
+
const stageOrgId = `${org.id}-stage-${subject.id}`;
|
|
3721
|
+
const keyName = `${org.slug}-${subject.id}-stage`;
|
|
3722
|
+
const heldKeys = this.ports.heldKeys;
|
|
3723
|
+
const held = (service) => ({
|
|
3724
|
+
refFor: (envName) => sharedSecretRef(org.id, `stage-${service}-${envName.toLowerCase()}-${subject.id}`),
|
|
3725
|
+
resolve: async (ref) => (await heldKeys?.resolve(ref)) ?? null,
|
|
3726
|
+
put: async (ref, value) => {
|
|
3727
|
+
if (typeof heldKeys?.put !== 'function')
|
|
3728
|
+
throw new Error('the held keys cannot put');
|
|
3729
|
+
await heldKeys.put(ref, value);
|
|
3730
|
+
},
|
|
3731
|
+
});
|
|
3732
|
+
const canHold = typeof heldKeys?.put === 'function';
|
|
3733
|
+
const reachable = async (get) => {
|
|
3734
|
+
try {
|
|
3735
|
+
await get();
|
|
3736
|
+
return true;
|
|
3737
|
+
}
|
|
3738
|
+
catch {
|
|
3739
|
+
return false;
|
|
3740
|
+
}
|
|
3741
|
+
};
|
|
3742
|
+
const owner = filesOwnerUserId(org, context.recordedIds());
|
|
3743
|
+
const files = {
|
|
3744
|
+
idEnv: 'FILES_ORGANISATION_ID',
|
|
3745
|
+
held: held('files'),
|
|
3746
|
+
available: async () => canHold && Boolean(owner) && (await reachable(() => this.filesPort())),
|
|
3747
|
+
create: async () => {
|
|
3748
|
+
const port = await this.filesPort();
|
|
3749
|
+
const made = await port.provisionOrganisation({
|
|
3750
|
+
organisationId: stageOrgId,
|
|
3751
|
+
ceilingBytes: null,
|
|
3752
|
+
ceilingMicros: null,
|
|
3753
|
+
name: `${org.displayName}-stage`,
|
|
3754
|
+
...(owner ? { owner } : {}),
|
|
3755
|
+
});
|
|
3756
|
+
return { id: made.organisationId };
|
|
3757
|
+
},
|
|
3758
|
+
mintKey: async (id) => {
|
|
3759
|
+
const port = await this.filesPort();
|
|
3760
|
+
const issued = await port.issueOrganisationKey(id, {
|
|
3761
|
+
name: keyName,
|
|
3762
|
+
kind: 'service',
|
|
3763
|
+
issuer: owner,
|
|
3764
|
+
permissions: [...(entry?.uses?.files?.permissions ?? FILES_ENTRY_PERMISSIONS)],
|
|
3765
|
+
expiresDays: FILES_KEY_DAYS,
|
|
3766
|
+
});
|
|
3767
|
+
return { keyIds: [issued.id], secrets: [{ env: 'FILES_API_KEY', value: issued.secret }] };
|
|
3768
|
+
},
|
|
3769
|
+
};
|
|
3770
|
+
const ai = {
|
|
3771
|
+
idEnv: 'AI_ORGANISATION_ID',
|
|
3772
|
+
held: held('ai'),
|
|
3773
|
+
available: async () => canHold && (await reachable(() => this.aiPort())),
|
|
3774
|
+
create: async () => {
|
|
3775
|
+
const port = await this.aiPort();
|
|
3776
|
+
// Its own budget line, with production's limit (its declared one, else the default).
|
|
3777
|
+
const limitMicros = org.ai.limitMicros ?? DEFAULT_AI_LIMIT_MICROS;
|
|
3778
|
+
const made = await port.provisionOrganisation({ organisationId: stageOrgId, limitMicros });
|
|
3779
|
+
await port.setOrganisationOffers(stageOrgId, [...(entry?.uses?.ai?.offerings ?? [])]);
|
|
3780
|
+
await port.updateBudget(stageOrgId, { keyId: null, limitMicros });
|
|
3781
|
+
return { id: made.organisationId };
|
|
3782
|
+
},
|
|
3783
|
+
mintKey: async (id) => {
|
|
3784
|
+
const port = await this.aiPort();
|
|
3785
|
+
const issued = await port.issueOrganisationKey(id, {
|
|
3786
|
+
name: keyName,
|
|
3787
|
+
expiresDays: AI_ENTRY_KEY_DAYS,
|
|
3788
|
+
permissions: [...(entry?.uses?.ai?.permissions ?? AI_ENTRY_PERMISSIONS)],
|
|
3789
|
+
});
|
|
3790
|
+
return { keyIds: [issued.id], secrets: [{ env: 'AI_API_KEY', value: issued.secret }] };
|
|
3791
|
+
},
|
|
3792
|
+
};
|
|
3793
|
+
const jobs = {
|
|
3794
|
+
held: held('jobs'),
|
|
3795
|
+
available: async () => canHold && Boolean(org.domain) && (await reachable(() => this.jobsPort(org))),
|
|
3796
|
+
create: async () => {
|
|
3797
|
+
const scheduler = await this.jobsPort(org);
|
|
3798
|
+
const projectName = `${org.slug}-stage-${subject.id}`;
|
|
3799
|
+
const registered = await scheduler.registerProject({
|
|
3800
|
+
name: projectName,
|
|
3801
|
+
baseUrl: `https://${stageHostname(org.domain, subject.id)}`,
|
|
3802
|
+
});
|
|
3803
|
+
return registered.secret && registered.syncKey
|
|
3804
|
+
? {
|
|
3805
|
+
id: registered.id,
|
|
3806
|
+
name: projectName,
|
|
3807
|
+
keyIds: [registered.id],
|
|
3808
|
+
secrets: [
|
|
3809
|
+
{ env: 'JOBS_SIGNING_SECRET', value: registered.secret },
|
|
3810
|
+
{ env: 'JOBS_SYNC_KEY', value: registered.syncKey },
|
|
3811
|
+
],
|
|
3812
|
+
}
|
|
3813
|
+
: { id: registered.id, name: projectName };
|
|
3814
|
+
},
|
|
3815
|
+
};
|
|
3816
|
+
const prodJobsProjectId = context.recordedIds()[`jobs:registration:${org.id}`];
|
|
3817
|
+
return { files, ai, jobs, ...(prodJobsProjectId ? { prodJobsProjectId } : {}) };
|
|
3818
|
+
}
|
|
3819
|
+
/**
|
|
3820
|
+
* Stage's own sign-in (`plan/stage-signin.ts`): a client named `<production client>-stage` on production's
|
|
3821
|
+
* sign-in organisation and project with redirect URIs on the stage host only, a broker entry
|
|
3822
|
+
* `<production entry>-stage` with a key of its own, and that key's held ref. `{ required: false }` for an entry
|
|
3823
|
+
* that does not sign in. `undefined`, with one fixed log line saying why, when something it needs is missing:
|
|
3824
|
+
* the guard then stays unrecorded and the copy waits. Platform-archon is not covered yet.
|
|
3825
|
+
*/
|
|
3826
|
+
stageSignInPort(org, subject, context) {
|
|
3827
|
+
const why = (reason) => {
|
|
3828
|
+
console.info(`stage sign-in for ${subject.id} is not set up: ${reason}`);
|
|
3829
|
+
return undefined;
|
|
3830
|
+
};
|
|
3831
|
+
const entry = subject.builtin
|
|
3832
|
+
? undefined
|
|
3833
|
+
: org.hosting.entries.find((e) => e.key === subject.id);
|
|
3834
|
+
if (!subject.builtin && !entry?.uses?.auth)
|
|
3835
|
+
return { required: false };
|
|
3836
|
+
const recorded = context.recordedIds();
|
|
3837
|
+
if (subject.builtin === 'agora') {
|
|
3838
|
+
// A static SPA: a public client named `<production client>-stage` (production's is `agora`), redirect URIs
|
|
3839
|
+
// on the stage host only. No broker entry, app key or cookie secret exist for it.
|
|
3840
|
+
const { zitadel } = this.ports;
|
|
3841
|
+
if (!org.domain)
|
|
3842
|
+
return why('the company has no domain, so stage has no host to sign in at');
|
|
3843
|
+
const zitadelOrgId = recorded.zitadel_org;
|
|
3844
|
+
if (!zitadelOrgId)
|
|
3845
|
+
return why('no sign-in organisation is recorded');
|
|
3846
|
+
const host = stageHostname(org.domain, subject.id);
|
|
3847
|
+
return {
|
|
3848
|
+
required: true,
|
|
3849
|
+
public: true,
|
|
3850
|
+
spec: {
|
|
3851
|
+
clientName: `${subject.id}-stage`,
|
|
3852
|
+
organizationId: zitadelOrgId,
|
|
3853
|
+
redirectUris: [`https://${host}/callback`],
|
|
3854
|
+
postLogoutRedirectUris: [`https://${host}/`],
|
|
3855
|
+
origins: [`https://${host}`],
|
|
3856
|
+
},
|
|
3857
|
+
findClient: async (name) => {
|
|
3858
|
+
const projectId = await this.ensureZitadelProject(org, zitadelOrgId, context);
|
|
3859
|
+
const { apps } = await zitadel.listApps(zitadelOrgId, projectId);
|
|
3860
|
+
const hit = apps.find((a) => a.name === name && a.state !== 'APP_STATE_REMOVED');
|
|
3861
|
+
return hit
|
|
3862
|
+
? { appId: hit.id, ...(hit.clientId ? { clientId: hit.clientId } : {}) }
|
|
3863
|
+
: undefined;
|
|
3864
|
+
},
|
|
3865
|
+
createClient: async (spec) => {
|
|
3866
|
+
const projectId = await this.ensureZitadelProject(org, zitadelOrgId, context);
|
|
3867
|
+
return zitadel.createOidcApp(zitadelOrgId, projectId, {
|
|
3868
|
+
name: spec.clientName,
|
|
3869
|
+
appType: 'web',
|
|
3870
|
+
redirectUris: spec.redirectUris,
|
|
3871
|
+
postLogoutRedirectUris: spec.postLogoutRedirectUris,
|
|
3872
|
+
});
|
|
3873
|
+
},
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
const wanted = entry
|
|
3877
|
+
? wantedBrokerEntry(org, entry, recorded)
|
|
3878
|
+
: wantedBouleBrokerEntry(org, recorded);
|
|
3879
|
+
if ('missing' in wanted)
|
|
3880
|
+
return why("production's sign-in is not recorded yet");
|
|
3881
|
+
const { broker, heldKeys, zitadel } = this.ports;
|
|
3882
|
+
if (!broker)
|
|
3883
|
+
return why('the broker port is not wired');
|
|
3884
|
+
if (typeof heldKeys?.put !== 'function')
|
|
3885
|
+
return why('the held keys cannot put');
|
|
3886
|
+
if (!org.domain)
|
|
3887
|
+
return why('the company has no domain, so stage has no host to sign in at');
|
|
3888
|
+
const zitadelOrgId = recorded.zitadel_org;
|
|
3889
|
+
if (!zitadelOrgId)
|
|
3890
|
+
return why('no sign-in organisation is recorded');
|
|
3891
|
+
const clientKey = subject.id;
|
|
3892
|
+
const host = stageHostname(org.domain, subject.id);
|
|
3893
|
+
const put = heldKeys.put;
|
|
3894
|
+
const ref = sharedSecretRef(org.id, `stage-auth-app-key-${subject.id}`);
|
|
3895
|
+
const want = wanted.want;
|
|
3896
|
+
return {
|
|
3897
|
+
required: true,
|
|
3898
|
+
spec: {
|
|
3899
|
+
clientName: `${clientKey}-stage`,
|
|
3900
|
+
organizationId: want.organizationId,
|
|
3901
|
+
redirectUris: [`https://${host}/auth/callback`, desktopCallbackUri(host)],
|
|
3902
|
+
postLogoutRedirectUris: postLogoutUrisFor([host]),
|
|
3903
|
+
brokerName: `${want.name}-stage`,
|
|
3904
|
+
origins: [`https://${host}`],
|
|
3905
|
+
from: want.from,
|
|
3906
|
+
registration: want.registration,
|
|
3907
|
+
...(want.emailLink === undefined ? {} : { emailLink: want.emailLink }),
|
|
3908
|
+
},
|
|
3909
|
+
findClient: async (name) => {
|
|
3910
|
+
const projectId = await this.ensureZitadelProject(org, zitadelOrgId, context);
|
|
3911
|
+
const { apps } = await zitadel.listApps(zitadelOrgId, projectId);
|
|
3912
|
+
const hit = apps.find((a) => a.name === name && a.state !== 'APP_STATE_REMOVED');
|
|
3913
|
+
return hit
|
|
3914
|
+
? { appId: hit.id, ...(hit.clientId ? { clientId: hit.clientId } : {}) }
|
|
3915
|
+
: undefined;
|
|
3916
|
+
},
|
|
3917
|
+
createClient: async (spec) => {
|
|
3918
|
+
const projectId = await this.ensureZitadelProject(org, zitadelOrgId, context);
|
|
3919
|
+
return zitadel.createOidcApp(zitadelOrgId, projectId, {
|
|
3920
|
+
name: spec.clientName,
|
|
3921
|
+
appType: 'web',
|
|
3922
|
+
redirectUris: spec.redirectUris,
|
|
3923
|
+
postLogoutRedirectUris: spec.postLogoutRedirectUris,
|
|
3924
|
+
});
|
|
3925
|
+
},
|
|
3926
|
+
writeBrokerEntry: async (entryWant, appKey) => {
|
|
3927
|
+
await writeBrokerEntry(broker, {
|
|
3928
|
+
want: { ...entryWant, replaces: null },
|
|
3929
|
+
appKey,
|
|
3930
|
+
previous: null,
|
|
3931
|
+
});
|
|
3932
|
+
},
|
|
3933
|
+
held: {
|
|
3934
|
+
ref,
|
|
3935
|
+
resolve: async () => (await heldKeys.resolve(ref)) ?? null,
|
|
3936
|
+
put: (value) => put.call(heldKeys, ref, value),
|
|
3937
|
+
},
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3608
3940
|
async coolifyApp(action, org, context) {
|
|
3609
3941
|
const recordedAppId = context.recordedIds()[`coolify:app:${action.key}`];
|
|
3610
3942
|
if (action.desired.enabled === false) {
|
|
@@ -4247,6 +4579,8 @@ export class FoundryStepExecutor {
|
|
|
4247
4579
|
}
|
|
4248
4580
|
case 'boule':
|
|
4249
4581
|
return { appIds: await byName(`${org.slug}-boule`), template: true };
|
|
4582
|
+
case 'kosmos':
|
|
4583
|
+
return { appIds: await byName(`${org.slug}-kosmos`), template: true };
|
|
4250
4584
|
case 'chat_service':
|
|
4251
4585
|
return { appIds: await byName(chat.service), template: true };
|
|
4252
4586
|
case 'calls_service':
|
|
@@ -4822,11 +5156,27 @@ export class FoundryStepExecutor {
|
|
|
4822
5156
|
async ensureBridgeSecret(appId) {
|
|
4823
5157
|
return this.mintIfAbsent(appId, BOULE_BRIDGE_SECRET_ENV, 32);
|
|
4824
5158
|
}
|
|
4825
|
-
|
|
5159
|
+
/**
|
|
5160
|
+
* `minLength`, when given, also counts a present-but-empty value as not set -- the same guard
|
|
5161
|
+
* {@link ensureAuthAppKey} already applies for `AUTH_APP_KEY`, now reusable here instead of a
|
|
5162
|
+
* third, bare copy of this check (foundry#970, thermo note 1 in #957). Omitted, the three existing
|
|
5163
|
+
* callers below keep their original behaviour.
|
|
5164
|
+
*/
|
|
5165
|
+
async mintIfAbsent(appId, key, bytes, minLength) {
|
|
4826
5166
|
const envs = await this.ports.coolify.listEnvs(appId);
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
5167
|
+
const already = envs.some((e) => e.key === key &&
|
|
5168
|
+
!e.is_preview &&
|
|
5169
|
+
(minLength === undefined || (e.value ?? '').length >= minLength));
|
|
5170
|
+
return already ? null : randomBytes(bytes).toString('base64url');
|
|
5171
|
+
}
|
|
5172
|
+
/**
|
|
5173
|
+
* Every company's Boule, F3/#970: `NATIVE_BUILD_KEY` is what Boule presents to Archon's
|
|
5174
|
+
* native-build route (estate#262) to ask it to dispatch `build-app.yml`. Minted once, like
|
|
5175
|
+
* {@link ensureAuthAppKey}, and never rotated; a present-but-empty value (the bug
|
|
5176
|
+
* `mintNativeBuildKey`'s own read-then-generate had, see #957) is treated as not set.
|
|
5177
|
+
*/
|
|
5178
|
+
async ensureNativeBuildKey(appId) {
|
|
5179
|
+
return this.mintIfAbsent(appId, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, 1);
|
|
4830
5180
|
}
|
|
4831
5181
|
/**
|
|
4832
5182
|
* `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
|
|
@@ -6530,16 +6880,18 @@ export class FoundryStepExecutor {
|
|
|
6530
6880
|
// ---- calls service (#145, part 2 of #128) -------------------------------
|
|
6531
6881
|
/**
|
|
6532
6882
|
* `plan/diff.ts`'s `planCallsService`: the org's own calls service --
|
|
6533
|
-
* `wtfalch/calls`'s `packages/service`, wired to the
|
|
6534
|
-
*
|
|
6535
|
-
*
|
|
6536
|
-
*
|
|
6537
|
-
*
|
|
6538
|
-
*
|
|
6539
|
-
*
|
|
6540
|
-
*
|
|
6541
|
-
*
|
|
6542
|
-
*
|
|
6883
|
+
* `wtfalch/calls`'s `packages/service`, wired to the shared LiveKit SFU
|
|
6884
|
+
* (#122/#127). #430: calls run transit-only (calls#93's
|
|
6885
|
+
* `CALLS_ENCRYPTION=transit`), so this no longer waits on or wires a
|
|
6886
|
+
* per-org Matrix stack at all -- an org whose stack is already live from
|
|
6887
|
+
* before this keeps running it, unused, until a human removes it
|
|
6888
|
+
* separately. Static config checks run first, before any Coolify call
|
|
6889
|
+
* (#128's own carried-over PR 344 review fix, restated in #145's own
|
|
6890
|
+
* issue body): the shared SFU's LiveKit credentials must already be
|
|
6891
|
+
* resolvable -- not minted or guessed here. Park stops the app only, the
|
|
6892
|
+
* same park-not-delete rule {@link coolifyCalls}/{@link coolifyApp}
|
|
6893
|
+
* already follow (#7 corrections 1); its own database is never touched
|
|
6894
|
+
* from that branch.
|
|
6543
6895
|
*/
|
|
6544
6896
|
async coolifyCallsService(action, org, context) {
|
|
6545
6897
|
if (action.desired.enabled === false) {
|
|
@@ -6550,18 +6902,8 @@ export class FoundryStepExecutor {
|
|
|
6550
6902
|
await this.ports.coolify.raw('POST', `/applications/${appId}/stop`);
|
|
6551
6903
|
return { resourceId: appId };
|
|
6552
6904
|
}
|
|
6553
|
-
// Refused before anything is created, like the missing-
|
|
6905
|
+
// Refused before anything is created, like the missing-SFU refusal below.
|
|
6554
6906
|
const sfuUrls = resolveCallsSfuUrls(this.ports.callsSfuUrls);
|
|
6555
|
-
const recorded = context.recordedIds();
|
|
6556
|
-
const synapseId = recorded[CALLS_SYNAPSE_SCOPE_KEY];
|
|
6557
|
-
const masId = recorded[CALLS_MAS_SCOPE_KEY];
|
|
6558
|
-
if (!synapseId || !masId) {
|
|
6559
|
-
throw new Error(`coolify:calls-service for ${org.slug}: the calls Matrix stack has not been provisioned yet -- coolify:calls must run first`);
|
|
6560
|
-
}
|
|
6561
|
-
const masCredentials = await this.readCallsMasClientCredentials(masId);
|
|
6562
|
-
if (!masCredentials) {
|
|
6563
|
-
throw new Error(`coolify:calls-service for ${org.slug}: the calls Matrix stack's MAS client credentials are not on the MAS app yet -- retry this apply once coolify:calls has minted them`);
|
|
6564
|
-
}
|
|
6565
6907
|
const sfu = await this.ports.callsSfu.resolveCredentials(org.slug);
|
|
6566
6908
|
if (!sfu) {
|
|
6567
6909
|
throw new Error(`coolify:calls-service for ${org.slug}: the shared calls SFU has no LiveKit credentials for this org yet -- see https://github.com/wtfalch/foundry/issues/152`);
|
|
@@ -6569,8 +6911,6 @@ export class FoundryStepExecutor {
|
|
|
6569
6911
|
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
6570
6912
|
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
6571
6913
|
const name = callsServiceInternalName(org);
|
|
6572
|
-
const synapseHost = `${org.slug}-calls-synapse`;
|
|
6573
|
-
const masHost = `${org.slug}-calls-mas`;
|
|
6574
6914
|
const dbName = `${name}-db`;
|
|
6575
6915
|
const serviceHome = await this.ensureHomeDatabase(org, context, 'calls_service', CALLS_SERVICE_DB_SCOPE_KEY, () => this.ensureCallsDatabase(org, context, projectUuid, environment, dbName, dbName.replace(/-/g, '_'), CALLS_SERVICE_DB_SCOPE_KEY));
|
|
6576
6916
|
if (!(await this.homeOwnerUrl(serviceHome))) {
|
|
@@ -6612,17 +6952,25 @@ export class FoundryStepExecutor {
|
|
|
6612
6952
|
});
|
|
6613
6953
|
// Every app foundry deploys keeps its own logs (#75), reused as-is here.
|
|
6614
6954
|
await this.ensureLogVolume(app.appId, org, app.existed);
|
|
6955
|
+
// #430: an org whose Matrix stack is still live from before this keeps
|
|
6956
|
+
// the Synapse/MAS apps running, unused, until a human removes them
|
|
6957
|
+
// separately -- but its calls service's own env drops their vars on its
|
|
6958
|
+
// next apply, the same as a brand new org that never got them.
|
|
6959
|
+
for (const key of CALLS_MATRIX_ENV_KEYS) {
|
|
6960
|
+
const env = (await this.ports.coolify.listEnvs(app.appId)).find((e) => e.key === key && !e.is_preview);
|
|
6961
|
+
if (env?.uuid)
|
|
6962
|
+
await this.ports.coolify.deleteEnv(app.appId, env.uuid);
|
|
6963
|
+
}
|
|
6615
6964
|
const bindingSecret = await this.ensureCallsBindingSecret(app.appId);
|
|
6616
6965
|
const envs = [
|
|
6617
6966
|
{ key: 'LIVEKIT_URL', value: sfuUrls.privateUrl },
|
|
6618
6967
|
{ key: 'LIVEKIT_PUBLIC_URL', value: sfuUrls.publicUrl },
|
|
6619
6968
|
{ key: 'LIVEKIT_API_KEY', value: sfu.apiKey },
|
|
6620
6969
|
{ key: 'LIVEKIT_API_SECRET', value: sfu.apiSecret },
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
{ key: '
|
|
6625
|
-
{ key: 'SYNAPSE_URL', value: `http://${synapseHost}:8008` },
|
|
6970
|
+
// #430 (calls#93): calls are transit-only, so the service never needs
|
|
6971
|
+
// a Matrix/MAS provider -- MAS_URL, MAS_HOMESERVER, MAS_CLIENT_ID,
|
|
6972
|
+
// MAS_CLIENT_SECRET and SYNAPSE_URL are never written.
|
|
6973
|
+
{ key: 'CALLS_ENCRYPTION', value: 'transit' },
|
|
6626
6974
|
{ key: 'CALLS_ORIGIN', value: `https://${agoraHostnameFor(org)}` },
|
|
6627
6975
|
{ key: 'CALLS_HOST_ADAPTER', value: 'production' },
|
|
6628
6976
|
// Deliberately kept as it was, not widened: the calls service refuses to
|
|
@@ -6748,6 +7096,9 @@ export class FoundryStepExecutor {
|
|
|
6748
7096
|
throw new Error(`${at}: token ${tokenId} is recorded but chat-service no longer holds a usable one; remove the recorded keys ${CHAT_CALLS_TOKEN_SCOPE_KEY} and ${CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY} to mint a new one`);
|
|
6749
7097
|
}
|
|
6750
7098
|
const token = usable ?? randomBytes(32).toString('hex');
|
|
7099
|
+
// Registered before either write, so a Coolify error body echoing the upsert is scrubbed
|
|
7100
|
+
// from the run record the same way the write loop's own dropped-error fallback intends.
|
|
7101
|
+
this.handledSecrets.add(token);
|
|
6751
7102
|
if (!tokenId) {
|
|
6752
7103
|
tokenId = randomBytes(8).toString('hex');
|
|
6753
7104
|
context.recordScopeId(CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, tokenId);
|
|
@@ -6852,22 +7203,6 @@ export class FoundryStepExecutor {
|
|
|
6852
7203
|
appId: client.appId,
|
|
6853
7204
|
};
|
|
6854
7205
|
}
|
|
6855
|
-
/**
|
|
6856
|
-
* Reads back the calls Matrix stack's MAS client id/secret from the MAS
|
|
6857
|
-
* app's own envs ({@link CALLS_SECRET_ENV_KEYS}) -- never mints: minting
|
|
6858
|
-
* either is {@link ensureCallsMatrixSecrets}'s own job, and a missing value
|
|
6859
|
-
* here means that stack has not finished provisioning yet, not something
|
|
6860
|
-
* this step can fix on its own.
|
|
6861
|
-
*/
|
|
6862
|
-
async readCallsMasClientCredentials(masAppId) {
|
|
6863
|
-
const envs = await this.ports.coolify.listEnvs(masAppId);
|
|
6864
|
-
const read = (key) => envs.find((e) => e.key === key && !e.is_preview)?.value;
|
|
6865
|
-
const clientId = read(CALLS_SECRET_ENV_KEYS.masClientId);
|
|
6866
|
-
const clientSecret = read(CALLS_SECRET_ENV_KEYS.masClientSecret);
|
|
6867
|
-
if (!clientId || !clientSecret)
|
|
6868
|
-
return null;
|
|
6869
|
-
return { clientId, clientSecret };
|
|
6870
|
-
}
|
|
6871
7206
|
/**
|
|
6872
7207
|
* `null` once the calls service app already has a `CALLS_BINDING_SECRET` --
|
|
6873
7208
|
* never rotated, the same rule {@link ensureAuthCookieSecret} follows for
|
|
@@ -7335,6 +7670,12 @@ export class FoundryStepExecutor {
|
|
|
7335
7670
|
this.handledSecrets.add(ownerSeedSecret);
|
|
7336
7671
|
envs.push({ key: BOULE_OWNER_SEED_SECRET_ENV, value: ownerSeedSecret });
|
|
7337
7672
|
}
|
|
7673
|
+
// The key Boule presents to Archon's native-build route, for every org's Boule (foundry#970).
|
|
7674
|
+
const nativeBuildKey = await this.ensureNativeBuildKey(app.appId);
|
|
7675
|
+
if (nativeBuildKey) {
|
|
7676
|
+
this.handledSecrets.add(nativeBuildKey);
|
|
7677
|
+
envs.push({ key: NATIVE_BUILD_KEY_ENV, value: nativeBuildKey });
|
|
7678
|
+
}
|
|
7338
7679
|
// Held, minted once (C3). Compared with the live value so a changed one restarts the app.
|
|
7339
7680
|
const credentialEnvs = [];
|
|
7340
7681
|
let archonPushedAt = null;
|
|
@@ -7395,6 +7736,7 @@ export class FoundryStepExecutor {
|
|
|
7395
7736
|
peerPassword,
|
|
7396
7737
|
bridgeSecret,
|
|
7397
7738
|
ownerSeedSecret,
|
|
7739
|
+
nativeBuildKey,
|
|
7398
7740
|
...credentialEnvs.map((credential) => credential.value),
|
|
7399
7741
|
...grants.secrets,
|
|
7400
7742
|
].filter((v) => v !== null);
|
|
@@ -7434,6 +7776,7 @@ export class FoundryStepExecutor {
|
|
|
7434
7776
|
peerPassword !== null ||
|
|
7435
7777
|
bridgeSecret !== null ||
|
|
7436
7778
|
ownerSeedSecret !== null ||
|
|
7779
|
+
nativeBuildKey !== null ||
|
|
7437
7780
|
secretsNotRestarted ||
|
|
7438
7781
|
registrarUnstarted ||
|
|
7439
7782
|
dbPush.changed ||
|
|
@@ -7452,6 +7795,98 @@ export class FoundryStepExecutor {
|
|
|
7452
7795
|
this.recordSignInEnvLive(context, 'boule', bouleHostnameFor(org), env);
|
|
7453
7796
|
return { resourceId: app.appId };
|
|
7454
7797
|
}
|
|
7798
|
+
// ---- kosmos (foundry#949) --------------------------------------------------
|
|
7799
|
+
/**
|
|
7800
|
+
* `plan/diff.ts`'s `planKosmos`: provisions the org's own kosmos
|
|
7801
|
+
* (https://github.com/wtfalch/kosmos) as one more Coolify application, from
|
|
7802
|
+
* that repo's root Dockerfile ({@link KOSMOS_REPO}), with a database of its
|
|
7803
|
+
* own, and pushes its sign-in env. The same shape as {@link coolifyBoule},
|
|
7804
|
+
* trimmed: kosmos is opt-in (`kosmosOn(org)` guards every caller, so this is
|
|
7805
|
+
* never reached while it is off), has no mail, no owner seeding and no
|
|
7806
|
+
* Archon credential.
|
|
7807
|
+
*
|
|
7808
|
+
* The sign-in env comes from `plan/kosmos.ts`'s `desiredKosmosEnv`, the same
|
|
7809
|
+
* function the plan compares the live app against; it never writes a
|
|
7810
|
+
* partial set. The database is `<slug>-kosmos-db`, split the same way
|
|
7811
|
+
* Boule's is: the superuser URL as `DATABASE_URL_OWNER` and a `<dbName>_rt`
|
|
7812
|
+
* role's URL as `DATABASE_URL`, both through `pushDatabaseUrl`.
|
|
7813
|
+
*
|
|
7814
|
+
* #504 (same reason {@link coolifyBoule}'s own comment gives): a brand new
|
|
7815
|
+
* app is never restarted, but it is deployed once ({@link KOSMOS_DEPLOYED_SCOPE_KEY}).
|
|
7816
|
+
*/
|
|
7817
|
+
async coolifyKosmos(action, org, context) {
|
|
7818
|
+
// Park stops the app only; its database is never touched (#7 corrections 1).
|
|
7819
|
+
if (action.desired.enabled === false) {
|
|
7820
|
+
const appId = context.recordedIds()[`coolify:kosmos:${action.key}`];
|
|
7821
|
+
if (!appId) {
|
|
7822
|
+
throw new Error(`coolify:kosmos for ${org.slug}: no live app recorded, nothing to park`);
|
|
7823
|
+
}
|
|
7824
|
+
await this.ports.coolify.raw('POST', `/applications/${appId}/stop`);
|
|
7825
|
+
return { resourceId: appId };
|
|
7826
|
+
}
|
|
7827
|
+
const issuer = action.desired.issuer ?? null;
|
|
7828
|
+
const { env, missing } = desiredKosmosEnv(org, context.recordedIds(), issuer);
|
|
7829
|
+
if (missing.length > 0) {
|
|
7830
|
+
throw new Error(`kosmos for ${org.slug} cannot be configured: missing ${missing.join(', ')}. Nothing was written.`);
|
|
7831
|
+
}
|
|
7832
|
+
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
7833
|
+
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
7834
|
+
const name = `${org.slug}-kosmos`;
|
|
7835
|
+
const dbName = `${org.slug}-kosmos-db`.replace(/-/g, '_');
|
|
7836
|
+
// #444: on the company's server there is no Coolify database to make, like Boule's.
|
|
7837
|
+
const kosmosServer = this.homeOnServer(org, context, 'kosmos')
|
|
7838
|
+
? await this.serverHome(org, context, 'kosmos')
|
|
7839
|
+
: null;
|
|
7840
|
+
const dbId = kosmosServer
|
|
7841
|
+
? ''
|
|
7842
|
+
: await this.ensureCallsDatabase(org, context, projectUuid, environment, `${org.slug}-kosmos-db`, dbName, KOSMOS_DATABASE_SCOPE_KEY);
|
|
7843
|
+
if (!kosmosServer)
|
|
7844
|
+
context.recordScopeId(KOSMOS_DATABASE_SCOPE_KEY, dbId);
|
|
7845
|
+
const server = await this.resolveServer(org, context);
|
|
7846
|
+
const githubApp = await this.ports.coolify.requireGithubApp();
|
|
7847
|
+
const app = await this.findOrCreateCallsApp(name, environment.id, () => this.ports.coolify.createGithubAppApplication({
|
|
7848
|
+
is_preview_deployments_enabled: false,
|
|
7849
|
+
project_uuid: projectUuid,
|
|
7850
|
+
server_uuid: server.uuid,
|
|
7851
|
+
environment_name: environment.name,
|
|
7852
|
+
environment_uuid: environment.uuid,
|
|
7853
|
+
github_app_uuid: githubApp.uuid,
|
|
7854
|
+
git_repository: KOSMOS_REPO,
|
|
7855
|
+
git_branch: 'main',
|
|
7856
|
+
build_pack: 'dockerfile',
|
|
7857
|
+
name,
|
|
7858
|
+
domains: coolifyTunnelDomains([kosmosHostnameFor(org)]),
|
|
7859
|
+
dockerfile_location: KOSMOS_APP_DOCKERFILE,
|
|
7860
|
+
}));
|
|
7861
|
+
// Every app foundry deploys keeps its own logs (#75), reused as-is here.
|
|
7862
|
+
await this.ensureLogVolume(app.appId, org, app.existed);
|
|
7863
|
+
await this.ports.coolify.updateApplication(app.appId, {
|
|
7864
|
+
...healthCheckSettings('/api/health', KOSMOS_PORT),
|
|
7865
|
+
...(await this.routedLabels(name, kosmosHostnameFor(org), KOSMOS_PORT)),
|
|
7866
|
+
});
|
|
7867
|
+
const liveRows = (await this.ports.coolify.listEnvs(app.appId)).filter((e) => !e.is_preview);
|
|
7868
|
+
const liveEnv = Object.fromEntries(liveRows.map((e) => [e.key, e.value]));
|
|
7869
|
+
const envChanged = kosmosEnvDrift(env, liveEnv).length > 0;
|
|
7870
|
+
const envs = Object.entries(env).map(([key, value]) => ({ key, value }));
|
|
7871
|
+
const cookieSecret = await this.ensureAuthCookieSecret(app.appId);
|
|
7872
|
+
if (cookieSecret)
|
|
7873
|
+
envs.push({ key: 'AUTH_COOKIE_SECRET', value: cookieSecret });
|
|
7874
|
+
await this.ports.coolify.bulkUpsertEnvs(app.appId, envs);
|
|
7875
|
+
const dbPush = await this.pushDatabaseUrl(app.appId, dbId, dbName, kosmosServer ?? undefined);
|
|
7876
|
+
if (!dbPush.pushed) {
|
|
7877
|
+
throw new Error(`kosmos for ${org.slug}: its database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
7878
|
+
}
|
|
7879
|
+
if (!context.recordedIds()[KOSMOS_DEPLOYED_SCOPE_KEY]) {
|
|
7880
|
+
const runtime = this.ports.runtime ?? {};
|
|
7881
|
+
const built = await waitForNewDeployment(this.ports.coolify, app.appId, publicHealthCheck(org, context.recordedIds(), kosmosHostnameFor(org), '/api/health'), runtime, runtime.sleep ?? defaultSleep, 'coolify:kosmos', `${org.slug}'s kosmos stays at its previous deploy`);
|
|
7882
|
+
await recordBuiltCommit(this.ports.coolify, { appId: app.appId, deploymentUuid: built.deploymentUuid, name: 'kosmos' }, (key, value) => context.recordScopeId(key, value));
|
|
7883
|
+
context.recordScopeId(KOSMOS_DEPLOYED_SCOPE_KEY, '1');
|
|
7884
|
+
}
|
|
7885
|
+
else if (app.existed && (envChanged || cookieSecret !== null || dbPush.changed)) {
|
|
7886
|
+
await this.ports.coolify.restart(app.appId);
|
|
7887
|
+
}
|
|
7888
|
+
return { resourceId: app.appId };
|
|
7889
|
+
}
|
|
7455
7890
|
/**
|
|
7456
7891
|
* C3: the Archon credential of an org's Boule. Archon's mint rotates on every call, so it is called only
|
|
7457
7892
|
* when none was pushed or the recorded push time (no secret) is 335 days old. Otherwise the held value is
|
|
@@ -10678,6 +11113,50 @@ export class FoundryStepExecutor {
|
|
|
10678
11113
|
case 'cloudflare:bg-gate':
|
|
10679
11114
|
await this.accessPort().deleteGate(target.id);
|
|
10680
11115
|
return;
|
|
11116
|
+
case 'files:bg-keys':
|
|
11117
|
+
case 'ai:bg-keys': {
|
|
11118
|
+
// Stage's own keys (`{ids, refs}`) in stage's own organisation, revoked before the organisation goes.
|
|
11119
|
+
const { ids } = JSON.parse(target.id);
|
|
11120
|
+
// The organisation foundry recorded; a record from before per-subject names has the shared one.
|
|
11121
|
+
const stageOrgId = recordedIds[stageServiceKey(target.key, 'org')] ?? `${org.id}-stage`;
|
|
11122
|
+
if (target.system === 'files') {
|
|
11123
|
+
const files = await this.filesPort();
|
|
11124
|
+
for (const id of ids)
|
|
11125
|
+
await files.revokeOrganisationKey(stageOrgId, id);
|
|
11126
|
+
}
|
|
11127
|
+
else {
|
|
11128
|
+
const ai = await this.aiPort();
|
|
11129
|
+
// REVOKE SEAM (ai#157): a port with no `revokeKey` leaves it to go with the organisation.
|
|
11130
|
+
if (ai.revokeKey)
|
|
11131
|
+
for (const id of ids)
|
|
11132
|
+
await ai.revokeKey(stageOrgId, id);
|
|
11133
|
+
}
|
|
11134
|
+
return;
|
|
11135
|
+
}
|
|
11136
|
+
case 'files:bg-org':
|
|
11137
|
+
case 'ai:bg-org': {
|
|
11138
|
+
// The organisation id foundry recorded when it made it (named for its subject).
|
|
11139
|
+
const stageOrgId = target.id;
|
|
11140
|
+
const port = target.system === 'files' ? await this.filesPort() : await this.aiPort();
|
|
11141
|
+
if (!port.deleteOrganisation) {
|
|
11142
|
+
throw new Error(`${target.system}:bg-org for ${org.slug}: the ${target.system} port cannot delete an organisation, so ${stageOrgId} stays`);
|
|
11143
|
+
}
|
|
11144
|
+
await port.deleteOrganisation(stageOrgId);
|
|
11145
|
+
return;
|
|
11146
|
+
}
|
|
11147
|
+
case 'jobs:bg-keys':
|
|
11148
|
+
// A jobs project's keys go with the project.
|
|
11149
|
+
return;
|
|
11150
|
+
case 'jobs:bg-org': {
|
|
11151
|
+
// The name recorded when the project was made; a record from before that has the shared `<slug>-stage`.
|
|
11152
|
+
const projectName = recordedIds[stageServiceKey(target.key, 'name')] ?? `${org.slug}-stage`;
|
|
11153
|
+
const scheduler = await this.jobsPort(org);
|
|
11154
|
+
if (!scheduler.deleteProject) {
|
|
11155
|
+
throw new Error(`jobs:bg-org for ${org.slug}: the jobs port cannot delete a project, so ${projectName} stays`);
|
|
11156
|
+
}
|
|
11157
|
+
await scheduler.deleteProject(projectName);
|
|
11158
|
+
return;
|
|
11159
|
+
}
|
|
10681
11160
|
case 'ai:key': {
|
|
10682
11161
|
// F6a: an entry's key (`<orgId>:<entry>`). REVOKE SEAM (ai#157): a
|
|
10683
11162
|
// port with no `revokeKey` leaves it live until it expires.
|
|
@@ -10756,6 +11235,26 @@ export class FoundryStepExecutor {
|
|
|
10756
11235
|
throw error;
|
|
10757
11236
|
}
|
|
10758
11237
|
return;
|
|
11238
|
+
case 'coolify:bg-broker': {
|
|
11239
|
+
// Stage's broker entry (`{name, organizationId}`): taken out of AUTH_APPS with its key.
|
|
11240
|
+
const broker = this.ports.broker;
|
|
11241
|
+
if (!broker) {
|
|
11242
|
+
throw new Error(`coolify:bg-broker for ${org.slug}: the broker port is not wired, cannot remove ${target.key}`);
|
|
11243
|
+
}
|
|
11244
|
+
const { name, organizationId } = JSON.parse(target.id);
|
|
11245
|
+
await removeBrokerEntry(broker, { name, organizationId });
|
|
11246
|
+
return;
|
|
11247
|
+
}
|
|
11248
|
+
case 'zitadel:bg-client': {
|
|
11249
|
+
// Stage's own sign-in client, in production's sign-in organisation and project.
|
|
11250
|
+
const zitadelOrgId = recordedIds.zitadel_org;
|
|
11251
|
+
const projectId = recordedIds.zitadel_project;
|
|
11252
|
+
if (!zitadelOrgId || !projectId) {
|
|
11253
|
+
throw new Error(`zitadel:bg-client for ${org.slug}: no ZITADEL org/project recorded, cannot delete ${target.key}`);
|
|
11254
|
+
}
|
|
11255
|
+
await this.ports.zitadel.deleteApp(zitadelOrgId, projectId, target.id);
|
|
11256
|
+
return;
|
|
11257
|
+
}
|
|
10759
11258
|
case 'cloudflare:bg-access':
|
|
10760
11259
|
await this.accessPort().deleteAccessApp(target.id);
|
|
10761
11260
|
return;
|
|
@@ -10925,3 +11424,10 @@ export async function setOidcAppLoginBaseUri(zitadel, orgId, projectId, appId, l
|
|
|
10925
11424
|
throw error;
|
|
10926
11425
|
}
|
|
10927
11426
|
}
|
|
11427
|
+
/** A recorded stage service key (`bg.<id>.stage<service>keys` or `...org`) with its last part swapped. */
|
|
11428
|
+
function stageServiceKey(key, part) {
|
|
11429
|
+
const match = /^(bg\..+\.stage(?:files|ai|jobs))(?:keys|org)$/.exec(key);
|
|
11430
|
+
if (!match?.[1])
|
|
11431
|
+
throw new Error(`stage service key ${key} is not one`);
|
|
11432
|
+
return `${match[1]}${part}`;
|
|
11433
|
+
}
|