@wtfalch/foundry 0.70.0 → 0.71.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.
@@ -75,6 +75,22 @@ export const PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY = 'zitadel_platform_aut
75
75
  export const AUTH_TUNNEL_ROUTES_VERSION = '2';
76
76
  /** The {@link AUTH_TUNNEL_ROUTES_VERSION} the sign-in tunnel was last written with. */
77
77
  export const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = 'auth_tunnel_routes_applied';
78
+ /**
79
+ * The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
80
+ * `LETTERMINT_API_TOKEN`). Raise it when a key is added or its meaning changes, so an applied
81
+ * sign-in plans its step once more and the apps get the env. Never holds a value.
82
+ * 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
83
+ */
84
+ export const AUTH_LOGIN_ENV_VERSION = '1';
85
+ /**
86
+ * What the login apps' env is written for: the version and the sender address (`LOGIN_MAIL_FROM`).
87
+ * The address is no secret; a new mail domain gives a new marker, so the step is planned again.
88
+ */
89
+ export function authLoginEnvMarker(smtpSender) {
90
+ return `${AUTH_LOGIN_ENV_VERSION}:${smtpSender}`;
91
+ }
92
+ /** The {@link authLoginEnvMarker} the login apps' env was last written with, once live on them. */
93
+ export const PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY = 'auth_login_env_applied';
78
94
  /**
79
95
  * `'1'` once the broker holds `MCP_DATABASE_URL` and `MCP_OAUTH_SECRET` (auth ADR 0002), recorded by the
80
96
  * sign-in step when an entry of that broker declares `uses.auth.mcp`. A broker entry with `mcp` is not
@@ -1112,6 +1128,8 @@ export const PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY = 'edge_platform_key_set';
1112
1128
  */
1113
1129
  export const PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY = 'edge_applied_hostname';
1114
1130
  export const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = 'edge_applied_git_ref';
1131
+ /** foundry#1024: what `provisionPlatformEdge` last wrote for {@link usageTailEdgeEnv}; see {@link usageTailEdgeEnvValue}. */
1132
+ export const PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY = 'edge_applied_usage_tail';
1115
1133
  /**
1116
1134
  * F4.dns: where edge answers on the Coolify network
1117
1135
  * (`http://<custom_internal_name>:3000`), recorded by the edge step once the
@@ -1396,6 +1414,56 @@ export function uptimeRecorded(recordedIds) {
1396
1414
  Boolean(recordedIds[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY]) ||
1397
1415
  Boolean(recordedIds[PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY]));
1398
1416
  }
1417
+ /**
1418
+ * foundry#1024: the usage tail Worker that meters every company Worker's
1419
+ * use, one `cloudflare:worker` step keyed {@link PLATFORM_USAGE_TAIL_KEY}.
1420
+ * `runPlatformSteps` records the script name under
1421
+ * `cloudflare:worker:platform:usage-tail` once it succeeds, which is what
1422
+ * stops `planUsageTail` planning it again.
1423
+ */
1424
+ export const PLATFORM_USAGE_TAIL_KEY = 'platform:usage-tail';
1425
+ /** The Worker script's name at Cloudflare. */
1426
+ export const PLATFORM_USAGE_TAIL_SCRIPT_NAME = 'platform-usage-tail';
1427
+ /**
1428
+ * The held ref of the Worker's one secret, `EDGE_USAGE_KEY` -- the narrow
1429
+ * `usage:ingest` credential edge's ingest route accepts. Minted once (rule
1430
+ * 1): the hash is never secret, so (the same pattern `provisionPlatformEdge`
1431
+ * uses for `EDGE_PLATFORM_KEY_HASH`) it doubles as both the undelivered
1432
+ * marker and the final recorded value, with nothing extra to compute or a
1433
+ * live read that could fall back to a blank value.
1434
+ */
1435
+ export const PLATFORM_USAGE_TAIL_KEY_REF = 'usage-tail:worker:key';
1436
+ export const PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY = 'usage_tail_key_undelivered';
1437
+ export const PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY = 'usage_tail_key_set';
1438
+ export const PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY = 'usage_tail_applied_tag';
1439
+ export const PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY = 'usage_tail_applied_sha256';
1440
+ export const PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY = 'usage_tail_removed';
1441
+ /**
1442
+ * What `platform:edge`'s own env should hold for the usage tail Worker
1443
+ * (`USAGE_TAIL_WORKER`, `EDGE_USAGE_KEY_HASH`), or `null` while the Worker is
1444
+ * not deployed or its key is not held yet. `provisionPlatformEdge` writes
1445
+ * these two names on every run alongside `EDGE_PLATFORM_KEY_HASH`; `planEdge`
1446
+ * reads {@link usageTailEdgeEnvValue} to replan edge once this changes.
1447
+ */
1448
+ export function usageTailEdgeEnv(recordedIds) {
1449
+ const deployed = Boolean(recordedIds[resourceKey('cloudflare', 'worker', PLATFORM_USAGE_TAIL_KEY)]) &&
1450
+ !recordedIds[PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY];
1451
+ const hash = recordedIds[PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY];
1452
+ if (!deployed || !hash)
1453
+ return null;
1454
+ return { USAGE_TAIL_WORKER: PLATFORM_USAGE_TAIL_SCRIPT_NAME, EDGE_USAGE_KEY_HASH: hash };
1455
+ }
1456
+ /** The value `provisionPlatformEdge` records once it has written {@link usageTailEdgeEnv} to edge's env; empty while there is nothing to write. */
1457
+ export function usageTailEdgeEnvValue(recordedIds) {
1458
+ const env = usageTailEdgeEnv(recordedIds);
1459
+ return env ? `${env.USAGE_TAIL_WORKER}|${env.EDGE_USAGE_KEY_HASH}` : '';
1460
+ }
1461
+ /** Whether anything of the usage tail Worker is recorded that disabling it must remove. */
1462
+ export function usageTailRecorded(recordedIds) {
1463
+ return ((Boolean(recordedIds[resourceKey('cloudflare', 'worker', PLATFORM_USAGE_TAIL_KEY)]) &&
1464
+ !recordedIds[PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY]) ||
1465
+ Boolean(recordedIds[PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY]));
1466
+ }
1399
1467
  /**
1400
1468
  * #167: the platform's own diff engine, mirroring `diff.ts`'s
1401
1469
  * `computePlanDetail` for the one shared {@link DeclaredPlatform} record
@@ -1449,6 +1517,11 @@ export function planPlatform(platform, live, options = {}) {
1449
1517
  // write, so while edge is neither applied nor planned in this plan they are
1450
1518
  // blocked, not planned (rule 2), naming edge as the cause.
1451
1519
  const edgeApplied = Boolean(recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]);
1520
+ // foundry#1024: after edge, which it uploads the script through and whose
1521
+ // env takes the key's hash.
1522
+ const usageTail = planUsageTail(platform, recordedIds, blocked.usageTail, edgeApplied);
1523
+ if (usageTail.step)
1524
+ plannedSteps.push(usageTail.step);
1452
1525
  const dnsBlocked = edgeApplied || edge.step
1453
1526
  ? undefined
1454
1527
  : (service) => `the platform's ${service} is blocked, not planned: every DNS record is written through edge, and edge is neither applied nor planned (${platform.edge.enabled
@@ -1480,7 +1553,7 @@ export function planPlatform(platform, live, options = {}) {
1480
1553
  // reconcile below can be planned in the *same* round hc first comes up,
1481
1554
  // the same way files/mail/archon are planned in auth's own first round.
1482
1555
  const hcReady = hc.step !== null || Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_HC_KEY)]);
1483
- const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false);
1556
+ const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? [], blocked.authOwnerInvite, drifted.authApps ?? false, blocked.authMailKeyHeld ?? true);
1484
1557
  if (auth.step)
1485
1558
  plannedSteps.push(auth.step);
1486
1559
  // #174: the integrations host. Planned after auth; neither step reads the
@@ -1718,6 +1791,9 @@ export function planPlatform(platform, live, options = {}) {
1718
1791
  unmanaged.push({ description: heartbeats.blocked, key: 'heartbeats' });
1719
1792
  for (const description of uptime.blocked)
1720
1793
  unmanaged.push({ description, key: 'uptime' });
1794
+ for (const description of usageTail.blocked) {
1795
+ unmanaged.push({ description, key: 'usage-tail' });
1796
+ }
1721
1797
  if (releaseHook.blocked) {
1722
1798
  unmanaged.push({ description: releaseHook.blocked, key: 'github-release-hook' });
1723
1799
  }
@@ -2153,7 +2229,7 @@ function planSfu(platform, callsOrgSlugs, recordedIds) {
2153
2229
  * promising "this apply re-mints" while one of those is still missing would
2154
2230
  * be wrong the moment it is read.
2155
2231
  */
2156
- function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false) {
2232
+ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authDrifted, otherInputsMissing = [], ownerInviteBlocked, appsDown = false, mailKeyHeld = true) {
2157
2233
  if (!platform.auth.enabled)
2158
2234
  return { step: null, blocked: null };
2159
2235
  // C3/S2: standing notes. The invite and the domain wait on a person; they hold nothing else back.
@@ -2211,6 +2287,11 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
2211
2287
  // step's redeploy, so a platform recorded before them still plans it.
2212
2288
  recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
2213
2289
  recordedIds[PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY] === AUTH_TUNNEL_ROUTES_VERSION &&
2290
+ // The login apps' mail env: a platform from before it, or with a new apex, is written again.
2291
+ // Without the held key the apply cannot record it, so it is asked for only once the key is held.
2292
+ (!mailKeyHeld ||
2293
+ recordedIds[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] ===
2294
+ authLoginEnvMarker(`noreply@${platform.apex}`)) &&
2214
2295
  recordedIds[PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY] === platform.auth.gitRef) {
2215
2296
  return { step: null, blocked: standingNote };
2216
2297
  }
@@ -2686,7 +2767,10 @@ function planFiles(platform, recordedIds, executorBlocked, authReady, drifted) {
2686
2767
  *
2687
2768
  * `drifted` (the host's own `driftedSteps`: edge holds an older Cloudflare
2688
2769
  * token) plans the same step on an applied edge. The step writes the held
2689
- * token into edge's env and redeploys it.
2770
+ * token into edge's env and redeploys it. foundry#1024: a usage tail Worker
2771
+ * key minted after edge's last deploy plans the step again the same way,
2772
+ * through {@link usageTailEdgeEnvValue} rather than a separate `drifted`
2773
+ * flag, since the planner can read it straight from `recordedIds`.
2690
2774
  *
2691
2775
  * Blocked, not planned: a platform key minted but never delivered (lost,
2692
2776
  * `ServiceError`-free the way {@link planFiles}'s own management-key check
@@ -2708,7 +2792,11 @@ function planEdge(platform, recordedIds, executorBlocked, drifted) {
2708
2792
  recordedIds[PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY] === platform.edge.hostname &&
2709
2793
  recordedIds[PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY] === platform.edge.gitRef &&
2710
2794
  // F4.dns: an edge applied before its address was recorded runs once more to record it.
2711
- Boolean(recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]);
2795
+ Boolean(recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]) &&
2796
+ // foundry#1024: a usage tail Worker key minted after edge's last deploy
2797
+ // has not reached edge's env yet; replan to write it and redeploy.
2798
+ (recordedIds[PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY] ?? '') ===
2799
+ usageTailEdgeEnvValue(recordedIds);
2712
2800
  if (applied && !drifted)
2713
2801
  return { step: null, blocked: null };
2714
2802
  if (executorBlocked)
@@ -3303,6 +3391,79 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
3303
3391
  notes,
3304
3392
  };
3305
3393
  }
3394
+ /**
3395
+ * foundry#1024: the usage tail Worker, one `cloudflare:worker` step. Blocked,
3396
+ * not planned (rule 2), while no artifact is pinned, edge is not applied, or
3397
+ * the executor lacks its dependencies. Planned again when the pinned tag or
3398
+ * sha256 changed, or a half-made deploy never recorded its applied values; it
3399
+ * mints nothing that is already held. When the platform no longer enables
3400
+ * the Worker, a destructive step removes whatever of it is recorded.
3401
+ */
3402
+ function planUsageTail(platform, recordedIds, executorBlocked, edgeApplied) {
3403
+ const { usageTail } = platform;
3404
+ const deployed = Boolean(recordedIds[resourceKey('cloudflare', 'worker', PLATFORM_USAGE_TAIL_KEY)]) &&
3405
+ !recordedIds[PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY];
3406
+ const action = (desired) => ({
3407
+ system: 'cloudflare',
3408
+ kind: 'worker',
3409
+ key: PLATFORM_USAGE_TAIL_KEY,
3410
+ desired,
3411
+ });
3412
+ if (!usageTail.enabled) {
3413
+ if (!usageTailRecorded(recordedIds))
3414
+ return { step: null, blocked: [] };
3415
+ if (executorBlocked)
3416
+ return { step: null, blocked: [executorBlocked] };
3417
+ return {
3418
+ step: {
3419
+ action: action({ remove: true }),
3420
+ step: {
3421
+ description: `remove the usage tail Worker ${PLATFORM_USAGE_TAIL_SCRIPT_NAME} and its held key`,
3422
+ destroys: true,
3423
+ migrates: false,
3424
+ },
3425
+ },
3426
+ blocked: [],
3427
+ };
3428
+ }
3429
+ if (!usageTail.tag || !usageTail.sha256) {
3430
+ return {
3431
+ step: null,
3432
+ blocked: [
3433
+ 'the usage tail Worker is blocked, not planned: no artifact is pinned; declare usageTail.tag (an edge release tag) and usageTail.sha256 (the digest of usage-tail-worker.js at that tag)',
3434
+ ],
3435
+ };
3436
+ }
3437
+ if (!edgeApplied) {
3438
+ return {
3439
+ step: null,
3440
+ blocked: [
3441
+ 'the usage tail Worker is blocked, not planned: platform:edge has not been applied yet',
3442
+ ],
3443
+ };
3444
+ }
3445
+ if (executorBlocked)
3446
+ return { step: null, blocked: [executorBlocked] };
3447
+ // Applied only once a run got to its end and recorded these: a Worker that
3448
+ // was uploaded and then failed has its key but none of the applied values.
3449
+ const applied = deployed &&
3450
+ Boolean(recordedIds[PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY]) &&
3451
+ recordedIds[PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY] === usageTail.tag &&
3452
+ recordedIds[PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY] === usageTail.sha256;
3453
+ if (applied)
3454
+ return { step: null, blocked: [] };
3455
+ return {
3456
+ step: {
3457
+ action: action({ gitRef: usageTail.tag, sha256: usageTail.sha256 }),
3458
+ step: {
3459
+ description: `deploy the usage tail Worker ${PLATFORM_USAGE_TAIL_SCRIPT_NAME} from edge release ${usageTail.tag}, with its held key`,
3460
+ destroys: false,
3461
+ migrates: false,
3462
+ },
3463
+ },
3464
+ blocked: [],
3465
+ };
3466
+ }
3306
3467
  /**
3307
3468
  * #172: the platform's AI host (https://github.com/wtfalch/ai) -- a Coolify
3308
3469
  * Postgres plus three apps built from that repo's root Dockerfile, one per
@@ -28,6 +28,7 @@ export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
28
28
  import { type PlatformGithubReleaseHookDeps } from './github-release-hook.js';
29
29
  import { type CallsSfuPlatformDeps } from './platform-sfu.js';
30
30
  import { type PlatformUptimeDeps } from './uptime-worker.js';
31
+ import { type PlatformUsageTailDeps } from './usage-tail-worker.js';
31
32
  /** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
32
33
  export declare function apexZoneKey(apex: string): string;
33
34
  /**
@@ -326,6 +327,12 @@ export interface FoundryPlatformStepExecutorOptions {
326
327
  * the same reason as `auth`; without it the step is reported blocked.
327
328
  */
328
329
  uptime?: PlatformUptimeDeps;
330
+ /**
331
+ * foundry#1024: dependencies for the usage tail Worker step
332
+ * (`cloudflare:worker` keyed {@link PLATFORM_USAGE_TAIL_KEY}) -- optional
333
+ * for the same reason as `auth`; without it the step is reported blocked.
334
+ */
335
+ usageTail?: PlatformUsageTailDeps;
329
336
  /**
330
337
  * #604: dependencies for the `cloudflare:access-provider` step -- optional
331
338
  * for the same reason as `auth`; edge and sign-in applied with this unset
@@ -362,6 +369,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
362
369
  private readonly registry?;
363
370
  private readonly heartbeats?;
364
371
  private readonly uptime?;
372
+ private readonly usageTail?;
365
373
  private readonly accessProvider?;
366
374
  private readonly oikosIngress?;
367
375
  private readonly githubReleaseHook?;
@@ -401,6 +409,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
401
409
  auth?: string;
402
410
  authExecutorMissing?: boolean;
403
411
  authOtherInputsMissing?: string[];
412
+ authMailKeyHeld?: boolean;
404
413
  authOwnerInvite?: string;
405
414
  files?: string;
406
415
  integrations?: string;
@@ -427,6 +436,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
427
436
  pending: boolean;
428
437
  blocked?: string;
429
438
  };
439
+ usageTail?: string;
430
440
  githubReleaseHook?: string;
431
441
  githubReleaseHookReady?: boolean;
432
442
  githubReleaseHookHash?: string;
@@ -449,6 +459,12 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
449
459
  * they hold.
450
460
  */
451
461
  private uptimeBlocked;
462
+ /**
463
+ * foundry#1024: why the usage tail Worker cannot run yet. A lost key is
464
+ * not a block here: the step mints a new one. The planner checks the pin
465
+ * and edge first, so none of the reads here happens before they hold.
466
+ */
467
+ private usageTailBlocked;
452
468
  /**
453
469
  * Whether Archon's live Access policy lets through anyone but the declared
454
470
  * emails, or (#211 round 4) the broker it last registered with no longer
@@ -33,7 +33,7 @@ import { MAIL_APP_SCOPE_KEY, MAIL_DATABASE_NAME, MAIL_DATABASE_PASSWORD_ENV, MAI
33
33
  import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied, oikosIngressZoneBlocked, provisionOikosIngress, } from './oikos-ingress.js';
34
34
  import { recordOwnDnsHostname } from './own-dns.js';
35
35
  import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.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
+ 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_LOGIN_ENV_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_APPLIED_USAGE_TAIL_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_USAGE_TAIL_KEY, PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, PLATFORM_USAGE_TAIL_SCRIPT_NAME, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, authLoginEnvMarker, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeTicksSyncValue, uptimeVarsDigest, usageTailEdgeEnv, usageTailEdgeEnvValue, usageTailRecorded, } from './platform-diff.js';
37
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';
38
38
  import { ensureRegistryLogins, provisionPlatformRegistry, registryLoginsDrifted, } from './platform-registry.js';
39
39
  import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
@@ -47,6 +47,7 @@ export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
47
47
  import { githubReleaseHookPlanInputs, provisionGithubReleaseHook, } from './github-release-hook.js';
48
48
  import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
49
49
  import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
50
+ import { provisionPlatformUsageTail } from './usage-tail-worker.js';
50
51
  import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, waitForSystemApi, } from './zitadel-system-api.js';
51
52
  /** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
52
53
  export function apexZoneKey(apex) {
@@ -152,6 +153,7 @@ export class FoundryPlatformStepExecutor {
152
153
  registry;
153
154
  heartbeats;
154
155
  uptime;
156
+ usageTail;
155
157
  accessProvider;
156
158
  oikosIngress;
157
159
  githubReleaseHook;
@@ -174,6 +176,7 @@ export class FoundryPlatformStepExecutor {
174
176
  this.registry = options.registry;
175
177
  this.heartbeats = options.heartbeats;
176
178
  this.uptime = options.uptime;
179
+ this.usageTail = options.usageTail;
177
180
  this.accessProvider = options.accessProvider;
178
181
  this.oikosIngress = options.oikosIngress;
179
182
  this.githubReleaseHook = options.githubReleaseHook;
@@ -407,7 +410,7 @@ export class FoundryPlatformStepExecutor {
407
410
  blocked.authExecutorMissing = true;
408
411
  }
409
412
  else if (platform.auth.enabled && this.auth) {
410
- const { reason: authBlocked, otherInputsMissing } = await authBrokerBlockedReason(this.auth, recordedIds);
413
+ const { reason: authBlocked, otherInputsMissing, mailKeyHeld, } = await authBrokerBlockedReason(this.auth, recordedIds);
411
414
  if (authBlocked) {
412
415
  blocked.auth = authBlocked;
413
416
  }
@@ -424,6 +427,9 @@ export class FoundryPlatformStepExecutor {
424
427
  // #216 round 5: see `PlanPlatformOptions.blocked.authOtherInputsMissing`.
425
428
  if (otherInputsMissing.length > 0)
426
429
  blocked.authOtherInputsMissing = otherInputsMissing;
430
+ // Without the held key `provisionAuthStack` never records the login env marker, so the plan must not ask for it.
431
+ if (mailKeyHeld !== undefined)
432
+ blocked.authMailKeyHeld = mailKeyHeld;
427
433
  // C3: the owner's invite needs a work address and the host's mail port. Without them it is
428
434
  // a blocked item, never a throw on every apply.
429
435
  const ownerCompany = await this.declaredOrgStore.get(platform.ownerOrgId);
@@ -612,6 +618,7 @@ export class FoundryPlatformStepExecutor {
612
618
  blocked.heartbeatsApiKeyMissing = true;
613
619
  }
614
620
  await this.uptimeBlocked(platform, recordedIds, blocked, owner);
621
+ await this.usageTailBlocked(platform, recordedIds, blocked);
615
622
  if (platform.archon.enabled && !this.githubReleaseHook) {
616
623
  blocked.githubReleaseHook = GITHUB_RELEASE_HOOK_DEPS_MISSING;
617
624
  }
@@ -797,6 +804,38 @@ export class FoundryPlatformStepExecutor {
797
804
  }
798
805
  }
799
806
  }
807
+ /**
808
+ * foundry#1024: why the usage tail Worker cannot run yet. A lost key is
809
+ * not a block here: the step mints a new one. The planner checks the pin
810
+ * and edge first, so none of the reads here happens before they hold.
811
+ */
812
+ async usageTailBlocked(platform, recordedIds, blocked) {
813
+ const deployed = Boolean(recordedIds[`cloudflare:worker:${PLATFORM_USAGE_TAIL_KEY}`]) &&
814
+ !recordedIds[PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY];
815
+ if (!platform.usageTail.enabled && !usageTailRecorded(recordedIds))
816
+ return;
817
+ const deps = this.usageTail;
818
+ if (!deps) {
819
+ blocked.usageTail =
820
+ 'the usage tail Worker is blocked, not planned: the host built the platform executor without its usage-tail dependencies';
821
+ return;
822
+ }
823
+ if (!platform.usageTail.enabled)
824
+ return;
825
+ const edgeUrl = recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
826
+ if (!deployed && edgeUrl && platform.usageTail.tag) {
827
+ try {
828
+ const workers = await deps.workers(edgeUrl);
829
+ if (await workers.scriptExists(PLATFORM_USAGE_TAIL_SCRIPT_NAME)) {
830
+ blocked.usageTail = `the usage tail Worker is blocked, not planned: a Worker named ${PLATFORM_USAGE_TAIL_SCRIPT_NAME} already exists at Cloudflare and foundry did not make it; remove it by hand`;
831
+ }
832
+ }
833
+ catch {
834
+ blocked.usageTail =
835
+ 'the usage tail Worker is blocked, not planned: edge could not be asked whether the Worker exists';
836
+ }
837
+ }
838
+ }
800
839
  /**
801
840
  * Whether Archon's live Access policy lets through anyone but the declared
802
841
  * emails, or (#211 round 4) the broker it last registered with no longer
@@ -1156,6 +1195,14 @@ export class FoundryPlatformStepExecutor {
1156
1195
  }
1157
1196
  return provisionPlatformUptime(context, this.uptime);
1158
1197
  }
1198
+ if (action.system === 'cloudflare' &&
1199
+ action.kind === 'worker' &&
1200
+ action.key === PLATFORM_USAGE_TAIL_KEY) {
1201
+ if (!this.usageTail) {
1202
+ throw new Error('platform:usage-tail needs FoundryPlatformStepExecutor to be constructed with its usage-tail dependencies (foundry#1024)');
1203
+ }
1204
+ return provisionPlatformUsageTail(context, this.usageTail);
1205
+ }
1159
1206
  if (action.system === 'coolify' &&
1160
1207
  action.kind === 'app' &&
1161
1208
  action.key === PLATFORM_OIKOS_INGRESS_KEY) {
@@ -2239,7 +2286,16 @@ export async function provisionAuthStack(scope, deps, target, input) {
2239
2286
  await waitHealthy(withCallTimeout(deps.coolify), zitadelAppId, runtime, label, 'ZITADEL');
2240
2287
  await record(SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, '');
2241
2288
  }
2242
- if (loginApp.justCreated || !loginApp.running || loginEnvsChanged || authRefChanged) {
2289
+ // The mail env counts as live only once the login apps were deployed after it was written: a run that
2290
+ // died between the write and the deploy leaves the marker unset, and the next run sees no env diff.
2291
+ const loginEnvUnconfirmed = Boolean(heldMailKey &&
2292
+ input.smtpSender &&
2293
+ recorded[PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY] !== authLoginEnvMarker(input.smtpSender));
2294
+ if (loginApp.justCreated ||
2295
+ !loginApp.running ||
2296
+ loginEnvsChanged ||
2297
+ loginEnvUnconfirmed ||
2298
+ authRefChanged) {
2243
2299
  await deps.coolify.deploy(loginAppId);
2244
2300
  }
2245
2301
  // The key counts as live only once the app was deployed after it was written: a crash between the write
@@ -2251,11 +2307,17 @@ export async function provisionAuthStack(scope, deps, target, input) {
2251
2307
  signInEnvsChanged ||
2252
2308
  bindKeyWritten ||
2253
2309
  bindKeyUnconfirmed ||
2310
+ loginEnvUnconfirmed ||
2254
2311
  authRefChanged)) {
2255
2312
  await deps.coolify.deploy(signInApp.appId);
2256
2313
  if (input.signInLogin)
2257
2314
  await record(SIGN_IN_BIND_KEY_SCOPE_KEY, '1');
2258
2315
  }
2316
+ // The mail env is live on the login apps: written above and deployed with them. Without the held
2317
+ // key the marker stays unset, so the step is planned again once the key is held.
2318
+ if (heldMailKey && input.smtpSender) {
2319
+ await record(PLATFORM_AUTH_LOGIN_ENV_SCOPE_KEY, authLoginEnvMarker(input.smtpSender));
2320
+ }
2259
2321
  // #211: the broker's own `server.ts` refuses to start without
2260
2322
  // `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `LETTERMINT_API_TOKEN` --
2261
2323
  // deploying it without them only produces a container that crash-loops
@@ -4022,8 +4084,9 @@ async function authBrokerBlockedReason(auth, recordedIds) {
4022
4084
  // #210: and `LETTERMINT_API_TOKEN`, once held -- see this function's own
4023
4085
  // doc comment.
4024
4086
  const otherInputsMissing = missing.filter((m) => m !== 'ZITADEL_SERVICE_USER_TOKEN' && !(m === 'LETTERMINT_API_TOKEN' && heldMailKey));
4087
+ const mailKeyHeld = Boolean(heldMailKey);
4025
4088
  if (missing.length === 0)
4026
- return { reason: null, otherInputsMissing };
4089
+ return { reason: null, otherInputsMissing, mailKeyHeld };
4027
4090
  // #210: say where the operator seals the Lettermint key, not only its env name.
4028
4091
  const named = (names) => names.map((m) => m === 'LETTERMINT_API_TOKEN'
4029
4092
  ? `LETTERMINT_API_TOKEN (seal it under the held-key ref ${PLATFORM_AUTH_BROKER_MAIL_KEY_REF})`
@@ -4036,6 +4099,7 @@ async function authBrokerBlockedReason(auth, recordedIds) {
4036
4099
  return {
4037
4100
  reason: `the platform's sign-in broker ${brokerAppId} is blocked, not deployed: its AUTH_APPS is malformed${otherSuffix}`,
4038
4101
  otherInputsMissing,
4102
+ mailKeyHeld,
4039
4103
  };
4040
4104
  }
4041
4105
  // AUTH_APPS isn't gated on #209/#210 the way the two tokens are; only cite
@@ -4044,6 +4108,7 @@ async function authBrokerBlockedReason(auth, recordedIds) {
4044
4108
  return {
4045
4109
  reason: `the platform's sign-in broker is created but not deployed, blocked: it has no ${named(missing).join(' and no ')} yet${suffix}`,
4046
4110
  otherInputsMissing,
4111
+ mailKeyHeld,
4047
4112
  };
4048
4113
  }
4049
4114
  /** Polls until Coolify reports the mail app stopped, so a healthy answer read afterwards is the new deployment's. */
@@ -8119,6 +8184,12 @@ async function edgeBlockedReason(names) {
8119
8184
  * - `EDGE_TRUST_CF_CONNECTING_IP` is always written `true` rather than
8120
8185
  * left to the host's own default, since this step always puts the
8121
8186
  * runner behind the Cloudflare tunnel it also creates.
8187
+ *
8188
+ * foundry#1024: `USAGE_TAIL_WORKER` and `EDGE_USAGE_KEY_HASH` ({@link usageTailEdgeEnv})
8189
+ * are written on every run too, once the usage tail Worker step has deployed
8190
+ * and holds its key; `planEdge` replans this step while the recorded
8191
+ * {@link PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY} differs from what is
8192
+ * now desired, the same way a changed hostname or `gitRef` does.
8122
8193
  */
8123
8194
  async function provisionPlatformEdge(context, deps) {
8124
8195
  const { platform } = context;
@@ -8228,6 +8299,10 @@ async function provisionPlatformEdge(context, deps) {
8228
8299
  await record(PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, keyHash);
8229
8300
  }
8230
8301
  // ---- base env, written on every run --------------------------------------
8302
+ // foundry#1024: the usage tail Worker's name and the hash of its key, so
8303
+ // every company script can name it as its tail consumer and edge's ingest
8304
+ // route can accept the key. Absent while the Worker is not deployed yet.
8305
+ const usageTailEnv = usageTailEdgeEnv(recorded);
8231
8306
  await deps.coolify.bulkUpsertEnvs(appId, [
8232
8307
  { key: 'DATABASE_URL_OWNER', value: db.internal_db_url },
8233
8308
  { key: 'DATABASE_URL', value: runtimeUrl },
@@ -8237,6 +8312,12 @@ async function provisionPlatformEdge(context, deps) {
8237
8312
  { key: 'EDGE_TRUST_CF_CONNECTING_IP', value: 'true' },
8238
8313
  { key: 'NODE_ENV', value: 'production' },
8239
8314
  { key: 'EDGE_PLATFORM_KEY_HASH', value: keyHash },
8315
+ ...(usageTailEnv
8316
+ ? [
8317
+ { key: 'USAGE_TAIL_WORKER', value: usageTailEnv.USAGE_TAIL_WORKER },
8318
+ { key: 'EDGE_USAGE_KEY_HASH', value: usageTailEnv.EDGE_USAGE_KEY_HASH },
8319
+ ]
8320
+ : []),
8240
8321
  ...(await missingEnvs(deps.coolify, appId, PLATFORM_EDGE_RUNNER_DEFAULTS)),
8241
8322
  ]);
8242
8323
  // ---- deploy and wait, on edge's own address ---------------------------------
@@ -8297,6 +8378,7 @@ async function provisionPlatformEdge(context, deps) {
8297
8378
  }
8298
8379
  // Last: a run that failed anywhere above is planned again.
8299
8380
  context.recordScopeId(PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, hostname);
8381
+ context.recordScopeId(PLATFORM_EDGE_APPLIED_USAGE_TAIL_SCOPE_KEY, usageTailEdgeEnvValue(recorded));
8300
8382
  await record(PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, gitRef);
8301
8383
  return { resourceId: appId };
8302
8384
  }
@@ -0,0 +1,29 @@
1
+ import type { PlatformStepExecutionContext } from './plan-apply.js';
2
+ import type { PlatformWorkersFactory } from './uptime-worker.js';
3
+ /**
4
+ * Dependencies of the usage tail Worker step. `deliverSecret` and
5
+ * `heldKeys` are the host's: the minted key goes only to `deliverSecret`,
6
+ * and it is read back only to hand it to edge, which never returns one.
7
+ */
8
+ export interface PlatformUsageTailDeps {
9
+ workers: PlatformWorkersFactory;
10
+ deliverSecret: (ref: string, secret: string) => Promise<void>;
11
+ heldKeys: {
12
+ resolve(ref: string): Promise<string | null>;
13
+ };
14
+ /** Tries per key delivery (rule 1), 3 by default. */
15
+ deliverAttempts?: number;
16
+ /** Waits between delivery tries; a real timer by default. */
17
+ sleep?: (ms: number) => Promise<void>;
18
+ }
19
+ /**
20
+ * foundry#1024: makes the usage tail Worker true. Upload the pinned edge
21
+ * release (the host fetches and verifies it) and record the script at once,
22
+ * so a later failure is a half-made Worker foundry owns. Then hold the key
23
+ * (minted once, its guard recorded as soon as its value is held) and set it
24
+ * on the Worker. The applied values are recorded only when all of it
25
+ * succeeded.
26
+ */
27
+ export declare function provisionPlatformUsageTail(context: PlatformStepExecutionContext, deps: PlatformUsageTailDeps): Promise<{
28
+ resourceId?: string;
29
+ }>;