@wtfalch/foundry 0.20.0 → 0.21.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.
@@ -117,7 +117,7 @@ export declare const JOBS_ADMIN_TOKEN_REF = "jobs:scheduler:adminToken";
117
117
  * org's own app, reused here for the platform's one scheduler instead of once
118
118
  * per org.
119
119
  */
120
- type JobsCoolifyPort = Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'getApplication' | 'deploy'>;
120
+ type JobsCoolifyPort = Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'getApplication' | 'deploy'>;
121
121
  /**
122
122
  * Options for {@link FoundryPlatformStepExecutor}'s constructor, replacing
123
123
  * four positional arguments that shifted on every merge that added a
@@ -182,6 +182,18 @@ export interface FoundryPlatformStepExecutorOptions {
182
182
  * planned.
183
183
  */
184
184
  backup?: PlatformBackupDeps;
185
+ /**
186
+ * #540: dependencies for `coolify:app` keyed {@link PLATFORM_HC_KEY} --
187
+ * optional for the same reason as `auth`; a host that declares
188
+ * `platform.hc.enabled` without it gets the step reported as blocked.
189
+ */
190
+ hc?: PlatformHcDeps;
191
+ /**
192
+ * #540: dependencies for the `heartbeats:reconcile` step -- optional for
193
+ * the same reason as `auth`; `hc` applied with this unset gets the
194
+ * heartbeat checks reported as blocked rather than planned.
195
+ */
196
+ heartbeats?: PlatformHeartbeatsDeps;
185
197
  }
186
198
  /**
187
199
  * The real {@link PlatformStepExecutor} for the two steps built so far:
@@ -261,6 +273,8 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
261
273
  private readonly edge?;
262
274
  private readonly archon?;
263
275
  private readonly backup?;
276
+ private readonly hc?;
277
+ private readonly heartbeats?;
264
278
  constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list' | 'get'>, options?: FoundryPlatformStepExecutorOptions);
265
279
  blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
266
280
  auth?: string;
@@ -273,6 +287,9 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
273
287
  archon?: string;
274
288
  mail?: string;
275
289
  backupExecutorMissing?: boolean;
290
+ hc?: string;
291
+ heartbeatsExecutorMissing?: boolean;
292
+ heartbeatsApiKeyMissing?: boolean;
276
293
  }>;
277
294
  /**
278
295
  * Whether Archon's live Access policy lets through anyone but the declared
@@ -676,6 +693,17 @@ export interface PlatformAiRuntime {
676
693
  migrateIntervalMs?: number;
677
694
  /** Tries at handing a secret to the host (default 3): the management key and the signing key both use this. */
678
695
  deliverAttempts?: number;
696
+ /**
697
+ * #543: injected for tests, against a fake runner host -- the runner's own
698
+ * deploy wait now does a `'ready'` probe of its `/api/health`, the same
699
+ * `DeployHealthRuntime.fetch` field `provisionPlatformMail`/
700
+ * `provisionPlatformArchon` already take. Defaults to the global `fetch`.
701
+ */
702
+ fetch?: typeof fetch;
703
+ /** How long to wait for the runner to answer ready (default 5 minutes). */
704
+ readyTimeoutMs?: number;
705
+ /** Pause between readiness polls (default 5 seconds). */
706
+ readyIntervalMs?: number;
679
707
  }
680
708
  /** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
681
709
  export declare const PLATFORM_AI_MANAGEMENT_KEY_REF = "ai:platform:managementKey";
@@ -849,6 +877,17 @@ export interface PlatformFilesRuntime {
849
877
  migrateIntervalMs?: number;
850
878
  /** Tries at handing each secret to the host (default 3). */
851
879
  deliverAttempts?: number;
880
+ /**
881
+ * #543: injected for tests, against a fake runner host -- the runner's own
882
+ * deploy wait now does a `'ready'` probe of its `/api/health`, same as
883
+ * `provisionPlatformMail`/`provisionPlatformArchon` already take. Defaults
884
+ * to the global `fetch`.
885
+ */
886
+ fetch?: typeof fetch;
887
+ /** How long to wait for the runner to answer ready (default 5 minutes). */
888
+ readyTimeoutMs?: number;
889
+ /** Pause between readiness polls (default 5 seconds). */
890
+ readyIntervalMs?: number;
852
891
  }
853
892
  /**
854
893
  * #171: how the host signs a files management request with the key this step
@@ -903,6 +942,30 @@ export interface PlatformEdgeDeps {
903
942
  /** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
904
943
  runtime?: PlatformEdgeRuntime;
905
944
  }
945
+ export interface PlatformHcRuntime {
946
+ fetch?: typeof fetch;
947
+ sleep?: (ms: number) => Promise<void>;
948
+ now?: () => number;
949
+ readyTimeoutMs?: number;
950
+ readyIntervalMs?: number;
951
+ }
952
+ /**
953
+ * #540: dependencies for `coolify:app` keyed {@link PLATFORM_HC_KEY} -- the
954
+ * {@link PlatformEdgeDeps} bundling shape minus a Cloudflare token of its
955
+ * own: `hc` writes its tunnel and DNS record through edge the same way
956
+ * mail and archon do, and needs no `heldKeys`/`deliverSecret` at all --
957
+ * `SECRET_KEY` (Django's session-signing key) is generated once by
958
+ * {@link provisionPlatformHc} itself and never leaves this Coolify app.
959
+ */
960
+ export interface PlatformHcDeps {
961
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
962
+ /** F4.dns: every DNS record call, through edge at its recorded address (see {@link PlatformDnsFactory}). */
963
+ dns: PlatformDnsFactory;
964
+ /** F4.tunnels: every tunnel call, through edge at its recorded address (see {@link PlatformTunnelsFactory}). */
965
+ tunnels: PlatformTunnelsFactory;
966
+ /** Clock, sleep and fetch for the readiness wait; real ones by default. */
967
+ runtime?: PlatformHcRuntime;
968
+ }
906
969
  /**
907
970
  * #174: everything {@link provisionPlatformIntegrations} needs beyond
908
971
  * {@link PlatformStepExecutionContext} -- a bundle, like
@@ -977,6 +1040,91 @@ export interface PlatformBackupDeps {
977
1040
  /** A cron string or one of Coolify's names (`daily`, `weekly`, ...). Default `'daily'`. */
978
1041
  frequency?: string;
979
1042
  }
1043
+ /** One check as self-hosted Healthchecks' own Management API reports it. */
1044
+ export interface HeartbeatCheck {
1045
+ id: string;
1046
+ name: string;
1047
+ pingUrl: string;
1048
+ }
1049
+ /**
1050
+ * #540: the heartbeat server's own Management API -- a project API key in a
1051
+ * header, `POST /api/v3/checks/` with `name`/`unique: ['name']` to upsert a
1052
+ * check idempotently by name, `GET /api/v3/checks/` to list every one,
1053
+ * `DELETE /api/v3/checks/<uuid>` to remove one -- restated here as the port
1054
+ * `provisionPlatformHeartbeats` plans and provisions against, never a real
1055
+ * HTTP client: this package never connects itself (`README.md`), so the
1056
+ * host implements this against its own running `hc` instance and its own
1057
+ * project API key, the same seam every other real I/O in this file crosses.
1058
+ */
1059
+ export interface HeartbeatsPort {
1060
+ listChecks(): Promise<HeartbeatCheck[]>;
1061
+ /** Creates a check named `name`, or returns the existing one of that name -- the Management API's own upsert. */
1062
+ createCheck(name: string): Promise<HeartbeatCheck>;
1063
+ /**
1064
+ * #540 round 2: pauses a check (`POST /api/v3/checks/<uuid>/pause/`) so it
1065
+ * reads "paused" rather than "down" while nothing actually pings it yet --
1066
+ * both the uptime Worker's own check and every backup check (see this
1067
+ * file's own doc comment on `PlatformHeartbeatsDeps` for why). Sending a
1068
+ * check a ping resumes it automatically (Healthchecks' own documented
1069
+ * behaviour), so foundry never has to call the reverse of this: once a
1070
+ * real pinger exists, the check un-pauses itself on its first ping.
1071
+ * `provisionPlatformHeartbeats` calls this exactly once, right after
1072
+ * `createCheck`, and never again on a check it merely finds already live
1073
+ * -- a check already being pinged, or one an operator unpaused by hand,
1074
+ * must never be paused again by a reconcile run.
1075
+ */
1076
+ pauseCheck(id: string): Promise<void>;
1077
+ deleteCheck(id: string): Promise<void>;
1078
+ }
1079
+ /**
1080
+ * #540: dependencies for the `heartbeats:reconcile` step
1081
+ * (`platform-diff.ts`'s `planHeartbeats`).
1082
+ *
1083
+ * #540 round 2: `resolve` is new. It guards only the *delete* half of
1084
+ * {@link provisionPlatformHeartbeats} -- a live check is only ever deleted
1085
+ * when foundry itself holds a ping URL for its exact name
1086
+ * (`heartbeatCheckUrlRef`, `platform-diff.ts`), proof this run or an
1087
+ * earlier one created it. Without that guard, a hand-made check sharing
1088
+ * foundry's naming scheme (an operator's own `uptime-worker`, say, made
1089
+ * before foundry ever ran) could be deleted the moment that name was not
1090
+ * yet desired -- a backup service with no recorded database yet is in
1091
+ * `provisionPlatformHeartbeats`'s own `ownedNames` but not `desiredSet`.
1092
+ * `resolve` is never consulted on the *create* half: a check already live
1093
+ * under a desired name is always treated as foundry's, the way it always
1094
+ * was, and (the round-2 fix for a second bug) its ping address is now
1095
+ * always re-saved rather than only on the create that first makes it live
1096
+ * -- see `put`'s own doc comment below for why that half carries no
1097
+ * adoption risk the way deleting does.
1098
+ *
1099
+ * `put` records a check's own ping URL -- on every create, and (new this
1100
+ * round) on every run that finds a desired check already live, not only
1101
+ * the run that created it. Before this fix, a save that failed once, or a
1102
+ * run that died between create and save, or a host that wired `heldKeys`
1103
+ * only after the original create, lost that address for good: the next run
1104
+ * found the check already live and never saved anything, despite this very
1105
+ * doc comment already claiming the step "can simply ask the live check
1106
+ * again." Required whenever an address actually needs saving -- optional
1107
+ * only so a host without one still compiles, in which case nothing here
1108
+ * can ever be created or re-confirmed, and the step throws rather than
1109
+ * silently drop an address: CLAUDE.md platform-step rule 1 is written for a
1110
+ * minted credential, not a ping URL (a lost one is simply re-asked from the
1111
+ * live check, never re-minted), but the rule's "never silently lose it"
1112
+ * half applies just as well here.
1113
+ *
1114
+ * Both fields are optional only so a host without either still compiles;
1115
+ * while `PlatformHeartbeatsDeps` is wired at all, `FoundryPlatformStepExecutor
1116
+ * .blockedSteps`'s own `heartbeatsApiKeyMissing` check blocks the step
1117
+ * until a project API key is held under {@link PLATFORM_HC_API_KEY_REF}, so
1118
+ * the live Healthchecks calls below are never reached with nothing behind
1119
+ * them.
1120
+ */
1121
+ export interface PlatformHeartbeatsDeps {
1122
+ heartbeats: HeartbeatsPort;
1123
+ heldKeys?: {
1124
+ resolve(ref: string): Promise<string | null>;
1125
+ put?(ref: string, value: string): Promise<void>;
1126
+ };
1127
+ }
980
1128
  /**
981
1129
  * The real {@link PlatformCloudflareReader}: the platform's apex zone as
982
1130
  * `planApexZone` expects to see it, and nothing else. The zone's DNS records
@@ -1006,7 +1154,7 @@ export declare class ProvidersPlatformCloudflareReader implements PlatformCloudf
1006
1154
  export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyReader {
1007
1155
  private readonly client;
1008
1156
  private readonly platformStore;
1009
- constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
1157
+ constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups' | 'listEnvs' | 'raw'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
1010
1158
  listResources(): Promise<LiveResource[]>;
1011
1159
  }
1012
1160
  /** Scope id (no `:`): the share domains the runner's Coolify `domains` was last set to, comma-joined. */