@wtfalch/foundry 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/index.d.ts +5 -3
  2. package/dist/index.js +3 -2
  3. package/dist/org.d.ts +37 -0
  4. package/dist/org.js +135 -10
  5. package/dist/plan/agora.d.ts +14 -0
  6. package/dist/plan/agora.js +21 -0
  7. package/dist/plan/backup-gate.d.ts +15 -0
  8. package/dist/plan/backup-gate.js +38 -0
  9. package/dist/plan/boule-owners-http.d.ts +12 -0
  10. package/dist/plan/boule-owners-http.js +59 -0
  11. package/dist/plan/boule.d.ts +38 -2
  12. package/dist/plan/boule.js +72 -1
  13. package/dist/plan/company-backups.d.ts +7 -1
  14. package/dist/plan/company-backups.js +42 -2
  15. package/dist/plan/cookie-domain.d.ts +41 -0
  16. package/dist/plan/cookie-domain.js +71 -0
  17. package/dist/plan/desktop-auth.d.ts +58 -0
  18. package/dist/plan/desktop-auth.js +144 -0
  19. package/dist/plan/desktop-callback.d.ts +26 -0
  20. package/dist/plan/desktop-callback.js +59 -0
  21. package/dist/plan/diff.d.ts +13 -4
  22. package/dist/plan/diff.js +272 -84
  23. package/dist/plan/executor.d.ts +136 -6
  24. package/dist/plan/executor.js +725 -98
  25. package/dist/plan/health-gaps.js +5 -0
  26. package/dist/plan/index.d.ts +3 -0
  27. package/dist/plan/index.js +2 -0
  28. package/dist/plan/job-app.d.ts +23 -0
  29. package/dist/plan/job-app.js +41 -0
  30. package/dist/plan/live.d.ts +6 -0
  31. package/dist/plan/live.js +16 -0
  32. package/dist/plan/login-host.d.ts +28 -0
  33. package/dist/plan/login-host.js +36 -0
  34. package/dist/plan/org-names.js +9 -1
  35. package/dist/plan/org-postgres-job.d.ts +22 -0
  36. package/dist/plan/org-postgres-job.js +91 -0
  37. package/dist/plan/org-postgres-migrate.d.ts +115 -0
  38. package/dist/plan/org-postgres-migrate.js +466 -0
  39. package/dist/plan/org-postgres.d.ts +90 -0
  40. package/dist/plan/org-postgres.js +293 -0
  41. package/dist/plan/plan-apply.d.ts +30 -2
  42. package/dist/plan/plan-apply.js +104 -7
  43. package/dist/plan/platform-diff.d.ts +45 -0
  44. package/dist/plan/platform-diff.js +159 -4
  45. package/dist/plan/platform-org.d.ts +2 -0
  46. package/dist/plan/platform-org.js +4 -0
  47. package/dist/plan/platform-providers.d.ts +12 -2
  48. package/dist/plan/platform-providers.js +279 -9
  49. package/dist/plan/post-logout-uris.d.ts +22 -0
  50. package/dist/plan/post-logout-uris.js +43 -0
  51. package/dist/plan/providers-readers.d.ts +10 -1
  52. package/dist/plan/providers-readers.js +41 -1
  53. package/dist/plan/sign-in-bindings.d.ts +4 -0
  54. package/dist/plan/sign-in-bindings.js +10 -0
  55. package/dist/plan/sign-in-org.d.ts +19 -6
  56. package/dist/plan/sign-in-org.js +35 -10
  57. package/dist/plan/sign-in.d.ts +12 -5
  58. package/dist/plan/sign-in.js +12 -5
  59. package/dist/plan/signin-db-move.js +10 -34
  60. package/package.json +5 -5
@@ -11,21 +11,24 @@ import { recordBuiltCommit } from './build-record.js';
11
11
  import { ensureCloudflaredConnector } from './cloudflared-connector.js';
12
12
  import { previewsOn } from './coolify-previews.js';
13
13
  import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
14
+ import { desktopCallbackUri, unionRedirectUris } from './desktop-callback.js';
14
15
  import { placedOnPlatform } from './destinations.js';
15
16
  import { ACCESS_POLICY_NAME, LOG_DIR, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
16
17
  import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
17
18
  import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, tcpProbe, } from './health-check-settings.js';
18
19
  import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
19
20
  import { LIMITS, limited } from './own-limits.js';
21
+ import { postLogoutAppliedValue, postLogoutUrisFor } from './post-logout-uris.js';
20
22
  import { createReleaseHost } from './release-host.js';
21
23
  export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
24
+ import { SIGN_IN_BIND_KEY_ENV, SIGN_IN_BIND_KEY_MIN_LENGTH, SIGN_IN_BIND_KEY_SCOPE_KEY, } from './cookie-domain.js';
22
25
  import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
23
26
  import { MAIL_RELAY_BLOCKED, provisionMailRelay } from './mail-relay.js';
24
27
  import { MAIL_APP_SCOPE_KEY, MAIL_DATABASE_NAME, MAIL_DATABASE_PASSWORD_ENV, MAIL_DATABASE_SCOPE_KEY, MAIL_STORE_POSTGRES, MAIL_STORE_SCOPE_KEY, mailStoreIsLegacy, } from './mail-store.js';
25
28
  import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
26
29
  import { recordOwnDnsHostname } from './own-dns.js';
27
30
  import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
28
- import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_ACCESS_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
31
+ import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_ACCESS_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_USER_ROLE, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
29
32
  import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
30
33
  import { LOGIN_HOST_BINDINGS_ENV } from './sign-in-bindings.js';
31
34
  import { ensureSignInOrg, inviteOwnerWithLink, signInOrgDomain } from './sign-in-org.js';
@@ -497,12 +500,32 @@ export class FoundryPlatformStepExecutor {
497
500
  : undefined;
498
501
  if (zitadel?.updateOidcAppIdTokenUserinfoAssertion)
499
502
  blocked.archonIdTokenUserinfo = true;
503
+ if (zitadel?.updateOidcAppPostLogoutRedirectUris)
504
+ blocked.archonPostLogoutUris = true;
505
+ if (zitadel?.getOidcAppRedirectUris && zitadel.updateOidcAppRedirectUris) {
506
+ blocked.archonDesktopCallback = true;
507
+ }
500
508
  }
501
509
  catch (e) {
502
510
  // No ZITADEL client for this instance: the flag stays unset and the repair is not planned.
503
511
  console.error(`platform:archon: could not reach the platform's ZITADEL client to plan the ID token repair: ${e instanceof Error ? e.message : String(e)}`);
504
512
  }
505
513
  }
514
+ // Not a block: without a client that can make a project role and grant it, the mail role step is not planned.
515
+ if (this.mail && platform.mail.enabled) {
516
+ try {
517
+ const mailZitadel = this.mail.zitadelForInstance?.(`https://${platform.auth.hostname}`) ?? this.mail.zitadel;
518
+ if (mailZitadel.ensureProjectRole && mailZitadel.grantProjectRoles) {
519
+ blocked.mailRole = true;
520
+ if (this.mail.requireMailRole && mailZitadel.setProjectRoleChecks) {
521
+ blocked.mailRoleCheck = true;
522
+ }
523
+ }
524
+ }
525
+ catch (e) {
526
+ console.error(`platform:mail: could not reach the platform's ZITADEL client to plan the mail role: ${e instanceof Error ? e.message : String(e)}`);
527
+ }
528
+ }
506
529
  // Not a block: without the auth dependencies and a ZITADEL client for the instance the sign-in policy step is not planned.
507
530
  if (this.auth && platform.auth.enabled && recordedIds[PLATFORM_ZITADEL_ORG_SCOPE_KEY]) {
508
531
  try {
@@ -684,6 +707,8 @@ export class FoundryPlatformStepExecutor {
684
707
  if (loginClientDrifted || (await this.signInLoginDrifted(this.auth, recordedIds))) {
685
708
  result.auth = true;
686
709
  }
710
+ if (await authAppsNotRunning(this.auth.coolify, recordedIds))
711
+ result.authApps = true;
687
712
  }
688
713
  // #380: once the runner app is recorded, re-check its live env against
689
714
  // every setting `ensureStarted()` requires -- the gap that left the
@@ -757,6 +782,9 @@ export class FoundryPlatformStepExecutor {
757
782
  const loginId = recordedIds[PLATFORM_AUTH_LOGIN_SCOPE_KEY];
758
783
  if (!signInId)
759
784
  return true;
785
+ // The session bind key is not yet live on the in-app login: plan again and mint it.
786
+ if (recordedIds[SIGN_IN_BIND_KEY_SCOPE_KEY] !== '1')
787
+ return true;
760
788
  if (!loginId)
761
789
  return false;
762
790
  try {
@@ -779,7 +807,7 @@ export class FoundryPlatformStepExecutor {
779
807
  throw new Error('platform:auth needs FoundryPlatformStepExecutor to be constructed with its auth dependencies (#168)');
780
808
  }
781
809
  const company = await this.declaredOrgStore.get(context.platform.ownerOrgId);
782
- return provisionPlatformAuth(context, this.limitedOnPlatform(this.auth, LIMITS.platformAuth), company, await this.signInLoginWanted());
810
+ return provisionPlatformAuth(context, this.limitedOnPlatform(this.auth, LIMITS.platformAuth), { ownerCompany: company, signInLogin: await this.signInLoginWanted() });
783
811
  }
784
812
  if (action.system === 'coolify' && action.kind === 'app' && action.key === PLATFORM_FILES_KEY) {
785
813
  if (!this.files) {
@@ -857,6 +885,30 @@ export class FoundryPlatformStepExecutor {
857
885
  }
858
886
  return switchOnArchonIdTokenUserinfo(context, this.archon);
859
887
  }
888
+ if (action.system === 'zitadel' &&
889
+ action.kind === 'post-logout-uris' &&
890
+ action.key === PLATFORM_ARCHON_KEY) {
891
+ if (!this.archon) {
892
+ throw new Error('platform:archon needs FoundryPlatformStepExecutor to be constructed with its archon dependencies (#175)');
893
+ }
894
+ return updateArchonPostLogoutUris(context, this.archon);
895
+ }
896
+ if (action.system === 'zitadel' &&
897
+ action.kind === 'desktop-callback' &&
898
+ action.key === PLATFORM_ARCHON_KEY) {
899
+ if (!this.archon) {
900
+ throw new Error('platform:archon needs FoundryPlatformStepExecutor to be constructed with its archon dependencies (#175)');
901
+ }
902
+ return registerArchonDesktopCallback(context, this.archon);
903
+ }
904
+ if (action.system === 'zitadel' &&
905
+ action.kind === 'mail-role' &&
906
+ action.key === PLATFORM_MAIL_KEY) {
907
+ if (!this.mail) {
908
+ throw new Error('platform:mail needs FoundryPlatformStepExecutor to be constructed with its mail dependencies (#169)');
909
+ }
910
+ return applyPlatformMailRole(context, this.mail);
911
+ }
860
912
  if (action.system === 'zitadel' &&
861
913
  action.kind === 'sign-in-policy' &&
862
914
  action.key === PLATFORM_AUTH_KEY) {
@@ -1691,10 +1743,16 @@ export async function provisionAuthStack(scope, deps, target, input) {
1691
1743
  });
1692
1744
  const loginAppId = loginApp.appId;
1693
1745
  await record(PLATFORM_AUTH_LOGIN_SCOPE_KEY, loginAppId);
1746
+ // A declared `gitRef` that differs from the one the apps were last built from reaches all three apps
1747
+ // that build from the auth repo: `git_branch` is patched and each is redeployed below.
1748
+ const authRefChanged = target.kind === 'platform' && recorded[PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY] !== gitRef;
1749
+ // An app made in this run was created on `gitRef` already.
1750
+ const gitBranchPatchFor = (app) => authRefChanged && !app.justCreated ? { git_branch: gitRef } : {};
1694
1751
  // The Dockerfile's own HEALTHCHECK is the live check (`IMAGE_HEALTHCHECK_SETTINGS`).
1695
1752
  await deps.coolify.updateApplication(loginAppId, {
1696
1753
  custom_internal_name: target.internal.authLogin,
1697
1754
  ...IMAGE_HEALTHCHECK_SETTINGS,
1755
+ ...gitBranchPatchFor(loginApp),
1698
1756
  });
1699
1757
  // ---- the in-app login app (`/sign-in`) ------------------------------------
1700
1758
  // A Next.js base path is fixed at build, so the login that serves `/sign-in` is a second app built from
@@ -1728,6 +1786,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
1728
1786
  await deps.coolify.updateApplication(signInApp.appId, {
1729
1787
  custom_internal_name: signInNames.internal,
1730
1788
  ...IMAGE_HEALTHCHECK_SETTINGS,
1789
+ ...gitBranchPatchFor(signInApp),
1731
1790
  });
1732
1791
  }
1733
1792
  // ---- the broker app (#204) -------------------------------------------------
@@ -1753,6 +1812,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
1753
1812
  await deps.coolify.updateApplication(brokerAppId, {
1754
1813
  custom_internal_name: target.internal.authBroker,
1755
1814
  ...IMAGE_HEALTHCHECK_SETTINGS,
1815
+ ...gitBranchPatchFor(brokerApp),
1756
1816
  });
1757
1817
  // ---- env -----------------------------------------------------------------
1758
1818
  // Generated once, handed straight to `bulkUpsertEnvs` and nowhere else --
@@ -1871,12 +1931,23 @@ export async function provisionAuthStack(scope, deps, target, input) {
1871
1931
  await deps.coolify.bulkUpsertEnvs(brokerAppId, brokerEnvs);
1872
1932
  // The in-app login: the login's env, the base path as a build argument, and the host table.
1873
1933
  let signInEnvsChanged = false;
1934
+ // The session bind key is minted once, on the in-app login app only: a key already live (32 characters or
1935
+ // more) is kept, so a rerun after a crash mints nothing. It goes to this app's env and nowhere else.
1936
+ let bindKeyWritten = false;
1874
1937
  if (signInApp && input.signInLogin) {
1875
1938
  const bindings = input.signInLogin.bindings ??
1876
1939
  (await currentEnvValue(deps.coolify, loginAppId, LOGIN_HOST_BINDINGS_ENV));
1940
+ const mintedBindKey = await ensureGeneratedEnv(deps.coolify, signInApp.appId, SIGN_IN_BIND_KEY_ENV, () => randomBytes(32).toString('hex'), SIGN_IN_BIND_KEY_MIN_LENGTH);
1941
+ const bindKey = mintedBindKey ?? (await currentEnvValue(deps.coolify, signInApp.appId, SIGN_IN_BIND_KEY_ENV));
1942
+ if (!bindKey) {
1943
+ throw new Error(`${label}: the in-app login app lost its session bind key between two reads`);
1944
+ }
1945
+ runtime.onSecret?.(bindKey);
1946
+ bindKeyWritten = mintedBindKey !== null;
1877
1947
  const signInEnvs = [
1878
1948
  ...loginEnvs,
1879
1949
  { key: SIGN_IN_BUILD_ARG_ENV, value: SIGN_IN_BASE_PATH, buildTime: true },
1950
+ { key: SIGN_IN_BIND_KEY_ENV, value: bindKey },
1880
1951
  ...(bindings ? [{ key: LOGIN_HOST_BINDINGS_ENV, value: bindings }] : []),
1881
1952
  ];
1882
1953
  signInEnvsChanged = await envsDiffer(deps.coolify, signInApp.appId, signInEnvs);
@@ -1902,11 +1973,22 @@ export async function provisionAuthStack(scope, deps, target, input) {
1902
1973
  await waitHealthy(withCallTimeout(deps.coolify), zitadelAppId, runtime, label, 'ZITADEL');
1903
1974
  await record(SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, '');
1904
1975
  }
1905
- if (loginApp.justCreated || !loginApp.running || loginEnvsChanged) {
1976
+ if (loginApp.justCreated || !loginApp.running || loginEnvsChanged || authRefChanged) {
1906
1977
  await deps.coolify.deploy(loginAppId);
1907
1978
  }
1908
- if (signInApp && (signInApp.justCreated || !signInApp.running || signInEnvsChanged)) {
1979
+ // The key counts as live only once the app was deployed after it was written: a crash between the write
1980
+ // and the deploy leaves the marker unset, so the next run deploys.
1981
+ const bindKeyUnconfirmed = Boolean(signInApp && input.signInLogin) && recorded[SIGN_IN_BIND_KEY_SCOPE_KEY] !== '1';
1982
+ if (signInApp &&
1983
+ (signInApp.justCreated ||
1984
+ !signInApp.running ||
1985
+ signInEnvsChanged ||
1986
+ bindKeyWritten ||
1987
+ bindKeyUnconfirmed ||
1988
+ authRefChanged)) {
1909
1989
  await deps.coolify.deploy(signInApp.appId);
1990
+ if (input.signInLogin)
1991
+ await record(SIGN_IN_BIND_KEY_SCOPE_KEY, '1');
1910
1992
  }
1911
1993
  // #211: the broker's own `server.ts` refuses to start without
1912
1994
  // `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `LETTERMINT_API_TOKEN` --
@@ -1929,6 +2011,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
1929
2011
  (brokerApp.justCreated ||
1930
2012
  !brokerApp.running ||
1931
2013
  brokerEnvsChanged ||
2014
+ authRefChanged ||
1932
2015
  brokerDeployNeeded(brokerMissingInputs, brokerAppId, authAppsRaw, recordedBrokerDeploy))) {
1933
2016
  await deps.coolify.deploy(brokerAppId);
1934
2017
  await record(PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, brokerDeployRecordValue(brokerAppId, authAppsRaw));
@@ -2302,6 +2385,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
2302
2385
  if (signInApp && (await currentEnvValue(deps.coolify, signInApp.appId, SERVICE_USER_TOKEN_ENV))) {
2303
2386
  await record(SIGN_IN_LOGIN_APP_SCOPE_KEY, signInApp.appId);
2304
2387
  }
2388
+ // Last, so a throw above runs the patch and the deploys again. Only the platform's own apps follow it.
2389
+ if (target.kind === 'platform')
2390
+ await record(PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY, gitRef);
2305
2391
  // ---- the owner company's own sign-in org ----------------------------------
2306
2392
  // Archon signs people in from this org; the `foundry` org holds none. The
2307
2393
  // company executor finds the same org by name (its slug) and makes no second.
@@ -2423,7 +2509,8 @@ const platformAuthProject = {
2423
2509
  },
2424
2510
  };
2425
2511
  /** The platform's sign-in: {@link provisionAuthStack} with {@link platformTarget} and the platform's own inputs. */
2426
- async function provisionPlatformAuth(context, deps, ownerCompany = null, signInLogin = false) {
2512
+ async function provisionPlatformAuth(context, deps, options = {}) {
2513
+ const { ownerCompany = null, signInLogin = false } = options;
2427
2514
  const { platform } = context;
2428
2515
  const scope = scopeFromPlatform(context);
2429
2516
  return provisionAuthStack(scope, deps, platformTarget(), {
@@ -2542,6 +2629,74 @@ async function provisionPlatformArchonOrg(context, deps, ownerCompany) {
2542
2629
  }
2543
2630
  return {};
2544
2631
  }
2632
+ /**
2633
+ * #394 (decision D2, release one): makes the role {@link PLATFORM_MAIL_USER_ROLE} on the mail
2634
+ * project and grants it to the platform owner (`zitadel:mail-role` keyed {@link PLATFORM_MAIL_KEY}).
2635
+ * Each marker is recorded right after its call, so a failure leaves the earlier ones and the next
2636
+ * plan names the step again. The role check is turned on only when the host asked for it and the
2637
+ * grant was recorded before this run began, never in the run that made the grant. A grant is made
2638
+ * only when the owner is in the mail project's own org (`planMailRole` reports the other case).
2639
+ * The error names ids only, never the cause.
2640
+ */
2641
+ async function applyPlatformMailRole(context, deps) {
2642
+ const { platform } = context;
2643
+ const recorded = context.recordedIds();
2644
+ const orgId = recorded[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
2645
+ const projectId = recorded[PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY];
2646
+ const invite = recorded[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
2647
+ if (!orgId || !projectId || !invite) {
2648
+ throw new Error("platform:mail: the sign-in's ZITADEL org, the mail project or the owner invite is not recorded yet -- cannot make the mail role");
2649
+ }
2650
+ const zitadel = deps.zitadelForInstance?.(`https://${platform.auth.hostname}`) ?? deps.zitadel;
2651
+ // The planner plans nothing without the methods; a stale plan does nothing.
2652
+ if (!zitadel.ensureProjectRole || !zitadel.grantProjectRoles)
2653
+ return {};
2654
+ const record = async (key, value) => {
2655
+ context.recordScopeId(key, value);
2656
+ await context.persist?.();
2657
+ };
2658
+ const grantedBefore = recorded[PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY] === `${projectId}|${invite}`;
2659
+ if (recorded[PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY] !== projectId) {
2660
+ try {
2661
+ await zitadel.ensureProjectRole(orgId, projectId, {
2662
+ key: PLATFORM_MAIL_USER_ROLE,
2663
+ displayName: 'Mail user',
2664
+ });
2665
+ }
2666
+ catch {
2667
+ throw new Error(`platform:mail: making the mail role failed on project ${projectId}`);
2668
+ }
2669
+ await record(PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, projectId);
2670
+ }
2671
+ const sameOrg = invite === 'none' || recorded[PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY] === orgId;
2672
+ if (!sameOrg)
2673
+ return {};
2674
+ if (!grantedBefore) {
2675
+ if (invite !== 'none') {
2676
+ try {
2677
+ await zitadel.grantProjectRoles(orgId, invite, projectId, [PLATFORM_MAIL_USER_ROLE]);
2678
+ }
2679
+ catch {
2680
+ throw new Error(`platform:mail: granting the mail role to user ${invite} failed on project ${projectId}`);
2681
+ }
2682
+ }
2683
+ await record(PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, `${projectId}|${invite}`);
2684
+ return {};
2685
+ }
2686
+ if (deps.requireMailRole && zitadel.setProjectRoleChecks) {
2687
+ try {
2688
+ await zitadel.setProjectRoleChecks(orgId, projectId, {
2689
+ projectRoleCheck: true,
2690
+ hasProjectCheck: true,
2691
+ });
2692
+ }
2693
+ catch {
2694
+ throw new Error(`platform:mail: turning on the mail role check failed on project ${projectId}`);
2695
+ }
2696
+ await record(PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, projectId);
2697
+ }
2698
+ return {};
2699
+ }
2545
2700
  /**
2546
2701
  * Reserves the username `admin` and switches self-registration off in the
2547
2702
  * platform's sign-in org (`zitadel:sign-in-policy` keyed {@link PLATFORM_AUTH_KEY}).
@@ -3829,6 +3984,76 @@ async function switchOnArchonIdTokenUserinfo(context, deps) {
3829
3984
  await context.persist?.();
3830
3985
  return {};
3831
3986
  }
3987
+ /**
3988
+ * Replaces the post-logout addresses of Archon's sign-in client
3989
+ * (`zitadel:post-logout-uris` keyed {@link PLATFORM_ARCHON_KEY}). The marker
3990
+ * (app id and set) is recorded only after the call returned, so a call that
3991
+ * throws leaves none and the next plan names the step again. The error names
3992
+ * the ids only, never the cause.
3993
+ */
3994
+ async function updateArchonPostLogoutUris(context, deps) {
3995
+ const recorded = context.recordedIds();
3996
+ const ownerSignIn = context.owner?.signIn ?? null;
3997
+ const zitadel = ownerSignIn
3998
+ ? deps.zitadelForInstance?.(ownerSignIn.issuerUrl, ownerSignIn.bearerRef)
3999
+ : undefined;
4000
+ // The planner plans nothing without the method; a stale plan does nothing.
4001
+ if (!ownerSignIn || !zitadel?.updateOidcAppPostLogoutRedirectUris)
4002
+ return {};
4003
+ const orgId = recorded[PLATFORM_ARCHON_CLIENT_ORG_SCOPE_KEY] ?? ownerSignIn.organisationId;
4004
+ const projectId = recorded[PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY];
4005
+ const appId = recorded[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
4006
+ if (!orgId || !projectId || !appId) {
4007
+ throw new Error('platform:archon: the sign-in client is not recorded yet -- cannot update its post-logout addresses');
4008
+ }
4009
+ const uris = postLogoutUrisFor([context.platform.archon.hostname]);
4010
+ try {
4011
+ await zitadel.updateOidcAppPostLogoutRedirectUris(orgId, projectId, appId, uris);
4012
+ }
4013
+ catch {
4014
+ throw new Error(`platform:archon: updating the post-logout addresses failed for the sign-in client ${appId}`);
4015
+ }
4016
+ context.recordScopeId(PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, postLogoutAppliedValue(appId, uris));
4017
+ await context.persist?.();
4018
+ return {};
4019
+ }
4020
+ /**
4021
+ * #339: registers `/auth/desktop/callback` on Archon's sign-in client
4022
+ * (`zitadel:desktop-callback` keyed {@link PLATFORM_ARCHON_KEY}), keeping the
4023
+ * redirect URIs it holds. The marker is the client's app id, recorded only
4024
+ * after the read and the write resolved. The error names the ids only.
4025
+ */
4026
+ async function registerArchonDesktopCallback(context, deps) {
4027
+ const recorded = context.recordedIds();
4028
+ const ownerSignIn = context.owner?.signIn ?? null;
4029
+ const zitadel = ownerSignIn
4030
+ ? deps.zitadelForInstance?.(ownerSignIn.issuerUrl, ownerSignIn.bearerRef)
4031
+ : undefined;
4032
+ // The planner plans nothing without the methods; a stale plan does nothing.
4033
+ if (!ownerSignIn || !zitadel?.getOidcAppRedirectUris || !zitadel.updateOidcAppRedirectUris) {
4034
+ return {};
4035
+ }
4036
+ const orgId = ownerSignIn.organisationId;
4037
+ const projectId = recorded[PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY];
4038
+ const appId = recorded[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
4039
+ const hostname = recorded[PLATFORM_ARCHON_CLIENT_HOSTNAME_SCOPE_KEY];
4040
+ if (!orgId || !projectId || !appId || !hostname) {
4041
+ throw new Error('platform:archon: the sign-in client is not recorded yet -- cannot register its desktop callback');
4042
+ }
4043
+ try {
4044
+ const current = await zitadel.getOidcAppRedirectUris(orgId, projectId, appId);
4045
+ const union = unionRedirectUris(current, [desktopCallbackUri(hostname)]);
4046
+ if (union.length > current.length) {
4047
+ await zitadel.updateOidcAppRedirectUris(orgId, projectId, appId, union);
4048
+ }
4049
+ }
4050
+ catch {
4051
+ throw new Error(`platform:archon: registering the desktop callback failed for the sign-in client ${appId}`);
4052
+ }
4053
+ context.recordScopeId(PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY, appId);
4054
+ await context.persist?.();
4055
+ return {};
4056
+ }
3832
4057
  /**
3833
4058
  * #175: makes `coolify:app` keyed {@link PLATFORM_ARCHON_KEY} true: Archon
3834
4059
  * from https://github.com/wtfalch/archon on `platform.archon.gitRef`, its own
@@ -4130,12 +4355,15 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4130
4355
  appType: 'web',
4131
4356
  // `@wtfalch/auth`'s default callback path (Archon's own
4132
4357
  // `docs/operator-deployment.md`, step 4).
4133
- redirectUris: [`https://${hostname}/auth/callback`],
4134
- postLogoutRedirectUris: [`https://${hostname}/`],
4358
+ redirectUris: [`https://${hostname}/auth/callback`, desktopCallbackUri(hostname)],
4359
+ postLogoutRedirectUris: postLogoutUrisFor([hostname]),
4135
4360
  });
4136
4361
  context.recordScopeId(PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, created.appId);
4362
+ context.recordScopeId(PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, postLogoutAppliedValue(created.appId, postLogoutUrisFor([hostname])));
4137
4363
  // Made with the setting on (providers 0.9.0): nothing to repair on this id.
4138
4364
  context.recordScopeId(PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, created.appId);
4365
+ // Made with the desktop callback (#339): nothing to repair on this id either.
4366
+ context.recordScopeId(PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY, created.appId);
4139
4367
  context.recordScopeId(PLATFORM_ARCHON_CLIENT_HOSTNAME_SCOPE_KEY, hostname);
4140
4368
  context.recordScopeId(PLATFORM_ARCHON_CLIENT_ORG_SCOPE_KEY, org.id);
4141
4369
  await record(PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY, created.clientId);
@@ -4654,14 +4882,27 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4654
4882
  if (!archonDeployedNow || !selfRedeploy) {
4655
4883
  if (inAppWanted) {
4656
4884
  await archonInAppPoint({ ...inAppTarget, zitadel: inAppCalls() });
4885
+ // A changed Archon host: the old address's binding and trusted domain go once the client points at the new one.
4886
+ const previousHost = archonInAppHost(context.recordedIds());
4887
+ if (previousHost !== '' && previousHost !== hostname && ownerSignIn.signInLoginAppId) {
4888
+ await archonInAppUnbind({
4889
+ ...inAppTarget,
4890
+ host: previousHost,
4891
+ zitadel: inAppCalls(),
4892
+ loginAppId: ownerSignIn.signInLoginAppId,
4893
+ });
4894
+ }
4657
4895
  await record(PLATFORM_ARCHON_IN_APP_SCOPE_KEY, `${hostname}=${clientId}`);
4658
4896
  }
4659
4897
  else if (inAppOwed) {
4898
+ if (!ownerSignIn.signInLoginAppId) {
4899
+ throw new Error("platform:archon: Archon's sign-in address cannot be given up without the owner company's in-app login app");
4900
+ }
4660
4901
  await archonInAppUnbind({
4661
4902
  ...inAppTarget,
4662
4903
  host: archonInAppHost(context.recordedIds()),
4663
4904
  zitadel: inAppCalls(),
4664
- ...(ownerSignIn.signInLoginAppId ? { loginAppId: ownerSignIn.signInLoginAppId } : {}),
4905
+ loginAppId: ownerSignIn.signInLoginAppId,
4665
4906
  });
4666
4907
  await record(PLATFORM_ARCHON_IN_APP_SCOPE_KEY, '');
4667
4908
  }
@@ -5454,6 +5695,32 @@ async function archonDeploymentState(coolify, deploymentUuid) {
5454
5695
  return isNotFoundError(error) ? 'failed' : 'unreadable';
5455
5696
  }
5456
5697
  }
5698
+ /**
5699
+ * #643: whether a recorded sign-in app (ZITADEL, login or broker) is gone from Coolify or not running.
5700
+ * The same status test the step's own deploy decision uses. A read that fails for any other reason
5701
+ * counts as running, with one fixed log line (the error can carry values).
5702
+ */
5703
+ async function authAppsNotRunning(coolify, recordedIds) {
5704
+ for (const scopeKey of [
5705
+ PLATFORM_AUTH_ZITADEL_SCOPE_KEY,
5706
+ PLATFORM_AUTH_LOGIN_SCOPE_KEY,
5707
+ PLATFORM_AUTH_BROKER_SCOPE_KEY,
5708
+ ]) {
5709
+ const appId = recordedIds[scopeKey];
5710
+ if (!appId)
5711
+ continue;
5712
+ try {
5713
+ if (!isRunningOrStarting((await coolify.getApplication(appId)).status))
5714
+ return true;
5715
+ }
5716
+ catch (error) {
5717
+ if (isNotFoundError(error))
5718
+ return true;
5719
+ console.error('platform:auth: could not read a sign-in app status; treating it as running');
5720
+ }
5721
+ }
5722
+ return false;
5723
+ }
5457
5724
  async function archonRollingUpdateDrifted(coolify, appId) {
5458
5725
  if (!appId)
5459
5726
  return false;
@@ -6638,7 +6905,10 @@ async function findOrCreateSignInLoginApp(coolify, input) {
6638
6905
  if (app.environment_id !== environment.id) {
6639
6906
  throw new Error(`${label}: an application named ${name} exists outside the ${environment.name} environment of the sign-in project; foundry does not adopt it`);
6640
6907
  }
6641
- if (app.git_repository !== undefined && !app.git_repository.endsWith(AUTH_REPO)) {
6908
+ if (app.git_repository === undefined) {
6909
+ throw new Error(`${label}: the application ${name} has no git repository, so foundry cannot tell it is built from ${AUTH_REPO} and does not adopt it`);
6910
+ }
6911
+ if (!app.git_repository.endsWith(AUTH_REPO)) {
6642
6912
  throw new Error(`${label}: the application ${name} is built from ${app.git_repository}, not ${AUTH_REPO}; foundry does not adopt it`);
6643
6913
  }
6644
6914
  }
@@ -0,0 +1,22 @@
1
+ import type { DeclaredOrg } from '../org.js';
2
+ /** The page `@wtfalch/auth` apps land on after ZITADEL ends the session. */
3
+ export declare const SIGNED_OUT_PATH = "/auth/signed-out";
4
+ /** Both landing addresses of a host: its root and its signed-out page. */
5
+ export declare function postLogoutUrisFor(hosts: readonly string[]): string[];
6
+ /**
7
+ * The marker value for an app: its ZITADEL id and the set it holds, in a fixed
8
+ * order. A changed set or a re-created app gives a different value, so the
9
+ * next plan names the app again.
10
+ */
11
+ export declare function postLogoutAppliedValue(appId: string, uris: readonly string[]): string;
12
+ /** True while the recorded marker is not the one the app holds with `uris`. */
13
+ export declare function postLogoutUpdateOwed(recordedMarker: string | undefined, appId: string, uris: readonly string[]): boolean;
14
+ /** The scope id holding company app `appKey`'s applied post-logout marker. */
15
+ export declare function zitadelAppPostLogoutAppliedScopeKey(appKey: string): string;
16
+ /**
17
+ * The post-logout addresses company app `appKey` is created with and kept at.
18
+ * Agora signs out locally and has no signed-out page, so it holds only its
19
+ * root; every `@wtfalch/auth` app (cms, boule, the app, entries with
20
+ * `uses.auth`) holds both on each of its hosts.
21
+ */
22
+ export declare function companyAppPostLogoutUris(org: DeclaredOrg, appKey: string): string[];
@@ -0,0 +1,43 @@
1
+ import { agoraHostnameFor } from './agora.js';
2
+ import { bouleHostnameFor } from './boule.js';
3
+ import { appHostname, cmsStudioHostnameFor, entryHostnames } from './hostnames.js';
4
+ /** The page `@wtfalch/auth` apps land on after ZITADEL ends the session. */
5
+ export const SIGNED_OUT_PATH = '/auth/signed-out';
6
+ /** Both landing addresses of a host: its root and its signed-out page. */
7
+ export function postLogoutUrisFor(hosts) {
8
+ return hosts.flatMap((host) => [`https://${host}/`, `https://${host}${SIGNED_OUT_PATH}`]);
9
+ }
10
+ /**
11
+ * The marker value for an app: its ZITADEL id and the set it holds, in a fixed
12
+ * order. A changed set or a re-created app gives a different value, so the
13
+ * next plan names the app again.
14
+ */
15
+ export function postLogoutAppliedValue(appId, uris) {
16
+ return `${appId} ${[...uris].sort().join(' ')}`;
17
+ }
18
+ /** True while the recorded marker is not the one the app holds with `uris`. */
19
+ export function postLogoutUpdateOwed(recordedMarker, appId, uris) {
20
+ return recordedMarker !== postLogoutAppliedValue(appId, uris);
21
+ }
22
+ /** The scope id holding company app `appKey`'s applied post-logout marker. */
23
+ export function zitadelAppPostLogoutAppliedScopeKey(appKey) {
24
+ return `zitadel_app_post_logout_applied.${appKey}`;
25
+ }
26
+ /**
27
+ * The post-logout addresses company app `appKey` is created with and kept at.
28
+ * Agora signs out locally and has no signed-out page, so it holds only its
29
+ * root; every `@wtfalch/auth` app (cms, boule, the app, entries with
30
+ * `uses.auth`) holds both on each of its hosts.
31
+ */
32
+ export function companyAppPostLogoutUris(org, appKey) {
33
+ const authEntry = org.hosting.entries.find((e) => e.key === appKey && e.uses?.auth);
34
+ const hosts = authEntry ? entryHostnames(org, authEntry) : null;
35
+ if (appKey === 'agora')
36
+ return [`https://${agoraHostnameFor(org)}/`];
37
+ const hostname = appKey === 'cms' && org.site.enabled
38
+ ? cmsStudioHostnameFor(org)
39
+ : appKey === 'boule'
40
+ ? bouleHostnameFor(org)
41
+ : appHostname(org);
42
+ return postLogoutUrisFor(hosts ?? [hostname]);
43
+ }
@@ -3,7 +3,7 @@ import type { CoolifyClient } from '@wtfalch/providers/coolify';
3
3
  import { type ZitadelClient } from '@wtfalch/providers/zitadel';
4
4
  import { type OrgId } from '../org.js';
5
5
  import type { DeclaredOrgStore } from '../store.js';
6
- import type { AiReader, CloudflareReader, CmsReader, CoolifyReader, LiveResource, ZitadelReader } from './live.js';
6
+ import { type AiReader, type CloudflareReader, type CmsReader, type CoolifyReader, type LiveResource, type ZitadelReader } from './live.js';
7
7
  /** The `CoolifyClient` methods the readers call, so a host's own implementation needs no cast. */
8
8
  type CoolifyReaderClient = Pick<CoolifyClient, 'listResources' | 'getApplication' | 'getDatabase' | 'listEnvs' | 'listStorages'>;
9
9
  /**
@@ -152,6 +152,15 @@ export declare class ProvidersZitadelReader implements ZitadelReader {
152
152
  private readonly resolveLoginHost;
153
153
  constructor(client: ZitadelClient, resolveScope: ScopeResolver, resolveRecordedIds: RecordedIds, resolveLoginHost?: ScopeResolver);
154
154
  listResources(orgId: OrgId): Promise<LiveResource[]>;
155
+ /**
156
+ * #557: a recorded app the listing does not return, proven gone by a direct
157
+ * read by app id in every project the listing shows. One `app-gone`
158
+ * resource per proven app, keyed by the app's name: `diff.ts` offers the
159
+ * replacement only for it. Nothing is proven when the client has no
160
+ * `getApp` (providers before 0.12.0), when the read throws, or when it
161
+ * finds the app, so the plan blocks instead.
162
+ */
163
+ private goneApps;
155
164
  /**
156
165
  * #510: `client.listResources` reports only what is *inside* a ZITADEL org
157
166
  * (its projects and grants), never the org record itself, so nothing ever
@@ -2,6 +2,7 @@ import { ZitadelError } from '@wtfalch/providers/zitadel';
2
2
  import { APP_ENTRY_KEY } from '../org.js';
3
3
  import { BOULE_MAIL_TOKEN_ENV, BOULE_MANAGED_ENV_KEYS } from './boule.js';
4
4
  import { previewsOn } from './coolify-previews.js';
5
+ import { APP_GONE_KIND, } from './live.js';
5
6
  import { SIGN_IN_UPSTREAM_ENV } from './sign-in.js';
6
7
  /**
7
8
  * A {@link ScopeResolver} backed by `AppliedState.serviceIds` (`../state.ts`)
@@ -416,7 +417,46 @@ export class ProvidersZitadelReader {
416
417
  this.liveIssuer(orgId, zitadelOrgId),
417
418
  ]);
418
419
  const mapped = applyRecordedIds(toFoundryResources('zitadel', resources), recorded);
419
- return issuer ? [...mapped, issuer] : mapped;
420
+ const gone = await this.goneApps(zitadelOrgId, resources, recorded);
421
+ return issuer ? [...mapped, ...gone, issuer] : [...mapped, ...gone];
422
+ }
423
+ /**
424
+ * #557: a recorded app the listing does not return, proven gone by a direct
425
+ * read by app id in every project the listing shows. One `app-gone`
426
+ * resource per proven app, keyed by the app's name: `diff.ts` offers the
427
+ * replacement only for it. Nothing is proven when the client has no
428
+ * `getApp` (providers before 0.12.0), when the read throws, or when it
429
+ * finds the app, so the plan blocks instead.
430
+ */
431
+ async goneApps(zitadelOrgId, listed, recorded) {
432
+ // A host may pass a client older than providers 0.12.0, which has no `getApp`.
433
+ const getApp = typeof this.client.getApp === 'function' ? this.client.getApp.bind(this.client) : undefined;
434
+ const listedIds = new Set(listed.filter((r) => r.kind === 'app').map((r) => r.key));
435
+ const projectIds = listed.filter((r) => r.kind === 'project').map((r) => r.key);
436
+ if (!getApp || projectIds.length === 0)
437
+ return [];
438
+ const gone = [];
439
+ for (const [appId, resourceKey] of Object.entries(recorded)) {
440
+ if (!resourceKey.startsWith('app:') || listedIds.has(appId))
441
+ continue;
442
+ try {
443
+ const found = await Promise.all(projectIds.map((projectId) => getApp(zitadelOrgId, projectId, appId)));
444
+ if (found.every((app) => app === null || app.state === 'APP_STATE_REMOVED')) {
445
+ gone.push({
446
+ system: 'zitadel',
447
+ kind: APP_GONE_KIND,
448
+ key: resourceKey.slice('app:'.length),
449
+ config: {},
450
+ });
451
+ }
452
+ }
453
+ catch (error) {
454
+ // Unproven, not gone: the plan blocks this app. Only the error's name is kept.
455
+ if (!(error instanceof ZitadelError))
456
+ throw error;
457
+ }
458
+ }
459
+ return gone;
420
460
  }
421
461
  /**
422
462
  * #510: `client.listResources` reports only what is *inside* a ZITADEL org
@@ -13,4 +13,8 @@ export declare function editSignInBindings(coolify: Pick<CoolifyPort, 'listEnvs'
13
13
  instanceHost: string;
14
14
  upsert?: readonly LoginHostBinding[];
15
15
  remove?: readonly string[];
16
+ clientOrganizations?: {
17
+ upsert?: Readonly<Record<string, string>>;
18
+ remove?: readonly string[];
19
+ };
16
20
  }): Promise<void>;