@wtfalch/foundry 0.62.0 → 0.64.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/index.d.ts +1 -0
- package/dist/apps/index.js +1 -0
- package/dist/apps/server-entry.d.ts +43 -0
- package/dist/apps/server-entry.js +99 -0
- package/dist/bootstrap/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/build-json.d.ts +25 -0
- package/dist/build-json.js +61 -0
- package/dist/catalogue/copy-en.js +10 -0
- package/dist/catalogue/integrations.js +10 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -2
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/modules/customer-grants.d.ts +3 -0
- package/dist/modules/customer-grants.js +28 -0
- package/dist/modules/index.js +49 -1
- package/dist/modules/repository.d.ts +6 -0
- package/dist/modules/repository.js +40 -0
- package/dist/modules/types.d.ts +8 -0
- package/dist/native-build.d.ts +7 -27
- package/dist/native-build.js +7 -27
- package/dist/org.d.ts +19 -1
- package/dist/org.js +45 -3
- 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 +61 -2
- package/dist/plan/blue-green.js +80 -6
- 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/diff.d.ts +5 -0
- package/dist/plan/diff.js +21 -2
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +7 -0
- package/dist/plan/executor.d.ts +71 -1
- package/dist/plan/executor.js +491 -56
- 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/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -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 +23 -1
- package/dist/plan/platform-diff.js +96 -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 +156 -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/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/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 +11 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
package/dist/plan/executor.js
CHANGED
|
@@ -5,16 +5,17 @@ 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';
|
|
@@ -22,13 +23,13 @@ import { CallError, withTimeout as withLabelledTimeout } from './call-timeout.js
|
|
|
22
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';
|
|
@@ -67,9 +68,9 @@ import { LIMITS, limited } from './own-limits.js';
|
|
|
67
68
|
import { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgNetwork, orgTarget, platformTarget, scopeFromOrg, } from './own-services.js';
|
|
68
69
|
import { ownSfuUrls, provisionSfuApp } from './own-sfu.js';
|
|
69
70
|
import { JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, } from './platform-diff.js';
|
|
70
|
-
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';
|
|
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, appImageAppliedScopeKey, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, entryImageFields, entryImageRef, orgDeclaresMcp, resolveAppEnv, serviceDbEnvAppliedScopeKey, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
71
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';
|
|
72
|
-
import { companyAppPostLogoutUris, postLogoutAppliedValue, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
73
|
+
import { companyAppPostLogoutUris, postLogoutAppliedValue, postLogoutUrisFor, zitadelAppPostLogoutAppliedScopeKey, } from './post-logout-uris.js';
|
|
73
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';
|
|
74
75
|
import { createReleaseHost } from './release-host.js';
|
|
75
76
|
import { TUNNEL_HTTP_INGRESS_SCOPE_KEY, applyRemoteDatabase, mergeTcpIngress, recordedTcpRoutes, } from './remote-database.js';
|
|
@@ -77,7 +78,8 @@ import { editSignInBindings } from './sign-in-bindings.js';
|
|
|
77
78
|
import { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, closeSignInOrgRegistration, ensureSignInOrg, inviteOwnerWithLink, registerSignInDomainOrBlock, signInOrgDomain, } from './sign-in-org.js';
|
|
78
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';
|
|
79
80
|
import { moveSigninDatabase } from './signin-db-move.js';
|
|
80
|
-
import {
|
|
81
|
+
import { buildStageCopyHostPorts, } from './stage-copy-host.js';
|
|
82
|
+
import { membersHash, stageGateEmails, stageGateKey, stageHostname, syncStageGateMembers, } from './stage-guards.js';
|
|
81
83
|
import { WORKER_APP_DB_SCOPE_KEYS, provisionWorkerAppDb, workerAppDbCaPem, workerAppDbOwnerUrl, } from './worker-app-db.js';
|
|
82
84
|
import { WORKER_APP_CALL_TIMEOUT_MS, applyWorkerApp, heldBrokerKey, teardownWorkerApp, withCallTimeout, } from './worker-app.js';
|
|
83
85
|
import { entryWorkerAppliedScopeKey, entryWorkerExclusionsScopeKey, entryWorkerRetiredScopeKey, entryWorkerRouteIdScopeKey, entryWorkerRoutePatternScopeKey, entryWorkerRoutePendingScopeKey, entryWorkerRouteZoneScopeKey, entryWorkerUploadedScopeKey, } from './worker.js';
|
|
@@ -3165,8 +3167,33 @@ export class FoundryStepExecutor {
|
|
|
3165
3167
|
...(this.ports.runtime?.fetch ? { fetch: this.ports.runtime.fetch } : {}),
|
|
3166
3168
|
renew,
|
|
3167
3169
|
label: `coolify:release:${subject.id}`,
|
|
3170
|
+
...this.stageCopyFor(org, subject, context),
|
|
3168
3171
|
});
|
|
3169
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
|
+
}
|
|
3170
3197
|
/** Whether the stage mail sink app is stopped or gone, read live. A failed read throws; the caller reads that as up. */
|
|
3171
3198
|
async stageSinkDown(appId) {
|
|
3172
3199
|
try {
|
|
@@ -3178,6 +3205,13 @@ export class FoundryStepExecutor {
|
|
|
3178
3205
|
throw error;
|
|
3179
3206
|
}
|
|
3180
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
|
+
}
|
|
3181
3215
|
/** Every server Coolify lists with whether it is usable and reachable, for the planner. */
|
|
3182
3216
|
async blueGreenServers() {
|
|
3183
3217
|
return (await this.ports.coolify.listServers()).map((s) => ({
|
|
@@ -3297,7 +3331,8 @@ export class FoundryStepExecutor {
|
|
|
3297
3331
|
primaryBlueAppId,
|
|
3298
3332
|
logDir: LOG_DIR,
|
|
3299
3333
|
isRunning: isRunningOrStarting,
|
|
3300
|
-
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),
|
|
3301
3336
|
stageSink: { slug: org.slug },
|
|
3302
3337
|
};
|
|
3303
3338
|
if (action.kind === 'bluegreen-lb') {
|
|
@@ -3336,6 +3371,8 @@ export class FoundryStepExecutor {
|
|
|
3336
3371
|
const guards = this.ports.stageProbes ? { probes: this.ports.stageProbes } : {};
|
|
3337
3372
|
if (gateId)
|
|
3338
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 };
|
|
3339
3376
|
const runtime = this.ports.runtime ?? {};
|
|
3340
3377
|
const authIssuer = action.desired.authIssuer ?? null;
|
|
3341
3378
|
const signIn = await this.workspaceStageSignIn(org, entry, authIssuer, context);
|
|
@@ -3604,8 +3641,12 @@ export class FoundryStepExecutor {
|
|
|
3604
3641
|
* the company's sign-in organisation, an owner, an Access port) and no `hosting.access` rule names the host: a
|
|
3605
3642
|
* bypass or service-token rule there would open stage, so no gate is made and the guard stays unrecorded.
|
|
3606
3643
|
*/
|
|
3607
|
-
stageGuards(org, subject, recorded, providerId) {
|
|
3644
|
+
stageGuards(org, subject, recorded, providerId, context) {
|
|
3608
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);
|
|
3609
3650
|
const claimValue = recorded[ZITADEL_ORG_SCOPE_KEY];
|
|
3610
3651
|
const emails = org.owners.map((owner) => owner.email);
|
|
3611
3652
|
if (!org.domain || typeof providerId !== 'string' || !providerId || !claimValue)
|
|
@@ -3617,19 +3658,285 @@ export class FoundryStepExecutor {
|
|
|
3617
3658
|
return guards;
|
|
3618
3659
|
guards.gate = {
|
|
3619
3660
|
hostname,
|
|
3620
|
-
create: () =>
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
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
|
+
},
|
|
3630
3683
|
};
|
|
3631
3684
|
return guards;
|
|
3632
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
|
+
}
|
|
3633
3940
|
async coolifyApp(action, org, context) {
|
|
3634
3941
|
const recordedAppId = context.recordedIds()[`coolify:app:${action.key}`];
|
|
3635
3942
|
if (action.desired.enabled === false) {
|
|
@@ -3656,7 +3963,7 @@ export class FoundryStepExecutor {
|
|
|
3656
3963
|
throw new Error(`coolify:app for ${org.slug}: no hosting entry for ${action.key}`);
|
|
3657
3964
|
// `planApp` blocks a `repo: null` entry at plan time; reaching here with
|
|
3658
3965
|
// one means a stale or hand-built plan.
|
|
3659
|
-
if (entry.repo === null) {
|
|
3966
|
+
if (entry.repo === null && entry.image === undefined) {
|
|
3660
3967
|
throw new Error(`coolify:app for ${org.slug}: entry '${entry.key}' has no repo`);
|
|
3661
3968
|
}
|
|
3662
3969
|
const appRepo = entry.repo;
|
|
@@ -3745,35 +4052,55 @@ export class FoundryStepExecutor {
|
|
|
3745
4052
|
}
|
|
3746
4053
|
else {
|
|
3747
4054
|
const server = await this.resolveServer(org, context);
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
4055
|
+
if (entry.image) {
|
|
4056
|
+
// A built image: pulled by digest, nothing to build. Coolify never deploys it by itself on create.
|
|
4057
|
+
const created = await this.ports.coolify.createDockerImageApplication({
|
|
4058
|
+
project_uuid: projectUuid,
|
|
4059
|
+
server_uuid: server.uuid,
|
|
4060
|
+
environment_name: environment.name,
|
|
4061
|
+
environment_uuid: environment.uuid,
|
|
4062
|
+
...entryImageFields(entry.image),
|
|
4063
|
+
// Coolify needs a port to create the app; a helper with none listens on none, so 80 is only its label.
|
|
4064
|
+
ports_exposes: String(entry.port ?? 80),
|
|
4065
|
+
name: appName,
|
|
4066
|
+
description: `${org.slug}'s ${entry.key} app, run from a built image`,
|
|
4067
|
+
instant_deploy: false,
|
|
4068
|
+
});
|
|
4069
|
+
appId = created.uuid;
|
|
4070
|
+
have = appSettingsOf(entry);
|
|
4071
|
+
haveDomains = wantDomains;
|
|
4072
|
+
}
|
|
4073
|
+
else {
|
|
4074
|
+
const githubAppUuid = await this.githubAppUuidFor(org, appRepo, context.recordedIds());
|
|
4075
|
+
if (isServerWorkspace(entry)) {
|
|
4076
|
+
pinnedCommit =
|
|
4077
|
+
entry.liveCommit ?? (await this.workspaceCommit(org, entry, server.uuid, context));
|
|
4078
|
+
}
|
|
4079
|
+
const created = await this.ports.coolify.createGithubAppApplication({
|
|
4080
|
+
is_preview_deployments_enabled: false,
|
|
4081
|
+
project_uuid: projectUuid,
|
|
4082
|
+
server_uuid: server.uuid,
|
|
4083
|
+
environment_name: environment.name,
|
|
4084
|
+
environment_uuid: environment.uuid,
|
|
4085
|
+
github_app_uuid: githubAppUuid,
|
|
4086
|
+
git_repository: appRepo,
|
|
4087
|
+
git_branch: entry.branch,
|
|
4088
|
+
build_pack: entry.buildPack,
|
|
4089
|
+
name: appName,
|
|
4090
|
+
autogenerate_domain: false,
|
|
4091
|
+
// A static site is served by Coolify's own web server on port 80.
|
|
4092
|
+
ports_exposes: isStatic ? '80' : entry.port === null ? undefined : String(entry.port),
|
|
4093
|
+
is_auto_deploy_enabled: appSettingsOf(entry).autoDeploy,
|
|
4094
|
+
// Also the folder a static entry serves: Coolify's static build
|
|
4095
|
+
// copies `base_directory`, and reads `publish_directory` only for a
|
|
4096
|
+
// built app.
|
|
4097
|
+
base_directory: entry.folder === null ? undefined : `/${entry.folder}`,
|
|
4098
|
+
});
|
|
4099
|
+
appId = created.uuid;
|
|
4100
|
+
// Create sent every setting, so nothing is left to patch.
|
|
4101
|
+
have = appSettingsOf(entry);
|
|
4102
|
+
haveDomains = wantDomains;
|
|
3752
4103
|
}
|
|
3753
|
-
const created = await this.ports.coolify.createGithubAppApplication({
|
|
3754
|
-
is_preview_deployments_enabled: false,
|
|
3755
|
-
project_uuid: projectUuid,
|
|
3756
|
-
server_uuid: server.uuid,
|
|
3757
|
-
environment_name: environment.name,
|
|
3758
|
-
environment_uuid: environment.uuid,
|
|
3759
|
-
github_app_uuid: githubAppUuid,
|
|
3760
|
-
git_repository: appRepo,
|
|
3761
|
-
git_branch: entry.branch,
|
|
3762
|
-
build_pack: entry.buildPack,
|
|
3763
|
-
name: appName,
|
|
3764
|
-
autogenerate_domain: false,
|
|
3765
|
-
// A static site is served by Coolify's own web server on port 80.
|
|
3766
|
-
ports_exposes: isStatic ? '80' : entry.port === null ? undefined : String(entry.port),
|
|
3767
|
-
is_auto_deploy_enabled: appSettingsOf(entry).autoDeploy,
|
|
3768
|
-
// Also the folder a static entry serves: Coolify's static build
|
|
3769
|
-
// copies `base_directory`, and reads `publish_directory` only for a
|
|
3770
|
-
// built app.
|
|
3771
|
-
base_directory: entry.folder === null ? undefined : `/${entry.folder}`,
|
|
3772
|
-
});
|
|
3773
|
-
appId = created.uuid;
|
|
3774
|
-
// Create sent every setting, so nothing is left to patch.
|
|
3775
|
-
have = appSettingsOf(entry);
|
|
3776
|
-
haveDomains = wantDomains;
|
|
3777
4104
|
}
|
|
3778
4105
|
// Patch what differs from what the app has. A crash before the marker is
|
|
3779
4106
|
// recorded below leaves the old one, so the retry patches the same
|
|
@@ -3793,6 +4120,13 @@ export class FoundryStepExecutor {
|
|
|
3793
4120
|
patch.is_preview_deployments_enabled = false;
|
|
3794
4121
|
if (pinnedCommit)
|
|
3795
4122
|
patch.git_commit_sha = pinnedCommit;
|
|
4123
|
+
// A new image reference reaches a live app: the digest is patched and the app deploys below.
|
|
4124
|
+
const imageRef = entry.image ? entryImageRef(entry.image) : null;
|
|
4125
|
+
const imageChanged = entry.image !== undefined &&
|
|
4126
|
+
existing !== null &&
|
|
4127
|
+
context.recordedIds()[appImageAppliedScopeKey(entry.key)] !== imageRef;
|
|
4128
|
+
if (entry.image && imageChanged)
|
|
4129
|
+
Object.assign(patch, entryImageFields(entry.image));
|
|
3796
4130
|
// #543: the health-check quartet too, every run -- the same reason
|
|
3797
4131
|
// Archon's own doc comment gives (without one, Coolify's rolling update
|
|
3798
4132
|
// has nothing to wait on and stops the old container immediately). The
|
|
@@ -3841,7 +4175,8 @@ export class FoundryStepExecutor {
|
|
|
3841
4175
|
if (Object.keys(patch).length > 0)
|
|
3842
4176
|
await this.ports.coolify.updateApplication(appId, patch);
|
|
3843
4177
|
// The container builds and listens from these only at its next deploy.
|
|
3844
|
-
const settingsNeedDeploy = Boolean(
|
|
4178
|
+
const settingsNeedDeploy = Boolean(imageChanged ||
|
|
4179
|
+
patch.base_directory ||
|
|
3845
4180
|
patch.ports_exposes ||
|
|
3846
4181
|
patch.domains !== undefined ||
|
|
3847
4182
|
patch.custom_internal_name ||
|
|
@@ -3911,7 +4246,10 @@ export class FoundryStepExecutor {
|
|
|
3911
4246
|
entryRoutesMarker(org, context.recordedIds())
|
|
3912
4247
|
? publicHealthCheck(org, context.recordedIds(), hostname, healthPath)
|
|
3913
4248
|
: { kind: 'deployment-only' }, runtime, runtime.sleep ?? defaultSleep, `coolify:app:${entry.key}`, `${org.slug}'s ${entry.key === APP_ENTRY_KEY ? 'app' : `${entry.key} app`} stays at its previous deploy`);
|
|
3914
|
-
|
|
4249
|
+
// An image app has no commit to read: its digest is the build.
|
|
4250
|
+
if (!entry.image) {
|
|
4251
|
+
await recordBuiltCommit(this.ports.coolify, { appId: appId, deploymentUuid: built.deploymentUuid, name: entry.key }, (key, value) => context.recordScopeId(key, value));
|
|
4252
|
+
}
|
|
3915
4253
|
}
|
|
3916
4254
|
else if ((appEnv.changed || dbNeedsRestart) && existing) {
|
|
3917
4255
|
await this.ports.coolify.restart(appId);
|
|
@@ -3923,6 +4261,8 @@ export class FoundryStepExecutor {
|
|
|
3923
4261
|
context.recordScopeId(settingsKey, JSON.stringify({ ...want, port: want.port ?? have.port }));
|
|
3924
4262
|
context.recordScopeId(domainsKey, JSON.stringify(wantDomains));
|
|
3925
4263
|
context.recordScopeId(labelsKey, wantLabelsHash);
|
|
4264
|
+
if (imageRef)
|
|
4265
|
+
context.recordScopeId(appImageAppliedScopeKey(entry.key), imageRef);
|
|
3926
4266
|
if (pinnedCommit && pinnedCommit === entry.liveCommit) {
|
|
3927
4267
|
context.recordScopeId(wsKey.liveCommit(entry.key), pinnedCommit);
|
|
3928
4268
|
}
|
|
@@ -4849,11 +5189,27 @@ export class FoundryStepExecutor {
|
|
|
4849
5189
|
async ensureBridgeSecret(appId) {
|
|
4850
5190
|
return this.mintIfAbsent(appId, BOULE_BRIDGE_SECRET_ENV, 32);
|
|
4851
5191
|
}
|
|
4852
|
-
|
|
5192
|
+
/**
|
|
5193
|
+
* `minLength`, when given, also counts a present-but-empty value as not set -- the same guard
|
|
5194
|
+
* {@link ensureAuthAppKey} already applies for `AUTH_APP_KEY`, now reusable here instead of a
|
|
5195
|
+
* third, bare copy of this check (foundry#970, thermo note 1 in #957). Omitted, the three existing
|
|
5196
|
+
* callers below keep their original behaviour.
|
|
5197
|
+
*/
|
|
5198
|
+
async mintIfAbsent(appId, key, bytes, minLength) {
|
|
4853
5199
|
const envs = await this.ports.coolify.listEnvs(appId);
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
5200
|
+
const already = envs.some((e) => e.key === key &&
|
|
5201
|
+
!e.is_preview &&
|
|
5202
|
+
(minLength === undefined || (e.value ?? '').length >= minLength));
|
|
5203
|
+
return already ? null : randomBytes(bytes).toString('base64url');
|
|
5204
|
+
}
|
|
5205
|
+
/**
|
|
5206
|
+
* Every company's Boule, F3/#970: `NATIVE_BUILD_KEY` is what Boule presents to Archon's
|
|
5207
|
+
* native-build route (estate#262) to ask it to dispatch `build-app.yml`. Minted once, like
|
|
5208
|
+
* {@link ensureAuthAppKey}, and never rotated; a present-but-empty value (the bug
|
|
5209
|
+
* `mintNativeBuildKey`'s own read-then-generate had, see #957) is treated as not set.
|
|
5210
|
+
*/
|
|
5211
|
+
async ensureNativeBuildKey(appId) {
|
|
5212
|
+
return this.mintIfAbsent(appId, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, 1);
|
|
4857
5213
|
}
|
|
4858
5214
|
/**
|
|
4859
5215
|
* `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
|
|
@@ -7347,6 +7703,12 @@ export class FoundryStepExecutor {
|
|
|
7347
7703
|
this.handledSecrets.add(ownerSeedSecret);
|
|
7348
7704
|
envs.push({ key: BOULE_OWNER_SEED_SECRET_ENV, value: ownerSeedSecret });
|
|
7349
7705
|
}
|
|
7706
|
+
// The key Boule presents to Archon's native-build route, for every org's Boule (foundry#970).
|
|
7707
|
+
const nativeBuildKey = await this.ensureNativeBuildKey(app.appId);
|
|
7708
|
+
if (nativeBuildKey) {
|
|
7709
|
+
this.handledSecrets.add(nativeBuildKey);
|
|
7710
|
+
envs.push({ key: NATIVE_BUILD_KEY_ENV, value: nativeBuildKey });
|
|
7711
|
+
}
|
|
7350
7712
|
// Held, minted once (C3). Compared with the live value so a changed one restarts the app.
|
|
7351
7713
|
const credentialEnvs = [];
|
|
7352
7714
|
let archonPushedAt = null;
|
|
@@ -7407,6 +7769,7 @@ export class FoundryStepExecutor {
|
|
|
7407
7769
|
peerPassword,
|
|
7408
7770
|
bridgeSecret,
|
|
7409
7771
|
ownerSeedSecret,
|
|
7772
|
+
nativeBuildKey,
|
|
7410
7773
|
...credentialEnvs.map((credential) => credential.value),
|
|
7411
7774
|
...grants.secrets,
|
|
7412
7775
|
].filter((v) => v !== null);
|
|
@@ -7446,6 +7809,7 @@ export class FoundryStepExecutor {
|
|
|
7446
7809
|
peerPassword !== null ||
|
|
7447
7810
|
bridgeSecret !== null ||
|
|
7448
7811
|
ownerSeedSecret !== null ||
|
|
7812
|
+
nativeBuildKey !== null ||
|
|
7449
7813
|
secretsNotRestarted ||
|
|
7450
7814
|
registrarUnstarted ||
|
|
7451
7815
|
dbPush.changed ||
|
|
@@ -10782,6 +11146,50 @@ export class FoundryStepExecutor {
|
|
|
10782
11146
|
case 'cloudflare:bg-gate':
|
|
10783
11147
|
await this.accessPort().deleteGate(target.id);
|
|
10784
11148
|
return;
|
|
11149
|
+
case 'files:bg-keys':
|
|
11150
|
+
case 'ai:bg-keys': {
|
|
11151
|
+
// Stage's own keys (`{ids, refs}`) in stage's own organisation, revoked before the organisation goes.
|
|
11152
|
+
const { ids } = JSON.parse(target.id);
|
|
11153
|
+
// The organisation foundry recorded; a record from before per-subject names has the shared one.
|
|
11154
|
+
const stageOrgId = recordedIds[stageServiceKey(target.key, 'org')] ?? `${org.id}-stage`;
|
|
11155
|
+
if (target.system === 'files') {
|
|
11156
|
+
const files = await this.filesPort();
|
|
11157
|
+
for (const id of ids)
|
|
11158
|
+
await files.revokeOrganisationKey(stageOrgId, id);
|
|
11159
|
+
}
|
|
11160
|
+
else {
|
|
11161
|
+
const ai = await this.aiPort();
|
|
11162
|
+
// REVOKE SEAM (ai#157): a port with no `revokeKey` leaves it to go with the organisation.
|
|
11163
|
+
if (ai.revokeKey)
|
|
11164
|
+
for (const id of ids)
|
|
11165
|
+
await ai.revokeKey(stageOrgId, id);
|
|
11166
|
+
}
|
|
11167
|
+
return;
|
|
11168
|
+
}
|
|
11169
|
+
case 'files:bg-org':
|
|
11170
|
+
case 'ai:bg-org': {
|
|
11171
|
+
// The organisation id foundry recorded when it made it (named for its subject).
|
|
11172
|
+
const stageOrgId = target.id;
|
|
11173
|
+
const port = target.system === 'files' ? await this.filesPort() : await this.aiPort();
|
|
11174
|
+
if (!port.deleteOrganisation) {
|
|
11175
|
+
throw new Error(`${target.system}:bg-org for ${org.slug}: the ${target.system} port cannot delete an organisation, so ${stageOrgId} stays`);
|
|
11176
|
+
}
|
|
11177
|
+
await port.deleteOrganisation(stageOrgId);
|
|
11178
|
+
return;
|
|
11179
|
+
}
|
|
11180
|
+
case 'jobs:bg-keys':
|
|
11181
|
+
// A jobs project's keys go with the project.
|
|
11182
|
+
return;
|
|
11183
|
+
case 'jobs:bg-org': {
|
|
11184
|
+
// The name recorded when the project was made; a record from before that has the shared `<slug>-stage`.
|
|
11185
|
+
const projectName = recordedIds[stageServiceKey(target.key, 'name')] ?? `${org.slug}-stage`;
|
|
11186
|
+
const scheduler = await this.jobsPort(org);
|
|
11187
|
+
if (!scheduler.deleteProject) {
|
|
11188
|
+
throw new Error(`jobs:bg-org for ${org.slug}: the jobs port cannot delete a project, so ${projectName} stays`);
|
|
11189
|
+
}
|
|
11190
|
+
await scheduler.deleteProject(projectName);
|
|
11191
|
+
return;
|
|
11192
|
+
}
|
|
10785
11193
|
case 'ai:key': {
|
|
10786
11194
|
// F6a: an entry's key (`<orgId>:<entry>`). REVOKE SEAM (ai#157): a
|
|
10787
11195
|
// port with no `revokeKey` leaves it live until it expires.
|
|
@@ -10860,6 +11268,26 @@ export class FoundryStepExecutor {
|
|
|
10860
11268
|
throw error;
|
|
10861
11269
|
}
|
|
10862
11270
|
return;
|
|
11271
|
+
case 'coolify:bg-broker': {
|
|
11272
|
+
// Stage's broker entry (`{name, organizationId}`): taken out of AUTH_APPS with its key.
|
|
11273
|
+
const broker = this.ports.broker;
|
|
11274
|
+
if (!broker) {
|
|
11275
|
+
throw new Error(`coolify:bg-broker for ${org.slug}: the broker port is not wired, cannot remove ${target.key}`);
|
|
11276
|
+
}
|
|
11277
|
+
const { name, organizationId } = JSON.parse(target.id);
|
|
11278
|
+
await removeBrokerEntry(broker, { name, organizationId });
|
|
11279
|
+
return;
|
|
11280
|
+
}
|
|
11281
|
+
case 'zitadel:bg-client': {
|
|
11282
|
+
// Stage's own sign-in client, in production's sign-in organisation and project.
|
|
11283
|
+
const zitadelOrgId = recordedIds.zitadel_org;
|
|
11284
|
+
const projectId = recordedIds.zitadel_project;
|
|
11285
|
+
if (!zitadelOrgId || !projectId) {
|
|
11286
|
+
throw new Error(`zitadel:bg-client for ${org.slug}: no ZITADEL org/project recorded, cannot delete ${target.key}`);
|
|
11287
|
+
}
|
|
11288
|
+
await this.ports.zitadel.deleteApp(zitadelOrgId, projectId, target.id);
|
|
11289
|
+
return;
|
|
11290
|
+
}
|
|
10863
11291
|
case 'cloudflare:bg-access':
|
|
10864
11292
|
await this.accessPort().deleteAccessApp(target.id);
|
|
10865
11293
|
return;
|
|
@@ -11029,3 +11457,10 @@ export async function setOidcAppLoginBaseUri(zitadel, orgId, projectId, appId, l
|
|
|
11029
11457
|
throw error;
|
|
11030
11458
|
}
|
|
11031
11459
|
}
|
|
11460
|
+
/** A recorded stage service key (`bg.<id>.stage<service>keys` or `...org`) with its last part swapped. */
|
|
11461
|
+
function stageServiceKey(key, part) {
|
|
11462
|
+
const match = /^(bg\..+\.stage(?:files|ai|jobs))(?:keys|org)$/.exec(key);
|
|
11463
|
+
if (!match?.[1])
|
|
11464
|
+
throw new Error(`stage service key ${key} is not one`);
|
|
11465
|
+
return `${match[1]}${part}`;
|
|
11466
|
+
}
|