@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.
- package/dist/build-json.d.ts +16 -1
- package/dist/build-json.js +21 -5
- package/dist/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/modules/analytics.d.ts +8 -0
- package/dist/modules/analytics.js +28 -0
- package/dist/modules/assistants.js +2 -2
- package/dist/modules/index.js +2 -0
- package/dist/org.d.ts +9 -1
- package/dist/org.js +6 -3
- package/dist/plan/analytics-boule.d.ts +17 -0
- package/dist/plan/analytics-boule.js +43 -0
- package/dist/plan/analytics.d.ts +58 -0
- package/dist/plan/analytics.js +89 -0
- package/dist/plan/blue-green.d.ts +6 -0
- package/dist/plan/blue-green.js +7 -0
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +87 -0
- package/dist/plan/entry-analytics-key.d.ts +35 -0
- package/dist/plan/entry-analytics-key.js +134 -0
- package/dist/plan/executor.d.ts +36 -0
- package/dist/plan/executor.js +290 -1
- package/dist/plan/hostnames.js +4 -0
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.js +3 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +3 -0
- package/dist/plan/plan-apply.js +15 -1
- package/dist/plan/platform-diff.d.ts +17 -1
- package/dist/plan/platform-diff.js +35 -2
- package/dist/plan/platform-providers.js +42 -2
- package/dist/plan/providers-readers.js +16 -1
- package/dist/plan/stage-services.d.ts +3 -1
- package/dist/plan/stage-services.js +2 -1
- package/dist/plan/uptime-worker.js +53 -3
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +5 -2
- package/dist/worker-app-config.js +2 -2
- 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
|
|
1362
|
+
/** The Worker secret names the step sets: the five it mints, and each held one. */
|
|
1343
1363
|
export function uptimeSecretNames(held) {
|
|
1344
|
-
return [
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
6
|
-
|
|
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.
|
|
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
|
}
|