@wtfalch/foundry 0.69.0 → 0.70.1
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/bootstrap/declare.js +2 -0
- package/dist/entry-rules.d.ts +2 -0
- package/dist/entry-rules.js +6 -0
- package/dist/migrations/0070_platform_usage_tail.sql +12 -0
- package/dist/modules/assistants.js +32 -1
- package/dist/org.d.ts +9 -2
- package/dist/org.js +14 -1
- package/dist/plan/blue-green-apply.js +8 -0
- package/dist/plan/broker-entries.d.ts +14 -8
- package/dist/plan/broker-entries.js +14 -2
- package/dist/plan/diff.js +28 -9
- package/dist/plan/edge-access.d.ts +9 -0
- package/dist/plan/edge-access.js +20 -0
- package/dist/plan/entry-auth-events.d.ts +53 -0
- package/dist/plan/entry-auth-events.js +112 -0
- package/dist/plan/executor.d.ts +4 -0
- package/dist/plan/executor.js +97 -14
- package/dist/plan/machine-paths.d.ts +1 -1
- package/dist/plan/machine-paths.js +6 -6
- package/dist/plan/own-diff.js +4 -1
- package/dist/plan/plan-apply.d.ts +2 -0
- package/dist/plan/platform-diff.d.ts +62 -0
- package/dist/plan/platform-diff.js +165 -4
- package/dist/plan/platform-providers.d.ts +16 -0
- package/dist/plan/platform-providers.js +86 -4
- package/dist/plan/stage-env.js +1 -0
- package/dist/plan/stage-isolation.js +1 -0
- package/dist/plan/usage-tail-worker.d.ts +29 -0
- package/dist/plan/usage-tail-worker.js +105 -0
- package/dist/plan/worker-app.d.ts +1 -1
- package/dist/plan/worker-app.js +21 -0
- package/dist/plan/workspace-sign-in.d.ts +2 -7
- package/dist/plan/workspace-sign-in.js +5 -1
- package/dist/platform.d.ts +20 -0
- package/dist/platform.js +14 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/plan/stage-env.js
CHANGED
|
@@ -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
|
+
}>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { usageTailArtifactUrl } from '../platform.js';
|
|
3
|
+
import { PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY, PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY, PLATFORM_USAGE_TAIL_KEY, PLATFORM_USAGE_TAIL_KEY_REF, PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY, PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, PLATFORM_USAGE_TAIL_SCRIPT_NAME, } from './platform-diff.js';
|
|
4
|
+
/** The date Cloudflare's Workers runtime compatibility is pinned to; no Workers feature here needs a later one. */
|
|
5
|
+
const USAGE_TAIL_COMPATIBILITY_DATE = '2026-10-01';
|
|
6
|
+
/**
|
|
7
|
+
* foundry#1024: makes the usage tail Worker true. Upload the pinned edge
|
|
8
|
+
* release (the host fetches and verifies it) and record the script at once,
|
|
9
|
+
* so a later failure is a half-made Worker foundry owns. Then hold the key
|
|
10
|
+
* (minted once, its guard recorded as soon as its value is held) and set it
|
|
11
|
+
* on the Worker. The applied values are recorded only when all of it
|
|
12
|
+
* succeeded.
|
|
13
|
+
*/
|
|
14
|
+
export async function provisionPlatformUsageTail(context, deps) {
|
|
15
|
+
const { platform } = context;
|
|
16
|
+
const { usageTail } = platform;
|
|
17
|
+
const recorded = context.recordedIds();
|
|
18
|
+
const edgeUrl = recorded[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
19
|
+
if (!edgeUrl) {
|
|
20
|
+
throw new Error(`platform:usage-tail: edge has not been applied yet (no recorded ${PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY}); the Worker is uploaded through it`);
|
|
21
|
+
}
|
|
22
|
+
const workers = await deps.workers(edgeUrl);
|
|
23
|
+
if (!usageTail.enabled) {
|
|
24
|
+
// Whatever of a (possibly half-made) deploy exists is removed; what does not exist is tolerated.
|
|
25
|
+
await workers.deleteScript(PLATFORM_USAGE_TAIL_SCRIPT_NAME);
|
|
26
|
+
await deps.deliverSecret(PLATFORM_USAGE_TAIL_KEY_REF, '');
|
|
27
|
+
for (const key of [
|
|
28
|
+
PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY,
|
|
29
|
+
PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY,
|
|
30
|
+
PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY,
|
|
31
|
+
PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY,
|
|
32
|
+
]) {
|
|
33
|
+
context.recordScopeId(key, '');
|
|
34
|
+
}
|
|
35
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, '1');
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
if (!usageTail.tag || !usageTail.sha256) {
|
|
39
|
+
throw new Error('platform:usage-tail: no artifact is pinned (usageTail.tag, usageTail.sha256)');
|
|
40
|
+
}
|
|
41
|
+
// ---- the key, once ------------------------------------------------------
|
|
42
|
+
// The hash is never secret, so (the same pattern `provisionPlatformEdge`
|
|
43
|
+
// uses for `EDGE_PLATFORM_KEY_HASH`) it doubles as both the undelivered
|
|
44
|
+
// marker and the final recorded value: a marker with no guard means the
|
|
45
|
+
// mint may never have reached `deliverSecret`, so it is tried again with
|
|
46
|
+
// the same hash rather than minting a second key.
|
|
47
|
+
let keyHash = recorded[PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY] ?? '';
|
|
48
|
+
if (!keyHash) {
|
|
49
|
+
keyHash = recorded[PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY] ?? '';
|
|
50
|
+
let secret = keyHash ? await deps.heldKeys.resolve(PLATFORM_USAGE_TAIL_KEY_REF) : null;
|
|
51
|
+
if (!secret) {
|
|
52
|
+
secret = randomBytes(32).toString('base64url');
|
|
53
|
+
keyHash = createHash('sha256').update(secret).digest('hex');
|
|
54
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_KEY_UNDELIVERED_SCOPE_KEY, keyHash);
|
|
55
|
+
await context.persist?.();
|
|
56
|
+
}
|
|
57
|
+
await deliverKey(deps, secret);
|
|
58
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_KEY_SET_SCOPE_KEY, keyHash);
|
|
59
|
+
await context.persist?.();
|
|
60
|
+
}
|
|
61
|
+
// ---- the script ----------------------------------------------------------
|
|
62
|
+
await workers.uploadScript({
|
|
63
|
+
name: PLATFORM_USAGE_TAIL_SCRIPT_NAME,
|
|
64
|
+
artifactUrl: usageTailArtifactUrl(usageTail.tag),
|
|
65
|
+
artifactSha256: usageTail.sha256,
|
|
66
|
+
compatibilityDate: USAGE_TAIL_COMPATIBILITY_DATE,
|
|
67
|
+
bindings: [{ type: 'plain_text', name: 'EDGE_USAGE_URL', text: edgeUrl }],
|
|
68
|
+
});
|
|
69
|
+
// The script is foundry's from here on. Recorded now, not at the end: after
|
|
70
|
+
// a later failure the next plan must repair this Worker, not take it for a
|
|
71
|
+
// foreign one. "Fully applied" is the applied values recorded at the end.
|
|
72
|
+
context.recordScopeId(`cloudflare:worker:${PLATFORM_USAGE_TAIL_KEY}`, PLATFORM_USAGE_TAIL_SCRIPT_NAME);
|
|
73
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_REMOVED_SCOPE_KEY, '');
|
|
74
|
+
await context.persist?.();
|
|
75
|
+
// The key itself is read back only to hand it to the Worker (rule 6: it
|
|
76
|
+
// never reaches an error, a log, or a plan item); edge never returns it.
|
|
77
|
+
const secretValue = await deps.heldKeys.resolve(PLATFORM_USAGE_TAIL_KEY_REF);
|
|
78
|
+
if (!secretValue) {
|
|
79
|
+
throw new Error(`platform:usage-tail: the held key (${PLATFORM_USAGE_TAIL_KEY_REF}) is gone after it was minted; hold a new one to recover`);
|
|
80
|
+
}
|
|
81
|
+
await workers.setSecrets(PLATFORM_USAGE_TAIL_SCRIPT_NAME, [
|
|
82
|
+
{ name: 'EDGE_USAGE_KEY', value: secretValue, version: keyHash },
|
|
83
|
+
]);
|
|
84
|
+
// Last, after every write the applied values depend on.
|
|
85
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_APPLIED_TAG_SCOPE_KEY, usageTail.tag);
|
|
86
|
+
context.recordScopeId(PLATFORM_USAGE_TAIL_APPLIED_SHA256_SCOPE_KEY, usageTail.sha256);
|
|
87
|
+
return { resourceId: PLATFORM_USAGE_TAIL_SCRIPT_NAME };
|
|
88
|
+
}
|
|
89
|
+
/** Rule 1: delivery is tried up to `deliverAttempts` times; the error names the ref only, never the cause. */
|
|
90
|
+
async function deliverKey(deps, secret) {
|
|
91
|
+
const attempts = deps.deliverAttempts ?? 3;
|
|
92
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
93
|
+
for (let attempt = 1;; attempt += 1) {
|
|
94
|
+
try {
|
|
95
|
+
await deps.deliverSecret(PLATFORM_USAGE_TAIL_KEY_REF, secret);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
if (attempt >= attempts) {
|
|
100
|
+
throw new Error(`platform:usage-tail: the key (${PLATFORM_USAGE_TAIL_KEY_REF}) could not be delivered after ${attempts} tries; it went nowhere, so the next apply mints a new one`);
|
|
101
|
+
}
|
|
102
|
+
await sleep(1000 * attempt);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/plan/worker-app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { APPS_DOMAIN, heldEnvRef } from '../org.js';
|
|
|
4
4
|
import { WORKER_APP_KEYS_SECRET, WORKER_APP_KEYS_URL_VAR, workerAppHasBuild, workerAppHostId, } from '../worker-app-config.js';
|
|
5
5
|
import { WORKER_APP_DNS_RECORD, pendingWorkerAppMigrations, strayContainerImage, withCallTimeout, workerAppRegistryPrefix, workerAppShape, workerAppUpload, } from '../worker-app-upload.js';
|
|
6
6
|
import { entryAuthEnv } from './broker-entries.js';
|
|
7
|
+
import { AUTH_EVENTS_ENV, ensureAuthEventsSecret } from './entry-auth-events.js';
|
|
7
8
|
import { workerAppHostname } from './hostnames.js';
|
|
8
9
|
import { workspaceWorkerParent } from './workspace-worker.js';
|
|
9
10
|
import { appStepKey } from './platform-diff.js';
|
|
@@ -165,6 +166,9 @@ export function wantedWorkerApp(org, entry, recorded, input) {
|
|
|
165
166
|
missing.push(what);
|
|
166
167
|
}
|
|
167
168
|
secrets.push({ name: WORKER_APP_AUTH_KEY_SECRET, source: 'generated' });
|
|
169
|
+
// The signing secret of assistant events: made by rule 1 (`ensureAuthEventsSecret`), shared with the broker entry.
|
|
170
|
+
if (entry.uses?.auth?.mcp?.events)
|
|
171
|
+
secrets.push({ name: AUTH_EVENTS_ENV, source: 'authEvents' });
|
|
168
172
|
if (entry.uses?.auth?.mcp) {
|
|
169
173
|
for (const name of ['MCP_AUDIENCE', 'MCP_RESOURCE', 'MCP_ISSUER']) {
|
|
170
174
|
if (env[name])
|
|
@@ -577,6 +581,23 @@ async function deliverSecrets(deps, org, entryKey, want, context, at, call) {
|
|
|
577
581
|
if (delivered[secret.name] !== version)
|
|
578
582
|
generated.push({ name: secret.name, value, version });
|
|
579
583
|
}
|
|
584
|
+
for (const secret of want.secrets) {
|
|
585
|
+
if (secret.source !== 'authEvents')
|
|
586
|
+
continue;
|
|
587
|
+
const value = await ensureAuthEventsSecret({
|
|
588
|
+
orgId: org.id,
|
|
589
|
+
slug: org.slug,
|
|
590
|
+
entryKey,
|
|
591
|
+
heldKeys,
|
|
592
|
+
recorded: () => context.recordedIds(),
|
|
593
|
+
record: (key, recordedValue) => context.recordScopeId(key, recordedValue),
|
|
594
|
+
persist: () => context.persistScopeIds(),
|
|
595
|
+
secret: (held) => deps.secret?.(held),
|
|
596
|
+
});
|
|
597
|
+
const version = secretVersion(value);
|
|
598
|
+
if (delivered[secret.name] !== version)
|
|
599
|
+
generated.push({ name: secret.name, value, version });
|
|
600
|
+
}
|
|
580
601
|
// Held secrets: the company's value, read from the held-key store. The plan found each one held; a
|
|
581
602
|
// value gone since stops the step, and the error names the ref, never a value.
|
|
582
603
|
for (const secret of want.secrets) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DeclaredOrg, HostingEntry } from '../org.js';
|
|
2
2
|
import type { UnmanagedItem } from '../plan.js';
|
|
3
|
-
import { type BrokerEntryInput, type BrokerEntryWant } from './broker-entries.js';
|
|
3
|
+
import { type BrokerEntryInput, type BrokerEntryMcp, type BrokerEntryWant } from './broker-entries.js';
|
|
4
4
|
import type { DraftStep } from './diff.js';
|
|
5
5
|
import type { LiveResource } from './live.js';
|
|
6
6
|
/** The step `coolify:workspace-broker-entry` and its action key, for the planner and the executor to share. */
|
|
@@ -10,12 +10,7 @@ export declare const WORKSPACE_BROKER_ENTRY_KIND = "workspace-broker-entry";
|
|
|
10
10
|
* project in the test customers organisation (recorded by `zitadel:mcp-project` under the client's name), and the
|
|
11
11
|
* onboarding page on the workspace host. `null` without `mcp` or until the project is recorded.
|
|
12
12
|
*/
|
|
13
|
-
export declare function workspaceMcp(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>):
|
|
14
|
-
resource: string;
|
|
15
|
-
projectId: string;
|
|
16
|
-
loginHost: string;
|
|
17
|
-
onboardingUrl?: string;
|
|
18
|
-
} | null;
|
|
13
|
+
export declare function workspaceMcp(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>): BrokerEntryMcp | null;
|
|
19
14
|
/**
|
|
20
15
|
* What the app's env says about who signs in at the workspace, as recorded so far, or `null` until the
|
|
21
16
|
* organisation, client and issuer are all known. Non-secret values only.
|
|
@@ -37,6 +37,8 @@ export function workspaceMcp(org, entry, recorded) {
|
|
|
37
37
|
projectId,
|
|
38
38
|
loginHost: loginHostnameFor(org),
|
|
39
39
|
...(mcp.onboardingPath ? { onboardingUrl: `https://${host}${mcp.onboardingPath}` } : {}),
|
|
40
|
+
...(mcp.events ? { events: { url: `https://${host}${mcp.events.path}` } } : {}),
|
|
41
|
+
...(mcp.managePath ? { manageUrl: `https://${host}${mcp.managePath}` } : {}),
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
@@ -90,7 +92,9 @@ export function liveClientIds(org, recorded) {
|
|
|
90
92
|
/** What `ws.<key>.signin` records: the sign-in env above as JSON, or `null` while it is not all known. */
|
|
91
93
|
export function workspaceSignInMarker(org, entry, recorded, issuer) {
|
|
92
94
|
const env = workspaceAuthEnv(org, entry, recorded, issuer);
|
|
93
|
-
|
|
95
|
+
if (!env)
|
|
96
|
+
return null;
|
|
97
|
+
return entry.uses?.auth?.mcp?.events ? `${JSON.stringify(env)}+events` : JSON.stringify(env);
|
|
94
98
|
}
|
|
95
99
|
/**
|
|
96
100
|
* The test customers organisation, planned once an entry's workspace signs in customers. Sign-up and the emailed
|
package/dist/platform.d.ts
CHANGED
|
@@ -66,6 +66,24 @@ export interface PlatformUptimeWorker {
|
|
|
66
66
|
}
|
|
67
67
|
/** Where Archon publishes the Worker script for `tag` (a private repo; the host's `fetchArtifact` fetches it). */
|
|
68
68
|
export declare function uptimeArtifactUrl(tag: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* foundry#1024: the usage tail Worker that meters every company Worker's
|
|
71
|
+
* use. `@wtfalch/edge`'s `packages/service/src/usage-tail-worker.ts` has no
|
|
72
|
+
* imports and builds to `usage-tail-worker.js`; foundry deploys the pinned
|
|
73
|
+
* build and never builds it, the same `tag`/`sha256` pin
|
|
74
|
+
* {@link PlatformUptimeWorker} uses. Both empty means nothing is pinned yet,
|
|
75
|
+
* and the planner reports that instead of planning the step. It has no
|
|
76
|
+
* hostname: it answers no request, only the tail events Cloudflare delivers.
|
|
77
|
+
*/
|
|
78
|
+
export interface PlatformUsageTailWorker {
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
/** An edge release tag, `v1.2.3`. */
|
|
81
|
+
tag: string;
|
|
82
|
+
/** Lower-case hex SHA-256 of `usage-tail-worker.js` at that tag. */
|
|
83
|
+
sha256: string;
|
|
84
|
+
}
|
|
85
|
+
/** Where edge publishes the Worker script for `tag` (the host's `fetchArtifact` fetches it). */
|
|
86
|
+
export declare function usageTailArtifactUrl(tag: string): string;
|
|
69
87
|
/**
|
|
70
88
|
* The shared services a declared platform names, decided in #165, plus
|
|
71
89
|
* `edge` (#317, F7): the service that mints an entry's own edge key and
|
|
@@ -137,6 +155,8 @@ export interface DeclaredPlatform {
|
|
|
137
155
|
coolify: PlatformService;
|
|
138
156
|
/** foundry#542: the outside prober Worker; see {@link PlatformUptimeWorker}. */
|
|
139
157
|
uptime: PlatformUptimeWorker;
|
|
158
|
+
/** foundry#1024: the usage tail Worker; see {@link PlatformUsageTailWorker}. */
|
|
159
|
+
usageTail: PlatformUsageTailWorker;
|
|
140
160
|
/**
|
|
141
161
|
* foundry#544: which Healthchecks instance manages the heartbeat checks
|
|
142
162
|
* for every piece of scheduled work foundry sets up (the uptime Worker,
|
package/dist/platform.js
CHANGED
|
@@ -4,6 +4,10 @@ import { validateBlueGreen, validateDomain } from './org.js';
|
|
|
4
4
|
export function uptimeArtifactUrl(tag) {
|
|
5
5
|
return `https://github.com/wtfalch/archon/releases/download/${tag}/archon-uptime-worker.js`;
|
|
6
6
|
}
|
|
7
|
+
/** Where edge publishes the Worker script for `tag` (the host's `fetchArtifact` fetches it). */
|
|
8
|
+
export function usageTailArtifactUrl(tag) {
|
|
9
|
+
return `https://github.com/wtfalch/edge/releases/download/${tag}/usage-tail-worker.js`;
|
|
10
|
+
}
|
|
7
11
|
export const PLATFORM_SERVICE_KEYS = [
|
|
8
12
|
'auth',
|
|
9
13
|
'mail',
|
|
@@ -87,6 +91,16 @@ export function validatePlatform(platform) {
|
|
|
87
91
|
if (uptime.sha256 && !/^[0-9a-f]{64}$/.test(uptime.sha256)) {
|
|
88
92
|
return 'uptime.sha256 must be 64 lower-case hex characters';
|
|
89
93
|
}
|
|
94
|
+
const { usageTail } = platform;
|
|
95
|
+
if (Boolean(usageTail.tag.trim()) !== Boolean(usageTail.sha256.trim())) {
|
|
96
|
+
return 'usageTail.tag and usageTail.sha256 must be set together: a pin is both';
|
|
97
|
+
}
|
|
98
|
+
if (usageTail.tag && !/^v[0-9A-Za-z][0-9A-Za-z.+_-]*$/.test(usageTail.tag)) {
|
|
99
|
+
return `usageTail.tag '${usageTail.tag}' must be a release tag such as v1.2.3`;
|
|
100
|
+
}
|
|
101
|
+
if (usageTail.sha256 && !/^[0-9a-f]{64}$/.test(usageTail.sha256)) {
|
|
102
|
+
return 'usageTail.sha256 must be 64 lower-case hex characters';
|
|
103
|
+
}
|
|
90
104
|
// #491: declared, never discovered -- a present-but-blank id is refused the
|
|
91
105
|
// same way `serverIpv4`'s blank string is, rather than being read as unset.
|
|
92
106
|
if (platform.mail.lettermintProjectId != null && !platform.mail.lettermintProjectId.trim()) {
|
package/dist/store/postgres.js
CHANGED
|
@@ -815,6 +815,11 @@ function rowToPlatform(row) {
|
|
|
815
815
|
gitRef: row.coolifyGitRef,
|
|
816
816
|
},
|
|
817
817
|
uptime: { enabled: row.uptimeEnabled, tag: row.uptimeTag, sha256: row.uptimeSha256 },
|
|
818
|
+
usageTail: {
|
|
819
|
+
enabled: row.usageTailEnabled,
|
|
820
|
+
tag: row.usageTailTag,
|
|
821
|
+
sha256: row.usageTailSha256,
|
|
822
|
+
},
|
|
818
823
|
heartbeatsProvider: row.heartbeatsProvider,
|
|
819
824
|
};
|
|
820
825
|
}
|
|
@@ -867,6 +872,9 @@ function platformToRow(platform) {
|
|
|
867
872
|
uptimeEnabled: platform.uptime.enabled,
|
|
868
873
|
uptimeTag: platform.uptime.tag,
|
|
869
874
|
uptimeSha256: platform.uptime.sha256,
|
|
875
|
+
usageTailEnabled: platform.usageTail.enabled,
|
|
876
|
+
usageTailTag: platform.usageTail.tag,
|
|
877
|
+
usageTailSha256: platform.usageTail.sha256,
|
|
870
878
|
heartbeatsProvider: platform.heartbeatsProvider,
|
|
871
879
|
};
|
|
872
880
|
}
|