@wtfalch/foundry 0.65.0 → 0.67.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 (88) hide show
  1. package/dist/apps/cloudflare.d.ts +5 -0
  2. package/dist/apps/deploy.js +33 -1
  3. package/dist/apps/migrations.d.ts +3 -0
  4. package/dist/apps/migrations.js +32 -7
  5. package/dist/catalogue/copy-en.js +5 -0
  6. package/dist/catalogue/integrations.js +5 -0
  7. package/dist/entry-rules.d.ts +11 -0
  8. package/dist/entry-rules.js +70 -0
  9. package/dist/index.d.ts +9 -4
  10. package/dist/index.js +5 -2
  11. package/dist/migrations/0069_release_events.sql +16 -0
  12. package/dist/modules/analytics.d.ts +8 -0
  13. package/dist/modules/analytics.js +28 -0
  14. package/dist/modules/database.js +22 -3
  15. package/dist/modules/index.js +2 -0
  16. package/dist/native-build.d.ts +16 -12
  17. package/dist/native-build.js +16 -12
  18. package/dist/org.d.ts +9 -1
  19. package/dist/org.js +4 -1
  20. package/dist/plan/analytics-boule.d.ts +17 -0
  21. package/dist/plan/analytics-boule.js +43 -0
  22. package/dist/plan/analytics.d.ts +58 -0
  23. package/dist/plan/analytics.js +89 -0
  24. package/dist/plan/boule.js +2 -0
  25. package/dist/plan/company-backups.js +2 -0
  26. package/dist/plan/diff.d.ts +4 -0
  27. package/dist/plan/diff.js +103 -3
  28. package/dist/plan/entry-analytics-key.d.ts +35 -0
  29. package/dist/plan/entry-analytics-key.js +134 -0
  30. package/dist/plan/executor.d.ts +79 -6
  31. package/dist/plan/executor.js +379 -19
  32. package/dist/plan/hostnames.js +4 -0
  33. package/dist/plan/index.d.ts +2 -0
  34. package/dist/plan/index.js +1 -0
  35. package/dist/plan/live.d.ts +1 -1
  36. package/dist/plan/locked-hostnames.js +3 -0
  37. package/dist/plan/login-host.d.ts +8 -0
  38. package/dist/plan/login-host.js +8 -0
  39. package/dist/plan/org-names.d.ts +3 -3
  40. package/dist/plan/org-names.js +3 -0
  41. package/dist/plan/own-limits.d.ts +6 -0
  42. package/dist/plan/own-limits.js +1 -0
  43. package/dist/plan/plan-apply.d.ts +12 -0
  44. package/dist/plan/plan-apply.js +40 -1
  45. package/dist/plan/platform-diff.d.ts +17 -1
  46. package/dist/plan/platform-diff.js +40 -2
  47. package/dist/plan/platform-org.d.ts +6 -0
  48. package/dist/plan/platform-providers.d.ts +8 -6
  49. package/dist/plan/platform-providers.js +56 -10
  50. package/dist/plan/providers-readers.js +16 -1
  51. package/dist/plan/release-host.d.ts +30 -1
  52. package/dist/plan/release-host.js +84 -41
  53. package/dist/plan/release-series-reader.d.ts +26 -0
  54. package/dist/plan/release-series-reader.js +40 -0
  55. package/dist/plan/sign-in-archon.d.ts +1 -1
  56. package/dist/plan/sign-in-archon.js +2 -2
  57. package/dist/plan/stage-services.d.ts +3 -1
  58. package/dist/plan/stage-services.js +2 -1
  59. package/dist/plan/uptime-worker.js +53 -3
  60. package/dist/plan/worker-app-database.d.ts +12 -1
  61. package/dist/plan/worker-app-database.js +13 -0
  62. package/dist/plan/worker-app.d.ts +27 -2
  63. package/dist/plan/worker-app.js +52 -7
  64. package/dist/plan/workspace.d.ts +2 -2
  65. package/dist/plan/workspace.js +3 -3
  66. package/dist/release/decide.d.ts +2 -0
  67. package/dist/release/decide.js +67 -10
  68. package/dist/release/events.d.ts +13 -0
  69. package/dist/release/events.js +67 -0
  70. package/dist/release/health.d.ts +5 -0
  71. package/dist/release/health.js +3 -3
  72. package/dist/release/run.d.ts +7 -2
  73. package/dist/release/run.js +8 -0
  74. package/dist/release/series.d.ts +58 -0
  75. package/dist/release/series.js +65 -0
  76. package/dist/release/state.d.ts +58 -1
  77. package/dist/release/state.js +45 -0
  78. package/dist/release/types.d.ts +44 -0
  79. package/dist/slug.d.ts +1 -1
  80. package/dist/slug.js +5 -2
  81. package/dist/store/postgres.js +29 -3
  82. package/dist/store/schema.d.ts +112 -0
  83. package/dist/store/schema.js +9 -0
  84. package/dist/worker-app-config.d.ts +22 -1
  85. package/dist/worker-app-config.js +73 -15
  86. package/dist/worker-app-migrations.d.ts +6 -2
  87. package/dist/worker-app-migrations.js +1 -0
  88. package/package.json +1 -1
@@ -6,13 +6,13 @@ export type OrgNameSource = 'own' | 'hosting';
6
6
  * hosting entries: Agora, Boule, the tunnel and its connector, and chat,
7
7
  * calls and the cms site while they are on.
8
8
  */
9
- export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site' | 'kosmos'>): string[];
9
+ export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site' | 'kosmos' | 'boule' | 'hosting'>): string[];
10
10
  /**
11
11
  * Every container name, network alias, database, tunnel and bucket name that
12
12
  * foundry derives for an org: its own-service names when it owns services,
13
13
  * and its hosting-entry and database names.
14
14
  */
15
- export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>): {
15
+ export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos' | 'boule'>): {
16
16
  name: string;
17
17
  source: OrgNameSource;
18
18
  }[];
@@ -26,5 +26,5 @@ export interface NameClash {
26
26
  source: OrgNameSource;
27
27
  }
28
28
  /** Every name `org` derives that another org or the platform also derives. */
29
- export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>[]): NameClash[];
29
+ export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos' | 'boule'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos' | 'boule'>[]): NameClash[];
30
30
  export declare function nameClashMessage(slug: string, clash: NameClash): string;
@@ -1,5 +1,6 @@
1
1
  import { kosmosOn, ownsServices } from '../org.js';
2
2
  import { entryHasDatabase } from '../worker-app-config.js';
3
+ import { analyticsWanted } from './analytics.js';
3
4
  import { chatInternalNames, chatRekeyedInternalNames } from './chat-matrix.js';
4
5
  import { workerAppHostname, workspaceHost, workspaceInboxHost } from './hostnames.js';
5
6
  import { orgTarget, platformTarget } from './own-services.js';
@@ -20,6 +21,8 @@ export function companyServiceNames(org) {
20
21
  `${s}-tunnel`,
21
22
  `${s}-cloudflared`,
22
23
  ];
24
+ if (analyticsWanted(org))
25
+ names.push(`${s}-analytics`, `${s}-analytics-db`);
23
26
  if (kosmosOn(org))
24
27
  names.push(`${s}-kosmos`, `${s}-kosmos-db`);
25
28
  if (org.chat?.enabled) {
@@ -113,6 +113,12 @@ export declare const LIMITS: {
113
113
  autoDeploy: false | "declared";
114
114
  capabilities: CapabilityTier;
115
115
  };
116
+ readonly analytics: {
117
+ memory: string;
118
+ dbMemory: string;
119
+ autoDeploy: false | "declared";
120
+ capabilities: CapabilityTier;
121
+ };
116
122
  readonly companyOther: {
117
123
  memory: string;
118
124
  dbMemory: string;
@@ -30,6 +30,7 @@ export const LIMITS = {
30
30
  agora: profile('1g', '512m'),
31
31
  boule: profile('512m'),
32
32
  kosmos: profile('512m'),
33
+ analytics: profile('512m', '512m'),
33
34
  companyOther: profile('1g', '512m'),
34
35
  platformArchon: profile('1g'),
35
36
  platformAi: profile('1g'),
@@ -583,8 +583,12 @@ export interface FoundryPlanApplyOptions {
583
583
  workerAppKeysUrl?: string;
584
584
  /** F7: the keys service's registrar address, when the executor has a `KeysRegistrarPort`. */
585
585
  keysRegistrarUrl?: string;
586
+ /** True when the host's executor was built with an `analytics` port. A `uses.analytics` key is blocked without it. */
587
+ analyticsPort?: boolean;
586
588
  /** C2: true when the host's executor was built with a `WorkerAppDatabasePort`. */
587
589
  workerAppDatabase?: boolean;
590
+ /** True when that port has `deletableTables`. A `workerApp.database.noDelete` is blocked without it. */
591
+ workerAppNoDeleteCheck?: boolean;
588
592
  /** ADR 0002: the platform's Cloudflare account id. A container image outside its registry is blocked. */
589
593
  workerAppRegistryAccountId?: string;
590
594
  /**
@@ -745,7 +749,9 @@ export declare class FoundryPlanApply implements PlanApply {
745
749
  private readonly workerApps;
746
750
  private readonly workerAppKeysUrl;
747
751
  private readonly keysRegistrarUrl;
752
+ private readonly analyticsPort;
748
753
  private readonly workerAppDatabase;
754
+ private readonly workerAppNoDeleteCheck;
749
755
  private readonly workerAppRegistryAccountId;
750
756
  private readonly ownerInviteMail;
751
757
  private readonly hostFiles;
@@ -980,6 +986,12 @@ export declare class FoundryPlanApply implements PlanApply {
980
986
  private repoKeysReadiness;
981
987
  /** Which refs of the platform GitHub App hold no value now. A resolver that throws counts as not held; only refs are kept. */
982
988
  private githubAppKeysReadiness;
989
+ /**
990
+ * The Worker apps' `heldSecret` secrets that hold a value now, by `<entry key>/<NAME>`, with the version of
991
+ * the value. A resolver that throws, or a name that fits no ref, counts as not held. The value is hashed and
992
+ * dropped here; only the version leaves.
993
+ */
994
+ private workerAppSecretsReadiness;
983
995
  /**
984
996
  * The org's `heldSecret` envs: the refs that hold a value now, which of those differ from the live
985
997
  * production env, which could not be read, and which fit no ref. A resolver that throws counts as
@@ -40,7 +40,7 @@ import { CLOUDFLARE_TUNNEL_APPS_HOSTNAMES_SCOPE_KEY, CLOUDFLARE_TUNNEL_ENTRY_ROU
40
40
  import { finishStageCopy, runSubjectRelease, } from './release-host.js';
41
41
  import { remoteDatabaseFactsFrom, remoteDatabaseKeys } from './remote-database.js';
42
42
  import { STAGE_COPY_LOCK_MS, claimStageCopyLock, parseStageCopyLock, stageCopyLockValue, } from './stage-copy-host.js';
43
- import { liveWorkerApps, workerAppTraceScopeKeys } from './worker-app.js';
43
+ import { heldSecretId, heldSecretVersion, liveWorkerApps, workerAppTraceScopeKeys, } from './worker-app.js';
44
44
  import { liveWorkers, workerTraceScopeKeys } from './worker.js';
45
45
  /** #441: the lease scope that serializes every mutation of one company, and the one for the shared platform. */
46
46
  const companyScope = (orgId) => `company:${orgId}`;
@@ -188,7 +188,9 @@ export class FoundryPlanApply {
188
188
  workerApps;
189
189
  workerAppKeysUrl;
190
190
  keysRegistrarUrl;
191
+ analyticsPort;
191
192
  workerAppDatabase;
193
+ workerAppNoDeleteCheck;
192
194
  workerAppRegistryAccountId;
193
195
  ownerInviteMail;
194
196
  hostFiles;
@@ -232,7 +234,9 @@ export class FoundryPlanApply {
232
234
  this.workerApps = options.workerApps ?? false;
233
235
  this.workerAppKeysUrl = options.workerAppKeysUrl;
234
236
  this.keysRegistrarUrl = options.keysRegistrarUrl;
237
+ this.analyticsPort = options.analyticsPort ?? false;
235
238
  this.workerAppDatabase = options.workerAppDatabase ?? false;
239
+ this.workerAppNoDeleteCheck = options.workerAppNoDeleteCheck ?? false;
236
240
  this.workerAppRegistryAccountId = options.workerAppRegistryAccountId;
237
241
  this.ownerInviteMail = options.ownerInviteMail ?? false;
238
242
  this.hostFiles = options.filesOrganisation ?? false;
@@ -1625,7 +1629,9 @@ export class FoundryPlanApply {
1625
1629
  workerApps: this.workerApps,
1626
1630
  workerAppKeysUrl: this.workerAppKeysUrl,
1627
1631
  keysRegistrarUrl: this.keysRegistrarUrl,
1632
+ analyticsPort: this.analyticsPort,
1628
1633
  workerAppDatabase: this.workerAppDatabase,
1634
+ workerAppNoDeleteCheck: this.workerAppNoDeleteCheck,
1629
1635
  workerAppRegistryAccountId: this.workerAppRegistryAccountId,
1630
1636
  ownerInviteMail: this.ownerInviteMail,
1631
1637
  otherSlugs,
@@ -1758,6 +1764,7 @@ export class FoundryPlanApply {
1758
1764
  const base = {
1759
1765
  ...(await this.sharedPlatformReadiness()),
1760
1766
  heldEnv: await this.heldEnvReadiness(org),
1767
+ workerAppSecrets: await this.workerAppSecretsReadiness(org),
1761
1768
  ...(org.hosting.github ? { githubAppKeys: await this.githubAppKeysReadiness() } : {}),
1762
1769
  ...(repoKeysInPlay(org, orgRecordedIds)
1763
1770
  ? { repoKeys: await this.repoKeysReadiness(org, orgRecordedIds) }
@@ -1805,6 +1812,32 @@ export class FoundryPlanApply {
1805
1812
  }
1806
1813
  return { store: true, missing };
1807
1814
  }
1815
+ /**
1816
+ * The Worker apps' `heldSecret` secrets that hold a value now, by `<entry key>/<NAME>`, with the version of
1817
+ * the value. A resolver that throws, or a name that fits no ref, counts as not held. The value is hashed and
1818
+ * dropped here; only the version leaves.
1819
+ */
1820
+ async workerAppSecretsReadiness(org) {
1821
+ const heldKeys = this.platformDeps?.heldKeys;
1822
+ const held = {};
1823
+ if (!heldKeys)
1824
+ return held;
1825
+ for (const entry of org.hosting.entries) {
1826
+ for (const [name, source] of Object.entries(entry.workerApp?.secrets ?? {})) {
1827
+ if (source.from !== 'heldSecret')
1828
+ continue;
1829
+ try {
1830
+ const value = await heldKeys.resolve(heldEnvRef(org.id, entry.key, name));
1831
+ if (value)
1832
+ held[heldSecretId(entry.key, name)] = heldSecretVersion(value);
1833
+ }
1834
+ catch {
1835
+ // Not held: the plan says the secret has no value yet, and the error is dropped.
1836
+ }
1837
+ }
1838
+ }
1839
+ return held;
1840
+ }
1808
1841
  /**
1809
1842
  * The org's `heldSecret` envs: the refs that hold a value now, which of those differ from the live
1810
1843
  * production env, which could not be read, and which fit no ref. A resolver that throws counts as
@@ -2635,6 +2668,8 @@ const STEP_FIELDS = {
2635
2668
  'cloudflare:worker': 'hosting.entries.worker',
2636
2669
  'cloudflare:worker-app': 'hosting.entries.workerApp',
2637
2670
  // F4 (#925): both hang off the company's Apps switch.
2671
+ 'coolify:analytics': 'hosting.entries.uses.analytics',
2672
+ 'coolify:analytics-boule': 'boule.apps',
2638
2673
  'coolify:org-apps-admin': 'boule.apps',
2639
2674
  'cloudflare:apps-db-route': 'boule.apps',
2640
2675
  'cloudflare:apps-zones': 'boule.apps',
@@ -2658,6 +2693,10 @@ function stepField(action) {
2658
2693
  // An entry's jobs project: always `<orgId>:<entry>`.
2659
2694
  if (action.system === 'jobs' && action.kind === 'project')
2660
2695
  return 'hosting.entries.uses.jobs';
2696
+ // An entry's analytics site key: always `<orgId>:<entry>`.
2697
+ if (action.system === 'analytics' && action.kind === 'key') {
2698
+ return 'hosting.entries.uses.analytics';
2699
+ }
2661
2700
  // An entry's audit ledger registration: always `<orgId>:<entry>`.
2662
2701
  if (action.system === 'audit' && action.kind === 'ledger')
2663
2702
  return 'hosting.entries.uses.audit';
@@ -1117,6 +1117,13 @@ export declare const PLATFORM_UPTIME_KV_TITLE = "platform-uptime";
1117
1117
  * a blocked item, never a throw.
1118
1118
  */
1119
1119
  export declare const PLATFORM_UPTIME_TOKEN_REF = "uptime:worker:token";
1120
+ /**
1121
+ * The release tick and audit seal tick tokens: minted by the step like `UPTIME_TOKEN`, each its own
1122
+ * value, set on the Worker and on Archon's env under the same names. The Worker skips a tick while
1123
+ * its token is unset, and Archon's route refuses every call while its own is unset.
1124
+ */
1125
+ export declare const PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF = "uptime:worker:release-tick-token";
1126
+ export declare const PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF = "uptime:worker:audit-seal-tick-token";
1120
1127
  export declare const PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF = "uptime:worker:access-client-id";
1121
1128
  export declare const PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF = "uptime:worker:access-client-secret";
1122
1129
  export declare const PLATFORM_UPTIME_CHAT_TOKEN_REF = "uptime:worker:chat-token";
@@ -1138,6 +1145,15 @@ export declare const PLATFORM_UPTIME_OPTIONAL_SECRETS: Readonly<Record<string, s
1138
1145
  */
1139
1146
  export declare const PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY = "uptime_token_undelivered";
1140
1147
  export declare const PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY = "uptime_token_id";
1148
+ /** The same pair of scope keys for each tick token: `<undelivered>` before the value is held, `<id>` once it is. */
1149
+ export declare const PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY = "uptime_release_tick_undelivered";
1150
+ export declare const PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY = "uptime_release_tick_id";
1151
+ export declare const PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY = "uptime_audit_seal_tick_undelivered";
1152
+ export declare const PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY = "uptime_audit_seal_tick_id";
1153
+ /** The tick token ids (`<release>:<seal>`) Archon's env was last written with; like {@link PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY}. */
1154
+ export declare const PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY = "archon_uptime_ticks_synced";
1155
+ /** What {@link PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY} holds once Archon's env carries the tick tokens; empty while neither is minted. */
1156
+ export declare function uptimeTicksSyncValue(recorded: Readonly<Record<string, string>>): string;
1141
1157
  /** The token id Archon's env was last written with; `planArchon` plans again when it differs from {@link PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY}. */
1142
1158
  export declare const PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY = "archon_uptime_token_synced";
1143
1159
  /** The tag, sha256 and Archon hostname the last successful run applied; a change plans the step again and mints nothing. */
@@ -1171,7 +1187,7 @@ export declare function uptimeEffectiveVars(hostVars: Readonly<Record<string, st
1171
1187
  } | null): Record<string, string | undefined>;
1172
1188
  /** The digest of the Worker's plain text variables that are set; empty when none is. */
1173
1189
  export declare function uptimeVarsDigest(vars?: Readonly<Record<string, string | undefined>>): string;
1174
- /** The Worker secret names the step sets: the three it mints, and each held one. */
1190
+ /** The Worker secret names the step sets: the five it mints, and each held one. */
1175
1191
  export declare function uptimeSecretNames(held: readonly string[]): string;
1176
1192
  /** The Access service token's name at Cloudflare; edge answers `conflict` when a token of this name exists. */
1177
1193
  export declare const PLATFORM_UPTIME_ACCESS_TOKEN_NAME = "platform-uptime";
@@ -8,6 +8,7 @@ import { buildDriftNotes } from './build-record.js';
8
8
  import { versionChangeNeedsConfirmation } from './confirm-upgrade.js';
9
9
  import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
10
10
  import { legacyAppHostname } from './hostnames.js';
11
+ import { LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION } from './login-host.js';
11
12
  import { PLATFORM_MAIL_RELAY_KEY, planMailRelay } from './mail-relay.js';
12
13
  import { MAIL_DATABASE_SCOPE_KEY, mailStoreIsLegacy, mailStoreSwitchChange, mailStoreSwitchDescription, } from './mail-store.js';
13
14
  import { planOikosIngress } from './oikos-ingress.js';
@@ -1271,6 +1272,13 @@ export const PLATFORM_UPTIME_KV_TITLE = 'platform-uptime';
1271
1272
  * a blocked item, never a throw.
1272
1273
  */
1273
1274
  export const PLATFORM_UPTIME_TOKEN_REF = 'uptime:worker:token';
1275
+ /**
1276
+ * The release tick and audit seal tick tokens: minted by the step like `UPTIME_TOKEN`, each its own
1277
+ * value, set on the Worker and on Archon's env under the same names. The Worker skips a tick while
1278
+ * its token is unset, and Archon's route refuses every call while its own is unset.
1279
+ */
1280
+ export const PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF = 'uptime:worker:release-tick-token';
1281
+ export const PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF = 'uptime:worker:audit-seal-tick-token';
1274
1282
  export const PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF = 'uptime:worker:access-client-id';
1275
1283
  export const PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF = 'uptime:worker:access-client-secret';
1276
1284
  export const PLATFORM_UPTIME_CHAT_TOKEN_REF = 'uptime:worker:chat-token';
@@ -1295,6 +1303,19 @@ export const PLATFORM_UPTIME_OPTIONAL_SECRETS = {
1295
1303
  */
1296
1304
  export const PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY = 'uptime_token_undelivered';
1297
1305
  export const PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY = 'uptime_token_id';
1306
+ /** The same pair of scope keys for each tick token: `<undelivered>` before the value is held, `<id>` once it is. */
1307
+ export const PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY = 'uptime_release_tick_undelivered';
1308
+ export const PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY = 'uptime_release_tick_id';
1309
+ export const PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY = 'uptime_audit_seal_tick_undelivered';
1310
+ export const PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY = 'uptime_audit_seal_tick_id';
1311
+ /** The tick token ids (`<release>:<seal>`) Archon's env was last written with; like {@link PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY}. */
1312
+ export const PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY = 'archon_uptime_ticks_synced';
1313
+ /** What {@link PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY} holds once Archon's env carries the tick tokens; empty while neither is minted. */
1314
+ export function uptimeTicksSyncValue(recorded) {
1315
+ const release = recorded[PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY];
1316
+ const seal = recorded[PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY];
1317
+ return release && seal ? `${release}:${seal}` : '';
1318
+ }
1298
1319
  /** The token id Archon's env was last written with; `planArchon` plans again when it differs from {@link PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY}. */
1299
1320
  export const PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY = 'archon_uptime_token_synced';
1300
1321
  /** The tag, sha256 and Archon hostname the last successful run applied; a change plans the step again and mints nothing. */
@@ -1338,9 +1359,16 @@ export function uptimeVarsDigest(vars = {}) {
1338
1359
  return '';
1339
1360
  return createHash('sha256').update(JSON.stringify(set)).digest('hex').slice(0, 16);
1340
1361
  }
1341
- /** The Worker secret names the step sets: the three it mints, and each held one. */
1362
+ /** The Worker secret names the step sets: the five it mints, and each held one. */
1342
1363
  export function uptimeSecretNames(held) {
1343
- return ['UPTIME_TOKEN', 'CF_ACCESS_CLIENT_ID', 'CF_ACCESS_CLIENT_SECRET', ...held]
1364
+ return [
1365
+ 'UPTIME_TOKEN',
1366
+ 'RELEASE_TICK_TOKEN',
1367
+ 'AUDIT_SEAL_TICK_TOKEN',
1368
+ 'CF_ACCESS_CLIENT_ID',
1369
+ 'CF_ACCESS_CLIENT_SECRET',
1370
+ ...held,
1371
+ ]
1344
1372
  .sort()
1345
1373
  .join(',');
1346
1374
  }
@@ -3249,6 +3277,7 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
3249
3277
  const applied = deployed &&
3250
3278
  Boolean(recordedIds[PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY]) &&
3251
3279
  uptimeHeld.token &&
3280
+ Boolean(uptimeTicksSyncValue(recordedIds)) &&
3252
3281
  Boolean(recordedIds[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY]) &&
3253
3282
  uptimeHeld.access &&
3254
3283
  recordedIds[PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY] === uptime.tag &&
@@ -3482,6 +3511,10 @@ function planArchon(platform, recordedIds, executorBlocked, ownerSignIn, ownerFi
3482
3511
  // In-app sign-in: Archon's own address is the sign-in address only once the owner company has the
3483
3512
  // switch on and its in-app login is applied, and goes back to the company's login host when it does not.
3484
3513
  archonInAppHost(recordedIds) === (ownerSignIn.signInInApp ? platform.archon.hostname : '') &&
3514
+ // With in-app sign-in on, `loginV2.required` is off and every app of the instance owes a base URI
3515
+ // until the sweep ran (an instance applied before it plans the step once more).
3516
+ (!ownerSignIn.signInInApp ||
3517
+ recordedIds[LOGIN_BASE_URI_SWEEP_SCOPE_KEY] === LOGIN_BASE_URI_SWEEP_VERSION) &&
3485
3518
  recordedIds[PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY] === archonOrgId &&
3486
3519
  !recordedIds[PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY] &&
3487
3520
  Boolean(recordedIds[PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY]) &&
@@ -3506,6 +3539,11 @@ function planArchon(platform, recordedIds, executorBlocked, ownerSignIn, ownerFi
3506
3539
  recordedIds[PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY] ===
3507
3540
  recordedIds[PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY]
3508
3541
  : !recordedIds[PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY]) &&
3542
+ // The tick tokens the same way: minted after Archon's last apply means not in its env yet.
3543
+ (platform.uptime.enabled
3544
+ ? (recordedIds[PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY] ?? '') ===
3545
+ uptimeTicksSyncValue(recordedIds)
3546
+ : !recordedIds[PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY]) &&
3509
3547
  // #398: a rotated integrations key id Archon's env was not yet written
3510
3548
  // with (see PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY). Absent
3511
3549
  // before the first rotation on a platform applied before #398.
@@ -194,6 +194,12 @@ export interface OrgPlatformReadiness {
194
194
  unreadable?: readonly string[];
195
195
  invalid?: readonly string[];
196
196
  };
197
+ /**
198
+ * The Worker apps' `heldSecret` secrets that hold a value now: `<entry key>/<NAME>` -> the version of that
199
+ * value (a hash, `heldSecretVersion`; never the value). A secret with no entry here is unset. Absent reads as
200
+ * none held.
201
+ */
202
+ workerAppSecrets?: Readonly<Record<string, string>>;
197
203
  /**
198
204
  * The platform GitHub App's held keys (`PLATFORM_GITHUB_APP_REFS`), read only for a company that declares
199
205
  * `hosting.github`. `store` is false when the host wired no held-keys reader; `missing` are the refs that hold
@@ -542,13 +542,15 @@ export declare const PLATFORM_AUTH_LOGIN_INTERNAL_NAME = "platform-auth-login";
542
542
  export declare function zitadelDockerfile(): string;
543
543
  export declare const AUTH_LOGIN_PORT = "3000";
544
544
  /**
545
- * Every ZITADEL OIDC/management/console/asset path -- `deploy/proxy/Caddyfile`'s
546
- * own explicit list, restated here as one regex since the tunnel routes by a
545
+ * Every ZITADEL OIDC/management/asset path -- `deploy/proxy/Caddyfile`'s own
546
+ * explicit list, restated here as one regex since the tunnel routes by a
547
547
  * single `path` per ingress rule rather than a set. Anything not matching
548
548
  * falls through to the login app's own ingress rule below (login v2 answers
549
- * everything else, including the root).
549
+ * everything else, including the root). `/ui/console` and `/ui/login/*` (the
550
+ * v1 login) are left out on purpose: nobody reaches ZITADEL's own screens, so
551
+ * they land on the login app, which sends them back to the app.
550
552
  */
551
- export declare const AUTH_ZITADEL_PATH_PATTERN = "^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|ui/console|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)";
553
+ export declare const AUTH_ZITADEL_PATH_PATTERN = "^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)";
552
554
  /**
553
555
  * The broker's MCP authorization server (auth ADR 0002): its metadata and `/mcp-oauth/*`.
554
556
  * Checked before {@link AUTH_ZITADEL_PATH_PATTERN}, which takes every other `/.well-known` path.
@@ -1000,7 +1002,7 @@ export interface PlatformArchonDeps {
1000
1002
  releasePreviewMembers?: ReleasePreviewMembers;
1001
1003
  cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
1002
1004
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
1003
- zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'> & Partial<Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'allowPerAppLoginBaseUri' | 'updateOidcAppPostLogoutRedirectUris' | 'getOidcAppRedirectUris' | 'updateOidcAppRedirectUris'>>;
1005
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'> & Partial<Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'allowPerAppLoginBaseUri' | 'listOrgs' | 'listProjects' | 'updateOidcAppPostLogoutRedirectUris' | 'getOidcAppRedirectUris' | 'updateOidcAppRedirectUris'>>;
1004
1006
  /**
1005
1007
  * {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client:
1006
1008
  * the owner company's own ZITADEL, by its issuer and the held-key reference of
@@ -1641,7 +1643,7 @@ export interface PlatformAccessProviderDeps {
1641
1643
  /** Edge's Access calls, at its recorded address (see {@link PlatformAccessFactory}). */
1642
1644
  access: (edgeUrl: string) => PlatformAccessProviderPort | Promise<PlatformAccessProviderPort>;
1643
1645
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
1644
- zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'listApps' | 'deleteApp'> & Required<Pick<ZitadelPort, 'createOidcAppWithSecret'>>;
1646
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'listApps' | 'deleteApp'> & Required<Pick<ZitadelPort, 'createOidcAppWithSecret'>> & Partial<Pick<ZitadelPort, 'updateOidcAppLoginBaseUri'>>;
1645
1647
  /** {@link PlatformArchonDeps.zitadelForInstance}: the ZITADEL client for the sign-in at `issuerUrl`. */
1646
1648
  zitadelForInstance?: (issuerUrl: string, bearerRef?: string) => PlatformAccessProviderDeps['zitadel'];
1647
1649
  /** Sleep and delivery tries for the one call that carries the secret; real ones by default. */
@@ -13,7 +13,7 @@ import { previewsOn } from './coolify-previews.js';
13
13
  import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
14
14
  import { desktopCallbackUri, unionRedirectUris } from './desktop-callback.js';
15
15
  import { placedOnPlatform } from './destinations.js';
16
- import { ACCESS_POLICY_NAME, LOG_DIR, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
16
+ import { ACCESS_POLICY_NAME, LOG_DIR, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, setOidcAppLoginBaseUri, } from './executor.js';
17
17
  import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
18
18
  import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, tcpProbe, } from './health-check-settings.js';
19
19
  import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
@@ -27,12 +27,13 @@ import { BROKER_DB_NAME, BROKER_DB_USER, ensureBrokerMcpEnv } from './broker-mcp
27
27
  import { withTimeout } from './call-timeout.js';
28
28
  import { SIGN_IN_BIND_KEY_ENV, SIGN_IN_BIND_KEY_MIN_LENGTH, SIGN_IN_BIND_KEY_SCOPE_KEY, } from './cookie-domain.js';
29
29
  import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
30
+ import { LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION } from './login-host.js';
30
31
  import { MAIL_RELAY_BLOCKED, provisionMailRelay } from './mail-relay.js';
31
32
  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';
32
33
  import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
33
34
  import { recordOwnDnsHostname } from './own-dns.js';
34
35
  import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
35
- import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, GITHUB_RELEASE_HOOK_DEPS_MISSING, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, 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_WEBHOOK_SECRET_DEPLOYED_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_MCP_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_TUNNEL_ROUTES_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_GITHUB_RELEASE_HOOK_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, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
36
+ import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, GITHUB_RELEASE_HOOK_DEPS_MISSING, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, 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_TICKS_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_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_MCP_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_TUNNEL_ROUTES_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_GITHUB_RELEASE_HOOK_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_AUDIT_SEAL_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF, 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, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeTicksSyncValue, uptimeVarsDigest, } from './platform-diff.js';
36
37
  import { PLATFORM_REGISTRY_KEY, PLATFORM_REGISTRY_LOGINS_KEY, PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY, PLATFORM_REGISTRY_PULL_PASSWORD_REF, registryImageTagBlocked, } from './platform-registry-keys.js';
37
38
  import { ensureRegistryLogins, provisionPlatformRegistry, registryLoginsDrifted, } from './platform-registry.js';
38
39
  import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
@@ -730,7 +731,10 @@ export class FoundryPlatformStepExecutor {
730
731
  const accessHeld = Boolean((await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF)) &&
731
732
  (await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF)));
732
733
  blocked.uptimeHeld = {
733
- token: Boolean(await deps.heldKeys.resolve(PLATFORM_UPTIME_TOKEN_REF)),
734
+ // The Worker token and both tick tokens: the step is not applied while one is missing.
735
+ token: Boolean((await deps.heldKeys.resolve(PLATFORM_UPTIME_TOKEN_REF)) &&
736
+ (await deps.heldKeys.resolve(PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF)) &&
737
+ (await deps.heldKeys.resolve(PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF))),
734
738
  access: accessHeld,
735
739
  vars: uptimeVarsDigest(uptimeEffectiveVars(deps.vars, deps.chat && owner.chat && recordedIds[PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY]
736
740
  ? {
@@ -1473,17 +1477,18 @@ export const AUTH_LOGIN_PORT = '3000';
1473
1477
  /** `broker/Dockerfile`'s own `ENV PORT=3000` / `EXPOSE 3000`. */
1474
1478
  const AUTH_BROKER_PORT = '3000';
1475
1479
  /**
1476
- * Every ZITADEL OIDC/management/console/asset path -- `deploy/proxy/Caddyfile`'s
1477
- * own explicit list, restated here as one regex since the tunnel routes by a
1480
+ * Every ZITADEL OIDC/management/asset path -- `deploy/proxy/Caddyfile`'s own
1481
+ * explicit list, restated here as one regex since the tunnel routes by a
1478
1482
  * single `path` per ingress rule rather than a set. Anything not matching
1479
1483
  * falls through to the login app's own ingress rule below (login v2 answers
1480
- * everything else, including the root).
1484
+ * everything else, including the root). `/ui/console` and `/ui/login/*` (the
1485
+ * v1 login) are left out on purpose: nobody reaches ZITADEL's own screens, so
1486
+ * they land on the login app, which sends them back to the app.
1481
1487
  */
1482
1488
  // `v2`, `v2beta` and `zitadel.<service>` are ZITADEL's v2 REST and connect
1483
1489
  // APIs (providers' `inviteOrgOwner` calls `POST /v2/users`); without them those
1484
- // calls reach the login app and answer 404. The login app itself lives under
1485
- // `/ui/v2/login`, which none of these match.
1486
- export const AUTH_ZITADEL_PATH_PATTERN = '^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|ui/console|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)';
1490
+ // calls reach the login app and answer 404. None of these match `/ui/*`.
1491
+ export const AUTH_ZITADEL_PATH_PATTERN = '^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)';
1487
1492
  /**
1488
1493
  * `@wtfalch/auth`'s own `brokerUrl` default, `<issuer>/api`
1489
1494
  * (`packages/auth/src/config.ts`) -- `deploy/proxy/Caddyfile`'s own
@@ -4671,6 +4676,8 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4671
4676
  // `docs/operator-deployment.md`, step 4).
4672
4677
  redirectUris: [`https://${hostname}/auth/callback`, desktopCallbackUri(hostname)],
4673
4678
  postLogoutRedirectUris: postLogoutUrisFor([hostname]),
4679
+ // The owner company's login host: with `loginV2.required` off a client with none lands on the v1 login.
4680
+ loginBaseUri: `https://${ownerSignIn.hostname}/`,
4674
4681
  });
4675
4682
  context.recordScopeId(PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, created.appId);
4676
4683
  context.recordScopeId(PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, postLogoutAppliedValue(created.appId, postLogoutUrisFor([hostname])));
@@ -4693,7 +4700,7 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4693
4700
  const archonAppZitadelId = context.recordedIds()[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
4694
4701
  const inAppOwed = archonInAppHost(context.recordedIds()) !== '';
4695
4702
  const inAppCalls = () => {
4696
- const { ensureTrustedDomain, deleteTrustedDomain, updateOidcAppLoginBaseUri, allowPerAppLoginBaseUri, } = zitadel;
4703
+ const { ensureTrustedDomain, deleteTrustedDomain, updateOidcAppLoginBaseUri, allowPerAppLoginBaseUri, listOrgs, listProjects, } = zitadel;
4697
4704
  if (!ensureTrustedDomain || !deleteTrustedDomain || !updateOidcAppLoginBaseUri) {
4698
4705
  throw new Error("platform:archon: the owner company's ZITADEL port lacks ensureTrustedDomain, deleteTrustedDomain or updateOidcAppLoginBaseUri, so Archon cannot sign in on its own address");
4699
4706
  }
@@ -4701,9 +4708,12 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4701
4708
  ensureTrustedDomain: ensureTrustedDomain.bind(zitadel),
4702
4709
  deleteTrustedDomain: deleteTrustedDomain.bind(zitadel),
4703
4710
  updateOidcAppLoginBaseUri: updateOidcAppLoginBaseUri.bind(zitadel),
4711
+ listApps: zitadel.listApps.bind(zitadel),
4704
4712
  ...(allowPerAppLoginBaseUri
4705
4713
  ? { allowPerAppLoginBaseUri: allowPerAppLoginBaseUri.bind(zitadel) }
4706
4714
  : {}),
4715
+ ...(listOrgs ? { listOrgs: listOrgs.bind(zitadel) } : {}),
4716
+ ...(listProjects ? { listProjects: listProjects.bind(zitadel) } : {}),
4707
4717
  };
4708
4718
  };
4709
4719
  const inAppTarget = {
@@ -4881,6 +4891,21 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4881
4891
  const uptimeToken = uptimeTokenId ? await deps.heldKeys.resolve(PLATFORM_UPTIME_TOKEN_REF) : null;
4882
4892
  if (uptimeToken)
4883
4893
  envs.push({ key: 'UPTIME_TOKEN', value: uptimeToken });
4894
+ // The two tick tokens, the same way. Foundry's held value replaces one set by hand, so the Worker
4895
+ // and Archon hold the same value.
4896
+ const ticksWanted = platform.uptime.enabled &&
4897
+ Boolean(recorded[PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY]) &&
4898
+ Boolean(recorded[PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY]);
4899
+ const releaseTickToken = ticksWanted
4900
+ ? await deps.heldKeys.resolve(PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF)
4901
+ : null;
4902
+ const auditSealTickToken = ticksWanted
4903
+ ? await deps.heldKeys.resolve(PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF)
4904
+ : null;
4905
+ const ticksSyncValue = releaseTickToken && auditSealTickToken ? uptimeTicksSyncValue(recorded) : '';
4906
+ if (releaseTickToken && auditSealTickToken) {
4907
+ envs.push({ key: 'RELEASE_TICK_TOKEN', value: releaseTickToken }, { key: 'AUDIT_SEAL_TICK_TOKEN', value: auditSealTickToken });
4908
+ }
4884
4909
  const changed = await envsDiffer(deps.coolify, appId, envs);
4885
4910
  // #467: this run's intended env set, hashed before anything below writes
4886
4911
  // it -- compared against {@link PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY}
@@ -4928,6 +4953,10 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4928
4953
  for (const stale of await deps.coolify.listEnvs(appId)) {
4929
4954
  // foundry#542: a disabled Worker's token leaves Archon's env too.
4930
4955
  const staleUptime = !platform.uptime.enabled && stale.key === 'UPTIME_TOKEN';
4956
+ // The tick tokens go only if foundry wrote them: a hand-set one is not ours to remove.
4957
+ const staleUptimeTick = !platform.uptime.enabled &&
4958
+ Boolean(recorded[PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY]) &&
4959
+ (stale.key === 'RELEASE_TICK_TOKEN' || stale.key === 'AUDIT_SEAL_TICK_TOKEN');
4931
4960
  // People now come from the owner company's store, so a first operator set by an older apply goes.
4932
4961
  const staleOperator = stale.key === 'AUTHZ_BOOTSTRAP_OPERATOR';
4933
4962
  // The link is written only while both ids are known; one that is no longer wanted goes.
@@ -4936,6 +4965,7 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4936
4965
  const staleSignIn = !inAppWanted && stale.key === SIGN_IN_UPSTREAM_ENV;
4937
4966
  if ((stale.key === 'FILES_MANAGEMENT_KEY' ||
4938
4967
  staleUptime ||
4968
+ staleUptimeTick ||
4939
4969
  staleOperator ||
4940
4970
  staleLink ||
4941
4971
  staleSignIn) &&
@@ -5217,6 +5247,7 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
5217
5247
  });
5218
5248
  }
5219
5249
  await record(PLATFORM_ARCHON_IN_APP_SCOPE_KEY, `${hostname}=${clientId}`);
5250
+ await record(LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION);
5220
5251
  }
5221
5252
  else if (inAppOwed) {
5222
5253
  if (!ownerSignIn.signInLoginAppId) {
@@ -5244,6 +5275,16 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
5244
5275
  (!selfRedeploy || deployState === 'finished')) {
5245
5276
  await record(PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, '');
5246
5277
  }
5278
+ if (!platform.uptime.enabled &&
5279
+ recorded[PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY] &&
5280
+ (!selfRedeploy || deployState === 'finished')) {
5281
+ await record(PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY, '');
5282
+ }
5283
+ if (ticksSyncValue &&
5284
+ recorded[PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY] !== ticksSyncValue &&
5285
+ (!selfRedeploy || deployState === 'finished')) {
5286
+ await record(PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY, ticksSyncValue);
5287
+ }
5247
5288
  if (uptimeToken &&
5248
5289
  uptimeTokenId &&
5249
5290
  recorded[PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY] !== uptimeTokenId &&
@@ -8320,6 +8361,11 @@ async function provisionPlatformAccessProvider(context, deps) {
8320
8361
  context.recordScopeId(PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, client.appId);
8321
8362
  context.recordScopeId(PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, client.appId);
8322
8363
  await context.persist?.();
8364
+ // The owner company's login host, so the client never lands on the v1 login when `loginV2.required` is off.
8365
+ // After the ids are persisted: a failure here leaves the app recorded and costs no second secret.
8366
+ if (zitadel.updateOidcAppLoginBaseUri) {
8367
+ await setOidcAppLoginBaseUri({ updateOidcAppLoginBaseUri: zitadel.updateOidcAppLoginBaseUri.bind(zitadel) }, org.id, project.id, client.appId, `https://${ownerSignIn.hostname}/`);
8368
+ }
8323
8369
  let providerId = '';
8324
8370
  await deliverWithRetry(async (_ref, secret) => {
8325
8371
  providerId = (await access.ensureProvider({
@@ -1,5 +1,6 @@
1
1
  import { ZitadelError } from '@wtfalch/providers/zitadel';
2
2
  import { APP_ENTRY_KEY } from '../org.js';
3
+ import { ANALYTICS_MANAGED_ENV_KEYS } from './analytics.js';
3
4
  import { BOULE_MAIL_TOKEN_ENV, BOULE_MANAGED_ENV_KEYS } from './boule.js';
4
5
  import { previewsOn } from './coolify-previews.js';
5
6
  import { KOSMOS_MANAGED_ENV_KEYS } from './kosmos.js';
@@ -320,6 +321,19 @@ async function withKosmosEnvConfig(client, resource, recorded) {
320
321
  }
321
322
  return { ...resource, config: { ...resource.config, env } };
322
323
  }
324
+ /** The recorded analytics backend's live env (`APP_URL`, `AUTH_ORGANIZATION_ID`) as `config.env`, read like {@link withKosmosEnvConfig}. */
325
+ async function withAnalyticsEnvConfig(client, resource, recorded) {
326
+ if (resource.kind !== 'application' || !recorded[resource.key]?.startsWith('analytics:')) {
327
+ return resource;
328
+ }
329
+ const managed = ANALYTICS_MANAGED_ENV_KEYS;
330
+ const env = {};
331
+ for (const e of await client.listEnvs(resource.key)) {
332
+ if (!e.is_preview && managed.includes(e.key))
333
+ env[e.key] = e.value;
334
+ }
335
+ return { ...resource, config: { ...resource.config, env } };
336
+ }
323
337
  /** `@wtfalch/providers`' own `LiveResource` (`resource.ts`) has no `system` field — foundry's adds it at this boundary. */
324
338
  function toFoundryResources(system, resources) {
325
339
  return resources.map((r) => ({ system, kind: r.kind, key: r.key, config: r.config }));
@@ -354,7 +368,8 @@ export class ProvidersCoolifyReader {
354
368
  const liveKeys = new Set(withAgoraConfig.map((r) => r.key));
355
369
  const withBouleConfig = await Promise.all(withAgoraConfig.map((r) => withBouleEnvConfig(this.client, r, recorded, liveKeys)));
356
370
  const withKosmosConfig = await Promise.all(withBouleConfig.map((r) => withKosmosEnvConfig(this.client, r, recorded)));
357
- return applyRecordedIds(withKosmosConfig, recorded);
371
+ const withAnalyticsConfig = await Promise.all(withKosmosConfig.map((r) => withAnalyticsEnvConfig(this.client, r, recorded)));
372
+ return applyRecordedIds(withAnalyticsConfig, recorded);
358
373
  }
359
374
  async liveEnvEquals(orgId, appKey, name, value) {
360
375
  const recorded = await this.resolveRecordedIds(orgId);