@wtfalch/foundry 0.62.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.
Files changed (75) hide show
  1. package/dist/bootstrap/declare.js +4 -1
  2. package/dist/bootstrap/versions.d.ts +2 -0
  3. package/dist/index.d.ts +6 -2
  4. package/dist/index.js +2 -1
  5. package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
  6. package/dist/migrations/0068_platform_registry.sql +17 -0
  7. package/dist/native-build.d.ts +7 -27
  8. package/dist/native-build.js +7 -27
  9. package/dist/plan/archon-blue-green.d.ts +5 -0
  10. package/dist/plan/archon-blue-green.js +2 -0
  11. package/dist/plan/blue-green-apply.d.ts +5 -0
  12. package/dist/plan/blue-green-apply.js +197 -22
  13. package/dist/plan/blue-green.d.ts +61 -2
  14. package/dist/plan/blue-green.js +80 -6
  15. package/dist/plan/boule.d.ts +1 -1
  16. package/dist/plan/boule.js +2 -0
  17. package/dist/plan/broker-entries.d.ts +9 -0
  18. package/dist/plan/broker-entries.js +29 -0
  19. package/dist/plan/diff.d.ts +5 -0
  20. package/dist/plan/diff.js +4 -0
  21. package/dist/plan/edge-access.d.ts +2 -3
  22. package/dist/plan/edge-access.js +7 -0
  23. package/dist/plan/executor.d.ts +71 -1
  24. package/dist/plan/executor.js +426 -24
  25. package/dist/plan/index.d.ts +2 -0
  26. package/dist/plan/index.js +1 -0
  27. package/dist/plan/jobs-scheduler-http.d.ts +2 -0
  28. package/dist/plan/jobs-scheduler-http.js +11 -0
  29. package/dist/plan/own-limits.d.ts +6 -0
  30. package/dist/plan/own-limits.js +1 -0
  31. package/dist/plan/plan-apply.d.ts +51 -1
  32. package/dist/plan/plan-apply.js +298 -25
  33. package/dist/plan/platform-diff.d.ts +6 -0
  34. package/dist/plan/platform-diff.js +74 -0
  35. package/dist/plan/platform-org.d.ts +2 -0
  36. package/dist/plan/platform-providers.d.ts +46 -1
  37. package/dist/plan/platform-providers.js +156 -1
  38. package/dist/plan/platform-registry-keys.d.ts +58 -0
  39. package/dist/plan/platform-registry-keys.js +69 -0
  40. package/dist/plan/platform-registry.d.ts +67 -0
  41. package/dist/plan/platform-registry.js +298 -0
  42. package/dist/plan/release-host.d.ts +31 -2
  43. package/dist/plan/release-host.js +114 -6
  44. package/dist/plan/stage-copy-host.d.ts +104 -0
  45. package/dist/plan/stage-copy-host.js +259 -0
  46. package/dist/plan/stage-copy.js +44 -0
  47. package/dist/plan/stage-env.d.ts +9 -0
  48. package/dist/plan/stage-env.js +38 -0
  49. package/dist/plan/stage-guards.d.ts +57 -1
  50. package/dist/plan/stage-guards.js +106 -0
  51. package/dist/plan/stage-isolation.js +56 -0
  52. package/dist/plan/stage-services.d.ts +93 -0
  53. package/dist/plan/stage-services.js +153 -0
  54. package/dist/plan/stage-signin.d.ts +152 -0
  55. package/dist/plan/stage-signin.js +133 -0
  56. package/dist/plan/stage-sink.d.ts +2 -0
  57. package/dist/plan/stage-sink.js +6 -1
  58. package/dist/platform.d.ts +7 -1
  59. package/dist/platform.js +1 -0
  60. package/dist/release/decide.d.ts +12 -0
  61. package/dist/release/decide.js +37 -2
  62. package/dist/release/run.d.ts +71 -1
  63. package/dist/release/run.js +92 -8
  64. package/dist/release/state.d.ts +14 -1
  65. package/dist/release/state.js +43 -0
  66. package/dist/release/types.d.ts +20 -0
  67. package/dist/release/types.js +2 -0
  68. package/dist/requested-changes.d.ts +11 -2
  69. package/dist/requested-changes.js +11 -0
  70. package/dist/store/postgres.js +8 -0
  71. package/dist/store/schema.d.ts +51 -0
  72. package/dist/store/schema.js +4 -0
  73. package/package.json +3 -2
  74. package/versions.digests.json +1 -0
  75. package/versions.json +1 -0
@@ -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 { APPS_DOMAIN, APP_ENTRY_KEY, 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';
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';
@@ -69,7 +70,7 @@ 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
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';
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 { stageGateKey, stageHostname, } from './stage-guards.js';
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: () => this.accessPort().createGate({
3621
- organisationId: org.id,
3622
- key: stageGateKey(subject.id),
3623
- entry: subject.id,
3624
- hostnames: [hostname],
3625
- providerId,
3626
- claimName: GATE_CLAIM_NAME,
3627
- claimValue,
3628
- emails,
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) {
@@ -4849,11 +5156,27 @@ export class FoundryStepExecutor {
4849
5156
  async ensureBridgeSecret(appId) {
4850
5157
  return this.mintIfAbsent(appId, BOULE_BRIDGE_SECRET_ENV, 32);
4851
5158
  }
4852
- async mintIfAbsent(appId, key, bytes) {
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) {
4853
5166
  const envs = await this.ports.coolify.listEnvs(appId);
4854
- if (envs.some((e) => e.key === key && !e.is_preview))
4855
- return null;
4856
- return randomBytes(bytes).toString('base64url');
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);
4857
5180
  }
4858
5181
  /**
4859
5182
  * `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
@@ -7347,6 +7670,12 @@ export class FoundryStepExecutor {
7347
7670
  this.handledSecrets.add(ownerSeedSecret);
7348
7671
  envs.push({ key: BOULE_OWNER_SEED_SECRET_ENV, value: ownerSeedSecret });
7349
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
+ }
7350
7679
  // Held, minted once (C3). Compared with the live value so a changed one restarts the app.
7351
7680
  const credentialEnvs = [];
7352
7681
  let archonPushedAt = null;
@@ -7407,6 +7736,7 @@ export class FoundryStepExecutor {
7407
7736
  peerPassword,
7408
7737
  bridgeSecret,
7409
7738
  ownerSeedSecret,
7739
+ nativeBuildKey,
7410
7740
  ...credentialEnvs.map((credential) => credential.value),
7411
7741
  ...grants.secrets,
7412
7742
  ].filter((v) => v !== null);
@@ -7446,6 +7776,7 @@ export class FoundryStepExecutor {
7446
7776
  peerPassword !== null ||
7447
7777
  bridgeSecret !== null ||
7448
7778
  ownerSeedSecret !== null ||
7779
+ nativeBuildKey !== null ||
7449
7780
  secretsNotRestarted ||
7450
7781
  registrarUnstarted ||
7451
7782
  dbPush.changed ||
@@ -10782,6 +11113,50 @@ export class FoundryStepExecutor {
10782
11113
  case 'cloudflare:bg-gate':
10783
11114
  await this.accessPort().deleteGate(target.id);
10784
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
+ }
10785
11160
  case 'ai:key': {
10786
11161
  // F6a: an entry's key (`<orgId>:<entry>`). REVOKE SEAM (ai#157): a
10787
11162
  // port with no `revokeKey` leaves it live until it expires.
@@ -10860,6 +11235,26 @@ export class FoundryStepExecutor {
10860
11235
  throw error;
10861
11236
  }
10862
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
+ }
10863
11258
  case 'cloudflare:bg-access':
10864
11259
  await this.accessPort().deleteAccessApp(target.id);
10865
11260
  return;
@@ -11029,3 +11424,10 @@ export async function setOidcAppLoginBaseUri(zitadel, orgId, projectId, appId, l
11029
11424
  throw error;
11030
11425
  }
11031
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
+ }
@@ -49,6 +49,8 @@ export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execR
49
49
  export type { Runner, StageCopyPortDeps } from './stage-copy-port.js';
50
50
  export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './platform-sfu.js';
51
51
  export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
52
+ export type { PlatformRegistryDeps, PlatformRegistryRuntime } from './platform-registry.js';
53
+ export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './platform-registry-keys.js';
52
54
  export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './mail-copy.js';
53
55
  export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
54
56
  export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './providers-readers.js';
@@ -30,6 +30,7 @@ export { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_IMAGE_REF, STAGE_SINK_SMTP_POR
30
30
  export { STAGE_COPY_EXCLUDED_TABLES, StageCopyFailedError, StageCopyRefusedError, stageCopy, tableExcluded, undoStageCopy, } from './stage-copy.js';
31
31
  export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execRunner, } from './stage-copy-port.js';
32
32
  export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
33
+ export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './platform-registry-keys.js';
33
34
  export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
34
35
  export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeLoginHostResolver, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
35
36
  export { PlatformBrokerPort } from './broker-entries.js';
@@ -26,4 +26,6 @@ export declare class HttpJobsSchedulerClient implements JobsSchedulerPort {
26
26
  secret?: string;
27
27
  syncKey?: string;
28
28
  }>;
29
+ /** `DELETE /admin/projects/<name>`. A refusal throws with the status only. */
30
+ deleteProject(name: string): Promise<void>;
29
31
  }
@@ -50,4 +50,15 @@ export class HttpJobsSchedulerClient {
50
50
  syncKey: typeof fields.syncKey === 'string' ? fields.syncKey : undefined,
51
51
  };
52
52
  }
53
+ /** `DELETE /admin/projects/<name>`. A refusal throws with the status only. */
54
+ async deleteProject(name) {
55
+ const response = await this.fetchImpl(`${this.baseUrl}/admin/projects/${encodeURIComponent(name)}`, {
56
+ method: 'DELETE',
57
+ headers: { authorization: `Bearer ${this.adminToken}` },
58
+ signal: AbortSignal.timeout(ADMIN_CALL_TIMEOUT_MS),
59
+ });
60
+ if (response.status === 200 || response.status === 204)
61
+ return;
62
+ throw Object.assign(new Error(`jobs scheduler: DELETE /admin/projects/<name> answered ${response.status}`), { status: response.status });
63
+ }
53
64
  }
@@ -173,6 +173,12 @@ export declare const LIMITS: {
173
173
  autoDeploy: false | "declared";
174
174
  capabilities: CapabilityTier;
175
175
  };
176
+ readonly platformRegistry: {
177
+ memory: string;
178
+ dbMemory: string;
179
+ autoDeploy: false | "declared";
180
+ capabilities: CapabilityTier;
181
+ };
176
182
  readonly platformMail: {
177
183
  memory: string;
178
184
  dbMemory: string;