@wtfalch/foundry 0.66.0 → 0.68.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 (47) hide show
  1. package/dist/build-json.d.ts +16 -1
  2. package/dist/build-json.js +21 -5
  3. package/dist/catalogue/copy-en.js +5 -0
  4. package/dist/catalogue/integrations.js +5 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +1 -0
  7. package/dist/modules/analytics.d.ts +8 -0
  8. package/dist/modules/analytics.js +28 -0
  9. package/dist/modules/assistants.js +2 -2
  10. package/dist/modules/index.js +2 -0
  11. package/dist/org.d.ts +9 -1
  12. package/dist/org.js +6 -3
  13. package/dist/plan/analytics-boule.d.ts +17 -0
  14. package/dist/plan/analytics-boule.js +43 -0
  15. package/dist/plan/analytics.d.ts +58 -0
  16. package/dist/plan/analytics.js +89 -0
  17. package/dist/plan/blue-green.d.ts +6 -0
  18. package/dist/plan/blue-green.js +7 -0
  19. package/dist/plan/company-backups.js +2 -0
  20. package/dist/plan/diff.d.ts +2 -0
  21. package/dist/plan/diff.js +87 -0
  22. package/dist/plan/entry-analytics-key.d.ts +35 -0
  23. package/dist/plan/entry-analytics-key.js +134 -0
  24. package/dist/plan/executor.d.ts +36 -0
  25. package/dist/plan/executor.js +290 -1
  26. package/dist/plan/hostnames.js +4 -0
  27. package/dist/plan/index.d.ts +2 -0
  28. package/dist/plan/index.js +1 -0
  29. package/dist/plan/live.d.ts +1 -1
  30. package/dist/plan/locked-hostnames.js +3 -0
  31. package/dist/plan/org-names.d.ts +3 -3
  32. package/dist/plan/org-names.js +3 -0
  33. package/dist/plan/own-limits.d.ts +6 -0
  34. package/dist/plan/own-limits.js +1 -0
  35. package/dist/plan/plan-apply.d.ts +3 -0
  36. package/dist/plan/plan-apply.js +15 -1
  37. package/dist/plan/platform-diff.d.ts +17 -1
  38. package/dist/plan/platform-diff.js +35 -2
  39. package/dist/plan/platform-providers.js +42 -2
  40. package/dist/plan/providers-readers.js +16 -1
  41. package/dist/plan/stage-services.d.ts +3 -1
  42. package/dist/plan/stage-services.js +2 -1
  43. package/dist/plan/uptime-worker.js +53 -3
  44. package/dist/slug.d.ts +1 -1
  45. package/dist/slug.js +5 -2
  46. package/dist/worker-app-config.js +2 -2
  47. package/package.json +1 -1
@@ -1272,6 +1272,13 @@ export const PLATFORM_UPTIME_KV_TITLE = 'platform-uptime';
1272
1272
  * a blocked item, never a throw.
1273
1273
  */
1274
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';
1275
1282
  export const PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF = 'uptime:worker:access-client-id';
1276
1283
  export const PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF = 'uptime:worker:access-client-secret';
1277
1284
  export const PLATFORM_UPTIME_CHAT_TOKEN_REF = 'uptime:worker:chat-token';
@@ -1296,6 +1303,19 @@ export const PLATFORM_UPTIME_OPTIONAL_SECRETS = {
1296
1303
  */
1297
1304
  export const PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY = 'uptime_token_undelivered';
1298
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
+ }
1299
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}. */
1300
1320
  export const PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY = 'archon_uptime_token_synced';
1301
1321
  /** The tag, sha256 and Archon hostname the last successful run applied; a change plans the step again and mints nothing. */
@@ -1339,9 +1359,16 @@ export function uptimeVarsDigest(vars = {}) {
1339
1359
  return '';
1340
1360
  return createHash('sha256').update(JSON.stringify(set)).digest('hex').slice(0, 16);
1341
1361
  }
1342
- /** 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. */
1343
1363
  export function uptimeSecretNames(held) {
1344
- 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
+ ]
1345
1372
  .sort()
1346
1373
  .join(',');
1347
1374
  }
@@ -3250,6 +3277,7 @@ function planUptime(platform, recordedIds, executorBlocked, heldOptional, archon
3250
3277
  const applied = deployed &&
3251
3278
  Boolean(recordedIds[PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY]) &&
3252
3279
  uptimeHeld.token &&
3280
+ Boolean(uptimeTicksSyncValue(recordedIds)) &&
3253
3281
  Boolean(recordedIds[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY]) &&
3254
3282
  uptimeHeld.access &&
3255
3283
  recordedIds[PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY] === uptime.tag &&
@@ -3511,6 +3539,11 @@ function planArchon(platform, recordedIds, executorBlocked, ownerSignIn, ownerFi
3511
3539
  recordedIds[PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY] ===
3512
3540
  recordedIds[PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY]
3513
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]) &&
3514
3547
  // #398: a rotated integrations key id Archon's env was not yet written
3515
3548
  // with (see PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY). Absent
3516
3549
  // before the first rotation on a platform applied before #398.
@@ -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_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';
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';
@@ -731,7 +731,10 @@ export class FoundryPlatformStepExecutor {
731
731
  const accessHeld = Boolean((await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF)) &&
732
732
  (await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF)));
733
733
  blocked.uptimeHeld = {
734
- 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))),
735
738
  access: accessHeld,
736
739
  vars: uptimeVarsDigest(uptimeEffectiveVars(deps.vars, deps.chat && owner.chat && recordedIds[PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY]
737
740
  ? {
@@ -2144,6 +2147,13 @@ export async function provisionAuthStack(scope, deps, target, input) {
2144
2147
  const heldMailKey = await deps.heldKeys?.resolve(target.refs.brokerMailKey);
2145
2148
  if (heldMailKey)
2146
2149
  runtime.onSecret?.(heldMailKey);
2150
+ // Auth 0.35.0: the login app mails verification and invite codes itself (it throws without
2151
+ // `LETTERMINT_API_TOKEN` and sends nothing without `LOGIN_MAIL_FROM`). Both the login and the
2152
+ // in-app login app read `loginEnvs`, so both get the held key and this stack's sender address.
2153
+ if (heldMailKey)
2154
+ loginEnvs.push({ key: 'LETTERMINT_API_TOKEN', value: heldMailKey });
2155
+ if (input.smtpSender)
2156
+ loginEnvs.push({ key: 'LOGIN_MAIL_FROM', value: input.smtpSender });
2147
2157
  // `AUTH_APPS` and `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` are not pushed here
2148
2158
  // -- this function's own doc comment, "Known gaps". `LETTERMINT_API_TOKEN`
2149
2159
  // is, once held (#210): pushing it costs nothing even while the broker
@@ -4888,6 +4898,21 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4888
4898
  const uptimeToken = uptimeTokenId ? await deps.heldKeys.resolve(PLATFORM_UPTIME_TOKEN_REF) : null;
4889
4899
  if (uptimeToken)
4890
4900
  envs.push({ key: 'UPTIME_TOKEN', value: uptimeToken });
4901
+ // The two tick tokens, the same way. Foundry's held value replaces one set by hand, so the Worker
4902
+ // and Archon hold the same value.
4903
+ const ticksWanted = platform.uptime.enabled &&
4904
+ Boolean(recorded[PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY]) &&
4905
+ Boolean(recorded[PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY]);
4906
+ const releaseTickToken = ticksWanted
4907
+ ? await deps.heldKeys.resolve(PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF)
4908
+ : null;
4909
+ const auditSealTickToken = ticksWanted
4910
+ ? await deps.heldKeys.resolve(PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF)
4911
+ : null;
4912
+ const ticksSyncValue = releaseTickToken && auditSealTickToken ? uptimeTicksSyncValue(recorded) : '';
4913
+ if (releaseTickToken && auditSealTickToken) {
4914
+ envs.push({ key: 'RELEASE_TICK_TOKEN', value: releaseTickToken }, { key: 'AUDIT_SEAL_TICK_TOKEN', value: auditSealTickToken });
4915
+ }
4891
4916
  const changed = await envsDiffer(deps.coolify, appId, envs);
4892
4917
  // #467: this run's intended env set, hashed before anything below writes
4893
4918
  // it -- compared against {@link PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY}
@@ -4935,6 +4960,10 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4935
4960
  for (const stale of await deps.coolify.listEnvs(appId)) {
4936
4961
  // foundry#542: a disabled Worker's token leaves Archon's env too.
4937
4962
  const staleUptime = !platform.uptime.enabled && stale.key === 'UPTIME_TOKEN';
4963
+ // The tick tokens go only if foundry wrote them: a hand-set one is not ours to remove.
4964
+ const staleUptimeTick = !platform.uptime.enabled &&
4965
+ Boolean(recorded[PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY]) &&
4966
+ (stale.key === 'RELEASE_TICK_TOKEN' || stale.key === 'AUDIT_SEAL_TICK_TOKEN');
4938
4967
  // People now come from the owner company's store, so a first operator set by an older apply goes.
4939
4968
  const staleOperator = stale.key === 'AUTHZ_BOOTSTRAP_OPERATOR';
4940
4969
  // The link is written only while both ids are known; one that is no longer wanted goes.
@@ -4943,6 +4972,7 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
4943
4972
  const staleSignIn = !inAppWanted && stale.key === SIGN_IN_UPSTREAM_ENV;
4944
4973
  if ((stale.key === 'FILES_MANAGEMENT_KEY' ||
4945
4974
  staleUptime ||
4975
+ staleUptimeTick ||
4946
4976
  staleOperator ||
4947
4977
  staleLink ||
4948
4978
  staleSignIn) &&
@@ -5252,6 +5282,16 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
5252
5282
  (!selfRedeploy || deployState === 'finished')) {
5253
5283
  await record(PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, '');
5254
5284
  }
5285
+ if (!platform.uptime.enabled &&
5286
+ recorded[PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY] &&
5287
+ (!selfRedeploy || deployState === 'finished')) {
5288
+ await record(PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY, '');
5289
+ }
5290
+ if (ticksSyncValue &&
5291
+ recorded[PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY] !== ticksSyncValue &&
5292
+ (!selfRedeploy || deployState === 'finished')) {
5293
+ await record(PLATFORM_ARCHON_UPTIME_TICKS_SYNCED_SCOPE_KEY, ticksSyncValue);
5294
+ }
5255
5295
  if (uptimeToken &&
5256
5296
  uptimeTokenId &&
5257
5297
  recorded[PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY] !== uptimeTokenId &&
@@ -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);
@@ -5,7 +5,7 @@ import { type StageKind } from './stage-isolation.js';
5
5
  * minted for it only. A stage key is worthless on production, and production's key never reaches stage. Nothing
6
6
  * here returns, logs or throws a secret: errors carry fixed text, ids and refs (CLAUDE.md, Platform steps).
7
7
  */
8
- export type StageService = 'files' | 'ai' | 'jobs';
8
+ export type StageService = 'files' | 'ai' | 'jobs' | 'analytics';
9
9
  export declare const STAGE_SERVICES: readonly StageService[];
10
10
  /** One credential a service shows once: the env name stage holds it under, and its value. */
11
11
  export interface StageSecret {
@@ -44,6 +44,8 @@ export interface StageServicesPort {
44
44
  files?: StageServicePort;
45
45
  ai?: StageServicePort;
46
46
  jobs?: StageServicePort;
47
+ /** The company's analytics backend: stage's own site key (environment `stage`), shown once with the mint. */
48
+ analytics?: StageServicePort;
47
49
  /** The id of production's jobs project, so stage's jobs key can be tried against it. */
48
50
  prodJobsProjectId?: string;
49
51
  }
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { bgKey } from './blue-green.js';
3
3
  import { isAllowed } from './stage-isolation.js';
4
- export const STAGE_SERVICES = ['files', 'ai', 'jobs'];
4
+ export const STAGE_SERVICES = ['files', 'ai', 'jobs', 'analytics'];
5
5
  /** Fixed text and ids only: safe to log, record and return. */
6
6
  export class StageServiceError extends Error {
7
7
  name = 'StageServiceError';
@@ -14,6 +14,7 @@ const SERVICE_ENV = {
14
14
  evidence: ['JOBS_SCHEDULER_URL', 'JOBS_SYNC_KEY', 'JOBS_SIGNING_SECRET'],
15
15
  url: 'JOBS_SCHEDULER_URL',
16
16
  },
17
+ analytics: { evidence: ['ANALYTICS_URL', 'ANALYTICS_SITE_KEY'], url: 'ANALYTICS_URL' },
17
18
  };
18
19
  /** The fixed reasons a used service is recorded absent. */
19
20
  export const STAGE_SERVICE_ABSENT = {
@@ -1,9 +1,19 @@
1
1
  import { randomBytes, randomUUID } from 'node:crypto';
2
2
  import { EdgeApiError } from '@wtfalch/edge';
3
3
  import { uptimeArtifactUrl } from '../platform.js';
4
- import { PLATFORM_EDGE_INTERNAL_URL_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_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_CHAT_TOKEN_REF, PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_URL_REF, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_KV_TITLE, PLATFORM_UPTIME_OPTIONAL_SECRETS, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeSecretNames, uptimeVarsDigest, } from './platform-diff.js';
5
- /** The Worker's cron trigger, from Archon's `workers/uptime/manifest.json`. */
6
- const UPTIME_CRONS = ['* * * * *'];
4
+ import { PLATFORM_EDGE_INTERNAL_URL_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_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF, PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_CHAT_TOKEN_REF, PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_URL_REF, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_KV_TITLE, PLATFORM_UPTIME_OPTIONAL_SECRETS, PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY, PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF, PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeSecretNames, uptimeVarsDigest, } from './platform-diff.js';
5
+ /**
6
+ * The Worker's cron triggers, in the order of Archon's `workers/uptime/manifest.json`: the probe, the
7
+ * usage tick, the charge tick, the release tick and the audit seal tick. The usage and charge ticks
8
+ * stay off because the step gives the Worker no `USAGE_TOKEN` or `CHARGE_TOKEN`.
9
+ */
10
+ const UPTIME_CRONS = [
11
+ '* * * * *',
12
+ '0 * * * *',
13
+ '15 3 * * *',
14
+ '*/10 * * * *',
15
+ '*/15 * * * *',
16
+ ];
7
17
  /** The date Archon's own Worker config used. */
8
18
  const UPTIME_COMPATIBILITY_DATE = '2026-09-27';
9
19
  /** How many times a delivery or a secrets write is tried before the step fails. */
@@ -124,6 +134,27 @@ async function attempt(what, task) {
124
134
  }
125
135
  throw new Error(`platform:uptime: ${what} failed after ${UPTIME_ATTEMPTS} attempts`);
126
136
  }
137
+ /**
138
+ * Holds a minted tick token under `ref`, the Worker token's pattern: the undelivered id is persisted
139
+ * before the value is held, the guard once it is. A guard with nothing held, or a marker without a
140
+ * guard, means the value is gone: a new one is minted and the old id dropped. Returns the held value
141
+ * and its id (the Worker secret's version label); the value goes nowhere else.
142
+ */
143
+ async function holdTickToken(context, deps, recorded, keys) {
144
+ const guard = recorded[keys.idKey];
145
+ const heldValue = guard ? await deps.heldKeys.resolve(keys.ref) : null;
146
+ if (guard && heldValue)
147
+ return { id: guard, value: heldValue };
148
+ const id = randomUUID();
149
+ const value = randomBytes(32).toString('hex');
150
+ context.recordScopeId(keys.undeliveredKey, id);
151
+ await context.persist?.();
152
+ await attempt(`holding token ${id}`, () => deps.deliverSecret(keys.ref, value));
153
+ context.recordScopeId(keys.idKey, id);
154
+ context.recordScopeId(keys.undeliveredKey, '');
155
+ await context.persist?.();
156
+ return { id, value };
157
+ }
127
158
  /** Removes an Access token edge made; one already gone is success. */
128
159
  async function removeAccessToken(access, id, what) {
129
160
  try {
@@ -165,6 +196,8 @@ export async function provisionPlatformUptime(context, deps) {
165
196
  }
166
197
  for (const ref of [
167
198
  PLATFORM_UPTIME_TOKEN_REF,
199
+ PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF,
200
+ PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF,
168
201
  PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF,
169
202
  PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF,
170
203
  ]) {
@@ -175,6 +208,10 @@ export async function provisionPlatformUptime(context, deps) {
175
208
  PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY,
176
209
  PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY,
177
210
  PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY,
211
+ PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY,
212
+ PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY,
213
+ PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY,
214
+ PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY,
178
215
  ]) {
179
216
  context.recordScopeId(key, '');
180
217
  }
@@ -280,6 +317,17 @@ export async function provisionPlatformUptime(context, deps) {
280
317
  tokenId = newId;
281
318
  token = minted;
282
319
  }
320
+ // The two tick tokens, minted and held the same way, each its own value.
321
+ const releaseTick = await holdTickToken(context, deps, recorded, {
322
+ ref: PLATFORM_UPTIME_RELEASE_TICK_TOKEN_REF,
323
+ idKey: PLATFORM_UPTIME_RELEASE_TICK_ID_SCOPE_KEY,
324
+ undeliveredKey: PLATFORM_UPTIME_RELEASE_TICK_UNDELIVERED_SCOPE_KEY,
325
+ });
326
+ const auditSealTick = await holdTickToken(context, deps, recorded, {
327
+ ref: PLATFORM_UPTIME_AUDIT_SEAL_TICK_TOKEN_REF,
328
+ idKey: PLATFORM_UPTIME_AUDIT_SEAL_TICK_ID_SCOPE_KEY,
329
+ undeliveredKey: PLATFORM_UPTIME_AUDIT_SEAL_TICK_UNDELIVERED_SCOPE_KEY,
330
+ });
283
331
  // The Access service token pair, minted through edge. The id is recorded
284
332
  // and persisted before the pair is held, the guard once both are held. A
285
333
  // pair that may be lost (marker without guard, or guard with nothing held)
@@ -341,6 +389,8 @@ export async function provisionPlatformUptime(context, deps) {
341
389
  const version = `pin-${uptime.sha256.slice(0, 12)}`;
342
390
  const secrets = [
343
391
  { name: 'UPTIME_TOKEN', value: token, version: tokenId },
392
+ { name: 'RELEASE_TICK_TOKEN', value: releaseTick.value, version: releaseTick.id },
393
+ { name: 'AUDIT_SEAL_TICK_TOKEN', value: auditSealTick.value, version: auditSealTick.id },
344
394
  { name: 'CF_ACCESS_CLIENT_ID', value: clientId, version: accessId },
345
395
  { name: 'CF_ACCESS_CLIENT_SECRET', value: clientSecret, version: accessId },
346
396
  // A ping URL works as a capability: a secret, not a plain text binding.
package/dist/slug.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export declare const RESERVED_SLUGS: ReadonlySet<string>;
3
3
  /** Every platform app, tunnel, project and database name starts `platform-`, so no company slug may. */
4
4
  export declare const RESERVED_SLUG_PREFIX = "platform-";
5
- /** Whether `label` ends in a suffix reserved for another org's own hostname (`-stage`, `-pr-<n>`, `-calls`, `-agora`, `-boule`, `-kosmos`, `-auth`, `-mail`, `-files`, `-sfu`, `-cms`). */
5
+ /** Whether `label` ends in a suffix reserved for another org's own hostname (`-stage`, `-pr-<n>`, `-calls`, `-agora`, `-boule`, `-kosmos`, `-analytics`, `-auth`, `-mail`, `-files`, `-sfu`, `-cms`). */
6
6
  export declare function hasBarredSlugSuffix(label: string): boolean;
7
7
  /**
8
8
  * Why {@link validateSlug} refused a candidate slug, by name so a caller —
package/dist/slug.js CHANGED
@@ -146,6 +146,8 @@ const BOULE_SUFFIX = /-boule$/;
146
146
  * hostname once kosmos is on, not because every org gets one.
147
147
  */
148
148
  const KOSMOS_SUFFIX = /-kosmos$/;
149
+ /** `<something>-analytics` — a level-1 org's own analytics backend hostname (`<slug>-analytics.oikos.host`, foundry#724). Reserved always, so a company that turns analytics on later never finds the name taken. */
150
+ const ANALYTICS_SUFFIX = /-analytics$/;
149
151
  /** `<something>-auth|-mail|-files|-sfu|-cms` — an own company's service hostnames on oikos.host (`plan/own-hostnames.ts`). */
150
152
  const OWN_SERVICE_SUFFIX = /-(auth|mail|files|sfu|cms)$/;
151
153
  /**
@@ -159,7 +161,7 @@ const OWN_SERVICE_SUFFIX = /-(auth|mail|files|sfu|cms)$/;
159
161
  * `names-`.
160
162
  */
161
163
  const NAMES_PREFIX = /^names(-|$)/;
162
- /** Whether `label` ends in a suffix reserved for another org's own hostname (`-stage`, `-pr-<n>`, `-calls`, `-agora`, `-boule`, `-kosmos`, `-auth`, `-mail`, `-files`, `-sfu`, `-cms`). */
164
+ /** Whether `label` ends in a suffix reserved for another org's own hostname (`-stage`, `-pr-<n>`, `-calls`, `-agora`, `-boule`, `-kosmos`, `-analytics`, `-auth`, `-mail`, `-files`, `-sfu`, `-cms`). */
163
165
  export function hasBarredSlugSuffix(label) {
164
166
  return (STAGE_SUFFIX.test(label) ||
165
167
  PR_SUFFIX.test(label) ||
@@ -167,6 +169,7 @@ export function hasBarredSlugSuffix(label) {
167
169
  AGORA_SUFFIX.test(label) ||
168
170
  BOULE_SUFFIX.test(label) ||
169
171
  KOSMOS_SUFFIX.test(label) ||
172
+ ANALYTICS_SUFFIX.test(label) ||
170
173
  OWN_SERVICE_SUFFIX.test(label));
171
174
  }
172
175
  /**
@@ -198,7 +201,7 @@ export function validateSlug(slug) {
198
201
  if (hasBarredSlugSuffix(slug)) {
199
202
  return {
200
203
  reason: 'environment-suffix',
201
- message: `slug '${slug}' ends with an environment suffix ('-stage', '-pr-<digits>', '-calls', '-agora', '-boule', '-kosmos', '-auth', '-mail', '-files', '-sfu' or '-cms') reserved for another org's own hostname (#28, #145, #164, #161, #949) — 'acme-calls' is only legal as acme's calls hostname, 'acme-agora' only as acme's Agora hostname, 'acme-boule' only as acme's Boule hostname, 'acme-kosmos' only as acme's kosmos hostname, never as an org slug in its own right`,
204
+ message: `slug '${slug}' ends with an environment suffix ('-stage', '-pr-<digits>', '-calls', '-agora', '-boule', '-kosmos', '-analytics', '-auth', '-mail', '-files', '-sfu' or '-cms') reserved for another org's own hostname (#28, #145, #164, #161, #949) — 'acme-calls' is only legal as acme's calls hostname, 'acme-agora' only as acme's Agora hostname, 'acme-boule' only as acme's Boule hostname, 'acme-kosmos' only as acme's kosmos hostname, never as an org slug in its own right`,
202
205
  };
203
206
  }
204
207
  return null;
@@ -196,9 +196,9 @@ export function validateEntryWorkerApp(entry, hosting, names) {
196
196
  if (Object.keys(entry.env).length > 0) {
197
197
  return `${at} has no Coolify app, so env does not apply: use workerApp.vars or workerApp.secrets`;
198
198
  }
199
- const unsupported = Object.keys(entry.uses ?? {}).find((use) => use !== 'auth' && use !== 'audit');
199
+ const unsupported = Object.keys(entry.uses ?? {}).find((use) => use !== 'auth' && use !== 'audit' && use !== 'analytics');
200
200
  if (unsupported)
201
- return `${at} supports uses.auth and uses.audit only, not uses.${unsupported}`;
201
+ return `${at} supports uses.auth, uses.audit and uses.analytics only, not uses.${unsupported}`;
202
202
  if (entry.key.length > WORKER_NAME_MAX_LENGTH - WORKER_NAME_FIXED_LENGTH) {
203
203
  return `${at} entry key is too long: the deployed Worker's name (an 8-character org hash, the key, and "${WORKER_APP_NAME_SUFFIX}") must fit Cloudflare's ${WORKER_NAME_MAX_LENGTH}-character limit`;
204
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",