@wtfalch/foundry 0.19.1 → 0.21.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/bootstrap/declare.d.ts +13 -0
- package/dist/bootstrap/declare.js +31 -7
- package/dist/bootstrap/versions.d.ts +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- package/dist/migrations/0046_platform_hc.sql +19 -0
- package/dist/plan/cloudflared-connector.d.ts +3 -2
- package/dist/plan/cloudflared-connector.js +2 -2
- package/dist/plan/deploy-health.js +5 -1
- package/dist/plan/diff.js +27 -4
- package/dist/plan/edge-access.d.ts +68 -0
- package/dist/plan/edge-access.js +123 -0
- package/dist/plan/edge-dns.d.ts +89 -0
- package/dist/plan/edge-dns.js +227 -0
- package/dist/plan/edge-ratelimits.d.ts +54 -0
- package/dist/plan/edge-ratelimits.js +79 -0
- package/dist/plan/edge-tunnels.d.ts +44 -0
- package/dist/plan/edge-tunnels.js +108 -0
- package/dist/plan/edge-workers.d.ts +31 -0
- package/dist/plan/edge-workers.js +165 -0
- package/dist/plan/edge-zones.d.ts +37 -0
- package/dist/plan/edge-zones.js +64 -0
- package/dist/plan/entry-edge-key.d.ts +10 -0
- package/dist/plan/entry-edge-key.js +38 -8
- package/dist/plan/executor.d.ts +146 -143
- package/dist/plan/executor.js +370 -115
- package/dist/plan/health-check-settings.d.ts +47 -0
- package/dist/plan/health-check-settings.js +47 -0
- package/dist/plan/health-targets.d.ts +86 -0
- package/dist/plan/health-targets.js +344 -0
- package/dist/plan/index.d.ts +17 -3
- package/dist/plan/index.js +9 -2
- package/dist/plan/live.d.ts +12 -1
- package/dist/plan/plan-apply.d.ts +4 -0
- package/dist/plan/plan-apply.js +5 -1
- package/dist/plan/platform-diff.d.ts +127 -0
- package/dist/plan/platform-diff.js +304 -8
- package/dist/plan/platform-providers.d.ts +202 -7
- package/dist/plan/platform-providers.js +545 -79
- package/dist/plan/providers-readers.d.ts +23 -1
- package/dist/plan/providers-readers.js +61 -1
- package/dist/plan/rollout.d.ts +9 -8
- package/dist/platform.d.ts +14 -1
- package/dist/platform.js +1 -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 +2 -1
- package/versions.json +2 -1
|
@@ -3,7 +3,7 @@ import type { CoolifyClient } from '@wtfalch/providers/coolify';
|
|
|
3
3
|
import { type ZitadelClient } from '@wtfalch/providers/zitadel';
|
|
4
4
|
import type { OrgId } from '../org.js';
|
|
5
5
|
import type { DeclaredOrgStore } from '../store.js';
|
|
6
|
-
import type { CloudflareReader, CmsReader, CoolifyReader, LiveResource, ZitadelReader } from './live.js';
|
|
6
|
+
import type { AiReader, CloudflareReader, CmsReader, CoolifyReader, LiveResource, ZitadelReader } from './live.js';
|
|
7
7
|
/**
|
|
8
8
|
* Real `CoolifyReader`, `CloudflareReader` and `ZitadelReader` implementations
|
|
9
9
|
* (`live.ts:31-45`), backed by `@wtfalch/providers`' HTTP clients instead of
|
|
@@ -144,6 +144,28 @@ export declare class ProvidersZitadelReader implements ZitadelReader {
|
|
|
144
144
|
*/
|
|
145
145
|
private liveIssuer;
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Reads the org's applied AI budget limit back, so `diff.ts`'s `planAiBudget`
|
|
149
|
+
* stops re-proposing "set the AI budget" once it has already landed (#515).
|
|
150
|
+
* AI's own API offers no read for this (`live.ts`'s `AiReader` doc comment:
|
|
151
|
+
* wiring a real budget read is #20, a non-goal of #27 -- unlike
|
|
152
|
+
* {@link ProvidersZitadelReader}/{@link ProvidersCloudflareReader}, which both
|
|
153
|
+
* call a real provider client), so `executor.ts`'s `aiBudget` recording the
|
|
154
|
+
* limit it just applied under the flat `ai_budget_limit_micros` scope key is
|
|
155
|
+
* the only evidence there is -- the same "nothing live yet" shape
|
|
156
|
+
* {@link storeScopeResolver}'s own doc comment describes for `cloudflare_zone`
|
|
157
|
+
* before #510 fixed it.
|
|
158
|
+
*
|
|
159
|
+
* Nothing recorded yet reads as `[]`, never a default: a never-applied org
|
|
160
|
+
* must still plan its first `ai:budget` step, and inventing the declared (or
|
|
161
|
+
* default) limit here would make a budget that actually drifted out of band
|
|
162
|
+
* read as converged.
|
|
163
|
+
*/
|
|
164
|
+
export declare class ProvidersAiReader implements AiReader {
|
|
165
|
+
private readonly resolveLimit;
|
|
166
|
+
constructor(resolveLimit: ScopeResolver);
|
|
167
|
+
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
168
|
+
}
|
|
147
169
|
/**
|
|
148
170
|
* Backed by the same `@wtfalch/providers/coolify`'s `CoolifyClient.listResources`
|
|
149
171
|
* call {@link ProvidersCoolifyReader} makes, not a real cms admin API --
|
|
@@ -134,6 +134,36 @@ async function withLogVolumeConfig(client, resource, recorded) {
|
|
|
134
134
|
return resource;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* #543: whether a recorded app's own Coolify health check is on, as
|
|
139
|
+
* `config.healthCheckEnabled`, so `diff.ts`'s `planApp` can notice a human
|
|
140
|
+
* turning it back off in the Coolify UI and plan the repair -- the same
|
|
141
|
+
* drift `platform-providers.ts`'s own `archonRollingUpdateDrifted` catches
|
|
142
|
+
* for Archon, now for a company's own hosting-entry app. Gated and failing
|
|
143
|
+
* exactly like {@link withLogVolumeConfig}: a failed read, or one that has
|
|
144
|
+
* not run at all (nothing recorded yet), leaves `healthCheckEnabled` absent,
|
|
145
|
+
* which `planApp` reads as compliant rather than drift.
|
|
146
|
+
*/
|
|
147
|
+
async function withHealthCheckConfig(client, resource, recorded) {
|
|
148
|
+
if (resource.kind !== 'application' || !recorded[resource.key]?.startsWith('app:')) {
|
|
149
|
+
return resource;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const app = await client.getApplication(resource.key);
|
|
153
|
+
// `@wtfalch/providers`' `CoolifyApplication` type does not declare
|
|
154
|
+
// `health_check_enabled` (as of 0.7.3), the same gap `withAgoraEnvConfig`
|
|
155
|
+
// above notes for `ports_exposes` -- `getApplication` returns the raw
|
|
156
|
+
// Coolify API response regardless.
|
|
157
|
+
const healthCheckEnabled = app.health_check_enabled;
|
|
158
|
+
return {
|
|
159
|
+
...resource,
|
|
160
|
+
config: { ...resource.config, healthCheckEnabled },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return resource;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
137
167
|
/**
|
|
138
168
|
* #239: whether a recorded database has an internal connection URL, as
|
|
139
169
|
* `config.hasInternalUrl` (a boolean, never the URL), so `planApp` can report
|
|
@@ -256,7 +286,8 @@ export class ProvidersCoolifyReader {
|
|
|
256
286
|
this.resolveRecordedIds(orgId),
|
|
257
287
|
]);
|
|
258
288
|
const withLogConfig = await Promise.all(toFoundryResources('coolify', resources).map((r) => withLogVolumeConfig(this.client, r, recorded)));
|
|
259
|
-
const
|
|
289
|
+
const withHealthCheck = await Promise.all(withLogConfig.map((r) => withHealthCheckConfig(this.client, r, recorded)));
|
|
290
|
+
const withEnvNames = await Promise.all(withHealthCheck.map((r) => withEnvNamesConfig(this.client, r, recorded)));
|
|
260
291
|
const withDbUrl = await Promise.all(withEnvNames.map((r) => withDatabaseUrlConfig(this.client, r, recorded)));
|
|
261
292
|
const withAgoraConfig = await Promise.all(withDbUrl.map((r) => withAgoraEnvConfig(this.client, r, recorded)));
|
|
262
293
|
const withBouleConfig = await Promise.all(withAgoraConfig.map((r) => withBouleEnvConfig(this.client, r, recorded)));
|
|
@@ -358,6 +389,35 @@ export class ProvidersZitadelReader {
|
|
|
358
389
|
return { system: 'zitadel', kind: 'issuer', key: orgId, config: {} };
|
|
359
390
|
}
|
|
360
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Reads the org's applied AI budget limit back, so `diff.ts`'s `planAiBudget`
|
|
394
|
+
* stops re-proposing "set the AI budget" once it has already landed (#515).
|
|
395
|
+
* AI's own API offers no read for this (`live.ts`'s `AiReader` doc comment:
|
|
396
|
+
* wiring a real budget read is #20, a non-goal of #27 -- unlike
|
|
397
|
+
* {@link ProvidersZitadelReader}/{@link ProvidersCloudflareReader}, which both
|
|
398
|
+
* call a real provider client), so `executor.ts`'s `aiBudget` recording the
|
|
399
|
+
* limit it just applied under the flat `ai_budget_limit_micros` scope key is
|
|
400
|
+
* the only evidence there is -- the same "nothing live yet" shape
|
|
401
|
+
* {@link storeScopeResolver}'s own doc comment describes for `cloudflare_zone`
|
|
402
|
+
* before #510 fixed it.
|
|
403
|
+
*
|
|
404
|
+
* Nothing recorded yet reads as `[]`, never a default: a never-applied org
|
|
405
|
+
* must still plan its first `ai:budget` step, and inventing the declared (or
|
|
406
|
+
* default) limit here would make a budget that actually drifted out of band
|
|
407
|
+
* read as converged.
|
|
408
|
+
*/
|
|
409
|
+
export class ProvidersAiReader {
|
|
410
|
+
resolveLimit;
|
|
411
|
+
constructor(resolveLimit) {
|
|
412
|
+
this.resolveLimit = resolveLimit;
|
|
413
|
+
}
|
|
414
|
+
async listResources(orgId) {
|
|
415
|
+
const limitMicros = await this.resolveLimit(orgId);
|
|
416
|
+
if (!limitMicros)
|
|
417
|
+
return [];
|
|
418
|
+
return [{ system: 'ai', kind: 'budget', key: orgId, config: { limitMicros } }];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
361
421
|
/**
|
|
362
422
|
* Whether an application counts as up, or on its way there, from its own
|
|
363
423
|
* Coolify `status` (`` `running:healthy`, `running:unknown`,
|
package/dist/plan/rollout.d.ts
CHANGED
|
@@ -10,14 +10,15 @@ import type { OrgId } from '../org.js';
|
|
|
10
10
|
* file knows nothing about that shape, only whether it threw.
|
|
11
11
|
*
|
|
12
12
|
* "Stop at the first failed health check" (the issue's own words) is, today,
|
|
13
|
-
* "stop at the first `apply` that throws" --
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* "stop at the first `apply` that throws" -- which is now also true of a
|
|
14
|
+
* failed health check specifically: #543 wired the shared health helper
|
|
15
|
+
* (`deploy-health.ts`'s `waitForNewDeployment`) into the org-level calls
|
|
16
|
+
* stack deploy too (`executor.ts`'s `coolifyCalls`/`coolifyCallsService`),
|
|
17
|
+
* alongside every other company-app step. Neither still patches a running
|
|
18
|
+
* app's image tag (see `org.ts`'s `CallsConfig.versionPin` doc comment,
|
|
19
|
+
* unrelated to #543). That step throwing on a failed health check is enough
|
|
20
|
+
* on its own: a caller's `apply` naturally does too, and this orchestration
|
|
21
|
+
* needs no change.
|
|
21
22
|
*/
|
|
22
23
|
/** One org's outcome from {@link applyOrgsInOrder}. */
|
|
23
24
|
export interface RolloutOrgResult {
|
package/dist/platform.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface PlatformMailService extends PlatformService {
|
|
|
30
30
|
* holds the Cloudflare token `<username>.<org domain>` claims use, so no
|
|
31
31
|
* hosting entry ever holds that credential itself.
|
|
32
32
|
*/
|
|
33
|
-
export type PlatformServiceKey = 'auth' | 'mail' | 'sfu' | 'files' | 'ai' | 'jobs' | 'integrations' | 'archon' | 'edge';
|
|
33
|
+
export type PlatformServiceKey = 'auth' | 'mail' | 'sfu' | 'files' | 'ai' | 'jobs' | 'integrations' | 'archon' | 'edge' | 'hc';
|
|
34
34
|
export declare const PLATFORM_SERVICE_KEYS: readonly PlatformServiceKey[];
|
|
35
35
|
/**
|
|
36
36
|
* The one shared platform every org's services run on -- decided in #165 as
|
|
@@ -68,6 +68,19 @@ export interface DeclaredPlatform {
|
|
|
68
68
|
archon: PlatformService;
|
|
69
69
|
/** #317 (F7). No deploy step reads this service's own `gitRef` yet -- `N1` is deployed by hand until it exists. */
|
|
70
70
|
edge: PlatformService;
|
|
71
|
+
/**
|
|
72
|
+
* #540: a self-hosted heartbeat server (Healthchecks) for the platform,
|
|
73
|
+
* for now -- whether it stays the platform's heartbeat server long-term,
|
|
74
|
+
* instead of or alongside the hand-set-up `hc.wtfalch.dev` instance, and
|
|
75
|
+
* what happens to that instance's own data, are both still open "Decide"
|
|
76
|
+
* items on issue #540's own thread, not settled by this field. `gitRef`
|
|
77
|
+
* is the Docker image tag it runs (the same overload `mail.gitRef`
|
|
78
|
+
* already carries for Stalwart's tag), not a git ref. Every piece of
|
|
79
|
+
* scheduled work foundry sets up (the uptime Worker, each platform
|
|
80
|
+
* database backup) gets one check here, named after it; see
|
|
81
|
+
* `plan/platform-diff.ts`'s `planHeartbeats`.
|
|
82
|
+
*/
|
|
83
|
+
hc: PlatformService;
|
|
71
84
|
}
|
|
72
85
|
/** `<service>.<apex>` -- the default `hostname` for a platform service, before an operator overrides it. */
|
|
73
86
|
export declare function defaultPlatformHostname(service: PlatformServiceKey, apex: string): string;
|
package/dist/platform.js
CHANGED
|
@@ -10,6 +10,7 @@ export const PLATFORM_SERVICE_KEYS = [
|
|
|
10
10
|
'integrations',
|
|
11
11
|
'archon',
|
|
12
12
|
'edge',
|
|
13
|
+
'hc',
|
|
13
14
|
];
|
|
14
15
|
/** `<service>.<apex>` -- the default `hostname` for a platform service, before an operator overrides it. */
|
|
15
16
|
export function defaultPlatformHostname(service, apex) {
|
package/dist/store/postgres.js
CHANGED
|
@@ -603,6 +603,11 @@ function rowToPlatform(row) {
|
|
|
603
603
|
hostname: row.edgeHostname,
|
|
604
604
|
gitRef: row.edgeGitRef,
|
|
605
605
|
},
|
|
606
|
+
hc: {
|
|
607
|
+
enabled: row.hcEnabled,
|
|
608
|
+
hostname: row.hcHostname,
|
|
609
|
+
gitRef: row.hcGitRef,
|
|
610
|
+
},
|
|
606
611
|
};
|
|
607
612
|
}
|
|
608
613
|
function platformToRow(platform) {
|
|
@@ -639,6 +644,9 @@ function platformToRow(platform) {
|
|
|
639
644
|
edgeEnabled: platform.edge.enabled,
|
|
640
645
|
edgeHostname: platform.edge.hostname,
|
|
641
646
|
edgeGitRef: platform.edge.gitRef,
|
|
647
|
+
hcEnabled: platform.hc.enabled,
|
|
648
|
+
hcHostname: platform.hc.hostname,
|
|
649
|
+
hcGitRef: platform.hc.gitRef,
|
|
642
650
|
};
|
|
643
651
|
}
|
|
644
652
|
/**
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -1932,6 +1932,57 @@ export declare const foundryDeclaredPlatform: import("drizzle-orm/pg-core").PgTa
|
|
|
1932
1932
|
identity: undefined;
|
|
1933
1933
|
generated: undefined;
|
|
1934
1934
|
}, {}, {}>;
|
|
1935
|
+
hcEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
1936
|
+
name: "hc_enabled";
|
|
1937
|
+
tableName: "foundry_declared_platform";
|
|
1938
|
+
dataType: "boolean";
|
|
1939
|
+
columnType: "PgBoolean";
|
|
1940
|
+
data: boolean;
|
|
1941
|
+
driverParam: boolean;
|
|
1942
|
+
notNull: true;
|
|
1943
|
+
hasDefault: true;
|
|
1944
|
+
isPrimaryKey: false;
|
|
1945
|
+
isAutoincrement: false;
|
|
1946
|
+
hasRuntimeDefault: false;
|
|
1947
|
+
enumValues: undefined;
|
|
1948
|
+
baseColumn: never;
|
|
1949
|
+
identity: undefined;
|
|
1950
|
+
generated: undefined;
|
|
1951
|
+
}, {}, {}>;
|
|
1952
|
+
hcHostname: import("drizzle-orm/pg-core").PgColumn<{
|
|
1953
|
+
name: "hc_hostname";
|
|
1954
|
+
tableName: "foundry_declared_platform";
|
|
1955
|
+
dataType: "string";
|
|
1956
|
+
columnType: "PgText";
|
|
1957
|
+
data: string;
|
|
1958
|
+
driverParam: string;
|
|
1959
|
+
notNull: true;
|
|
1960
|
+
hasDefault: false;
|
|
1961
|
+
isPrimaryKey: false;
|
|
1962
|
+
isAutoincrement: false;
|
|
1963
|
+
hasRuntimeDefault: false;
|
|
1964
|
+
enumValues: [string, ...string[]];
|
|
1965
|
+
baseColumn: never;
|
|
1966
|
+
identity: undefined;
|
|
1967
|
+
generated: undefined;
|
|
1968
|
+
}, {}, {}>;
|
|
1969
|
+
hcGitRef: import("drizzle-orm/pg-core").PgColumn<{
|
|
1970
|
+
name: "hc_git_ref";
|
|
1971
|
+
tableName: "foundry_declared_platform";
|
|
1972
|
+
dataType: "string";
|
|
1973
|
+
columnType: "PgText";
|
|
1974
|
+
data: string;
|
|
1975
|
+
driverParam: string;
|
|
1976
|
+
notNull: true;
|
|
1977
|
+
hasDefault: true;
|
|
1978
|
+
isPrimaryKey: false;
|
|
1979
|
+
isAutoincrement: false;
|
|
1980
|
+
hasRuntimeDefault: false;
|
|
1981
|
+
enumValues: [string, ...string[]];
|
|
1982
|
+
baseColumn: never;
|
|
1983
|
+
identity: undefined;
|
|
1984
|
+
generated: undefined;
|
|
1985
|
+
}, {}, {}>;
|
|
1935
1986
|
serviceIds: import("drizzle-orm/pg-core").PgColumn<{
|
|
1936
1987
|
name: "service_ids";
|
|
1937
1988
|
tableName: "foundry_declared_platform";
|
package/dist/store/schema.js
CHANGED
|
@@ -182,6 +182,10 @@ export const foundryDeclaredPlatform = pgTable('foundry_declared_platform', {
|
|
|
182
182
|
edgeEnabled: boolean('edge_enabled').notNull().default(false),
|
|
183
183
|
edgeHostname: text('edge_hostname').notNull(),
|
|
184
184
|
edgeGitRef: text('edge_git_ref').notNull().default(''),
|
|
185
|
+
/** #540, `migrations/0046_platform_hc.sql`: the platform's own heartbeat server (self-hosted Healthchecks). */
|
|
186
|
+
hcEnabled: boolean('hc_enabled').notNull().default(false),
|
|
187
|
+
hcHostname: text('hc_hostname').notNull(),
|
|
188
|
+
hcGitRef: text('hc_git_ref').notNull().default(''),
|
|
185
189
|
/** `migrations/0018_platform_apply.sql`: the platform's applied state, `system:kind:key` to provider id. */
|
|
186
190
|
serviceIds: jsonb('service_ids').$type().notNull().default({}),
|
|
187
191
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@wtfalch/audit": "0.6.0",
|
|
44
44
|
"@wtfalch/auth": "0.16.0",
|
|
45
|
+
"@wtfalch/edge": "0.9.0",
|
|
45
46
|
"@wtfalch/email": "^0.17.0",
|
|
46
47
|
"@wtfalch/providers": "0.7.3",
|
|
47
48
|
"ssh2": "1.17.0",
|