@wtfalch/foundry 0.3.0 → 0.5.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/bootstrap.d.ts +96 -0
- package/dist/bootstrap/bootstrap.js +285 -0
- package/dist/bootstrap/declare.d.ts +36 -0
- package/dist/bootstrap/declare.js +94 -0
- package/dist/bootstrap/drive.d.ts +40 -0
- package/dist/bootstrap/drive.js +51 -0
- package/dist/bootstrap/index.d.ts +3 -0
- package/dist/bootstrap/index.js +1 -0
- package/dist/calls-sfu-deploy-support.d.ts +9 -6
- package/dist/calls-sfu-deploy-support.js +9 -6
- package/dist/calls-sfu-keys.d.ts +21 -0
- package/dist/calls-sfu-keys.js +27 -0
- package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
- package/dist/calls-sfu-ssh-adapter.js +78 -0
- package/dist/calls-sfu-ssh-client.d.ts +49 -0
- package/dist/calls-sfu-ssh-client.js +82 -0
- package/dist/calls-sfu.d.ts +22 -4
- package/dist/calls-sfu.js +22 -4
- package/dist/index.d.ts +31 -6
- package/dist/index.js +16 -3
- package/dist/migrations/0014_calls_enabled.sql +7 -0
- package/dist/migrations/0015_domain_unique.sql +39 -0
- package/dist/migrations/0016_platform.sql +61 -0
- package/dist/migrations/0017_requested_changes.sql +42 -0
- package/dist/migrations/0018_platform_apply.sql +19 -0
- package/dist/org.d.ts +18 -2
- package/dist/org.js +56 -0
- package/dist/plan/agora.d.ts +77 -0
- package/dist/plan/agora.js +127 -0
- package/dist/plan/calls-matrix.d.ts +135 -0
- package/dist/plan/calls-matrix.js +275 -0
- package/dist/plan/cloudflared-connector.d.ts +70 -0
- package/dist/plan/cloudflared-connector.js +106 -0
- package/dist/plan/diff.d.ts +26 -5
- package/dist/plan/diff.js +720 -106
- package/dist/plan/executor.d.ts +731 -36
- package/dist/plan/executor.js +1180 -41
- package/dist/plan/index.d.ts +22 -5
- package/dist/plan/index.js +12 -2
- package/dist/plan/jobs-scheduler-http.d.ts +29 -0
- package/dist/plan/jobs-scheduler-http.js +50 -0
- package/dist/plan/live.d.ts +51 -2
- package/dist/plan/live.js +8 -0
- package/dist/plan/plan-apply.d.ts +192 -3
- package/dist/plan/plan-apply.js +290 -14
- package/dist/plan/platform-diff.d.ts +312 -0
- package/dist/plan/platform-diff.js +867 -0
- package/dist/plan/platform-org-ports.d.ts +50 -0
- package/dist/plan/platform-org-ports.js +70 -0
- package/dist/plan/platform-org.d.ts +120 -0
- package/dist/plan/platform-org.js +115 -0
- package/dist/plan/platform-providers.d.ts +501 -0
- package/dist/plan/platform-providers.js +3029 -0
- package/dist/plan/platform-sfu.d.ts +93 -0
- package/dist/plan/platform-sfu.js +100 -0
- package/dist/plan/providers-readers.js +22 -1
- package/dist/plan/zitadel-machine-user.d.ts +34 -0
- package/dist/plan/zitadel-machine-user.js +95 -0
- package/dist/plan.d.ts +6 -0
- package/dist/platform.d.ts +55 -0
- package/dist/platform.js +46 -0
- package/dist/requested-changes.d.ts +108 -0
- package/dist/requested-changes.js +95 -0
- package/dist/slug.js +21 -2
- package/dist/store/audit.js +4 -2
- package/dist/store/postgres.d.ts +40 -7
- package/dist/store/postgres.js +257 -29
- package/dist/store/schema.d.ts +700 -0
- package/dist/store/schema.js +58 -0
- package/dist/store.d.ts +56 -0
- package/package.json +2 -2
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import type { CloudflareClient } from '@wtfalch/providers/cloudflare';
|
|
2
|
+
import type { DeclaredPlatform } from '../platform.js';
|
|
3
|
+
import type { DeclaredOrgStore, PlatformStore } from '../store.js';
|
|
4
|
+
import type { StepAction } from './diff.js';
|
|
5
|
+
import type { CloudflarePort, CoolifyPort, FoundryStepExecutorPorts, ZitadelPort } from './executor.js';
|
|
6
|
+
import type { LiveResource, PlatformCloudflareReader, PlatformCoolifyReader } from './live.js';
|
|
7
|
+
import type { PlatformStepExecutionContext, PlatformStepExecutor } from './plan-apply.js';
|
|
8
|
+
import { type CallsSfuPlatformDeps } from './platform-sfu.js';
|
|
9
|
+
/**
|
|
10
|
+
* #168: everything {@link provisionPlatformAuth} needs beyond
|
|
11
|
+
* {@link PlatformStepExecutionContext} -- a bundle, not more constructor
|
|
12
|
+
* positions, so every #167-era caller of {@link FoundryPlatformStepExecutor}
|
|
13
|
+
* (zone only) keeps compiling with this third argument simply omitted.
|
|
14
|
+
*/
|
|
15
|
+
export interface PlatformAuthDeps {
|
|
16
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
|
|
17
|
+
cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'
|
|
18
|
+
/** #197: fetches the tunnel's connector token for {@link ensureCloudflaredConnector}. */
|
|
19
|
+
| 'getTunnelToken'>;
|
|
20
|
+
/** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
|
|
21
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser'>;
|
|
22
|
+
/**
|
|
23
|
+
* Builds the ZITADEL port for the new instance from its issuer URL
|
|
24
|
+
* (`https://<auth.hostname>`). When set it is used instead of
|
|
25
|
+
* {@link zitadel}, so the mint always hits the instance this step just
|
|
26
|
+
* built. See `createZitadelAdminMinter` for the mint half.
|
|
27
|
+
*/
|
|
28
|
+
zitadelForInstance?: (issuerUrl: string) => PlatformAuthDeps['zitadel'];
|
|
29
|
+
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
30
|
+
/** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
|
|
31
|
+
runtime?: PlatformAuthRuntime;
|
|
32
|
+
}
|
|
33
|
+
export interface PlatformAuthRuntime {
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
sleep?: (ms: number) => Promise<void>;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
/** How long to wait for the new instance to answer (default 5 minutes). */
|
|
38
|
+
readyTimeoutMs?: number;
|
|
39
|
+
/** Pause between readiness polls (default 5 seconds). */
|
|
40
|
+
readyIntervalMs?: number;
|
|
41
|
+
/** Tries at handing the admin key to the host (default 3). */
|
|
42
|
+
deliverAttempts?: number;
|
|
43
|
+
}
|
|
44
|
+
/** The ref the scheduler's admin token is delivered under: the host's real `JobsSchedulerPort` adapter reads it back. */
|
|
45
|
+
export declare const JOBS_ADMIN_TOKEN_REF = "jobs:scheduler:adminToken";
|
|
46
|
+
/**
|
|
47
|
+
* The narrow slice of {@link CoolifyPort} {@link FoundryPlatformStepExecutor}
|
|
48
|
+
* needs for `coolify:jobs` (#173) -- the same project/environment/server/app/
|
|
49
|
+
* database calls `executor.ts`'s `coolifyApp`/`coolifyDatabase` make for an
|
|
50
|
+
* org's own app, reused here for the platform's one scheduler instead of once
|
|
51
|
+
* per org.
|
|
52
|
+
*/
|
|
53
|
+
type JobsCoolifyPort = Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'getApplication' | 'deploy'>;
|
|
54
|
+
/**
|
|
55
|
+
* Options for {@link FoundryPlatformStepExecutor}'s constructor, replacing
|
|
56
|
+
* four positional arguments that shifted on every merge that added a
|
|
57
|
+
* platform service (#186, #187, #188) -- see
|
|
58
|
+
* https://github.com/wtfalch/foundry/pull/188#issuecomment-5895935971, the
|
|
59
|
+
* merge comment that named this shape (#189).
|
|
60
|
+
*/
|
|
61
|
+
export interface FoundryPlatformStepExecutorOptions {
|
|
62
|
+
coolify?: JobsCoolifyPort;
|
|
63
|
+
/**
|
|
64
|
+
* Hands a just-generated secret to the host (`FoundryStepExecutorPorts.
|
|
65
|
+
* deliverSecret`'s contract: never returned, recorded, logged or put in
|
|
66
|
+
* an error). Needed by `coolify:jobs` for the scheduler's admin token.
|
|
67
|
+
*/
|
|
68
|
+
deliverSecret?: (ref: string, secret: string) => Promise<void>;
|
|
69
|
+
sfu?: CallsSfuPlatformDeps;
|
|
70
|
+
/**
|
|
71
|
+
* #168: dependencies for `coolify:app` keyed {@link PLATFORM_AUTH_KEY} --
|
|
72
|
+
* optional so every #167-era caller (zone only) keeps compiling; a host
|
|
73
|
+
* that declares `platform.auth.enabled` must construct this with it set,
|
|
74
|
+
* or `execute` throws naming what's missing.
|
|
75
|
+
*/
|
|
76
|
+
auth?: PlatformAuthDeps;
|
|
77
|
+
/**
|
|
78
|
+
* #169: dependencies for `coolify:app` keyed {@link PLATFORM_MAIL_KEY} --
|
|
79
|
+
* the same optionality as {@link auth}, for the identical reason.
|
|
80
|
+
*/
|
|
81
|
+
mail?: PlatformMailDeps;
|
|
82
|
+
/**
|
|
83
|
+
* #174: dependencies for the integrations host, `coolify:app` keyed
|
|
84
|
+
* {@link PLATFORM_INTEGRATIONS_KEY}. Without it, an enabled integrations
|
|
85
|
+
* host is reported blocked by `blockedSteps` rather than planned.
|
|
86
|
+
*/
|
|
87
|
+
integrations?: PlatformIntegrationsDeps;
|
|
88
|
+
/**
|
|
89
|
+
* #171: dependencies for `coolify:app` keyed {@link PLATFORM_FILES_KEY} --
|
|
90
|
+
* optional for the same reason as `auth`; a host that declares
|
|
91
|
+
* `platform.files.enabled` without them has the step reported blocked.
|
|
92
|
+
*/
|
|
93
|
+
files?: PlatformFilesDeps;
|
|
94
|
+
/**
|
|
95
|
+
* #172: dependencies for `coolify:app` keyed {@link PLATFORM_AI_KEY} --
|
|
96
|
+
* optional for the same reason as `auth`; a host that declares
|
|
97
|
+
* `platform.ai.enabled` without it gets the step reported as blocked.
|
|
98
|
+
*/
|
|
99
|
+
ai?: PlatformAiDeps;
|
|
100
|
+
/**
|
|
101
|
+
* #175: dependencies for `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}
|
|
102
|
+
* -- optional for the same reason as {@link auth}.
|
|
103
|
+
*/
|
|
104
|
+
archon?: PlatformArchonDeps;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The real {@link PlatformStepExecutor} for the two steps built so far:
|
|
108
|
+
* `cloudflare:zone` for the platform's apex (#167) and `coolify:jobs` for the
|
|
109
|
+
* jobs scheduler (#173). `coolify` is optional so every #167-era caller --
|
|
110
|
+
* anything that only ever plans the apex zone -- keeps constructing this with
|
|
111
|
+
* two arguments; a `coolify:jobs` step reaching this executor with no
|
|
112
|
+
* `coolify` port configured throws, naming what is missing, rather than
|
|
113
|
+
* silently doing nothing.
|
|
114
|
+
*
|
|
115
|
+
* `cloudflareZone` mirrors `executor.ts`'s `cloudflareZone`: find the zone by
|
|
116
|
+
* apex and adopt it, create it only when absent. companyincluded.com already
|
|
117
|
+
* exists on Cloudflare, empty, so the first apply adopts it. The owner org
|
|
118
|
+
* (Company Included, whose own domain is the apex) may record the same zone
|
|
119
|
+
* under its own applied state, so that owner is allowed. Any other org that
|
|
120
|
+
* recorded it makes this refuse to adopt. There is no delete here and
|
|
121
|
+
* `PlatformStepExecutor` has none: neither path ever removes the zone.
|
|
122
|
+
*
|
|
123
|
+
* `coolifyJobs` mirrors `executor.ts`'s `coolifyApp`/`coolifyDatabase`,
|
|
124
|
+
* simplified for one platform-level singleton rather than one per org: find
|
|
125
|
+
* or create a dedicated `platform` Coolify project, find or create the
|
|
126
|
+
* scheduler's own database, find or create the scheduler app itself (from
|
|
127
|
+
* https://github.com/wtfalch/jobs, one Dockerfile at its repo root, on the
|
|
128
|
+
* declared `gitRef`), then push the database's `internal_db_url` in as
|
|
129
|
+
* `DATABASE_URL`. There is no park branch yet, matching `planJobs`'s own
|
|
130
|
+
* scope note -- `platform.jobs.enabled` going from true to false plans
|
|
131
|
+
* nothing, so this method is never asked to stop or delete anything.
|
|
132
|
+
*
|
|
133
|
+
* #170 adds `sfu:livekit`, the platform's shared calls SFU. `sfu` is
|
|
134
|
+
* optional: an existing host that constructs this executor for the zone step
|
|
135
|
+
* alone keeps compiling, and a plan that includes an `sfu:livekit` step
|
|
136
|
+
* without it configured refuses clearly rather than silently doing nothing.
|
|
137
|
+
*
|
|
138
|
+
* #168 adds `coolify:app` keyed {@link PLATFORM_AUTH_KEY}, the platform's own
|
|
139
|
+
* sign-in ({@link provisionPlatformAuth}), once a host passes
|
|
140
|
+
* {@link PlatformAuthDeps}.
|
|
141
|
+
*
|
|
142
|
+
* #169 adds `coolify:app` keyed {@link PLATFORM_MAIL_KEY}, the platform's own
|
|
143
|
+
* mail server ({@link provisionPlatformMail}), once a host passes
|
|
144
|
+
* {@link PlatformMailDeps}.
|
|
145
|
+
*
|
|
146
|
+
* #174 adds `coolify:app` keyed {@link PLATFORM_INTEGRATIONS_KEY},
|
|
147
|
+
* the integrations host ({@link provisionPlatformIntegrations}), once a host
|
|
148
|
+
* passes {@link PlatformIntegrationsDeps}.
|
|
149
|
+
*
|
|
150
|
+
* #171 adds `coolify:app` keyed {@link PLATFORM_FILES_KEY}, the files host
|
|
151
|
+
* ({@link provisionPlatformFiles}), once a host passes {@link PlatformFilesDeps}.
|
|
152
|
+
*
|
|
153
|
+
* #172 adds `coolify:app` keyed {@link PLATFORM_AI_KEY}, the platform's AI
|
|
154
|
+
* host ({@link provisionPlatformAi}), once a host passes {@link PlatformAiDeps}.
|
|
155
|
+
*
|
|
156
|
+
* #175 adds `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, Archon itself
|
|
157
|
+
* ({@link provisionPlatformArchon}), once a host passes
|
|
158
|
+
* {@link PlatformArchonDeps}.
|
|
159
|
+
*/
|
|
160
|
+
export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor {
|
|
161
|
+
private readonly cloudflare;
|
|
162
|
+
private readonly declaredOrgStore;
|
|
163
|
+
private readonly coolify?;
|
|
164
|
+
private readonly deliverSecret?;
|
|
165
|
+
private readonly sfu?;
|
|
166
|
+
private readonly auth?;
|
|
167
|
+
private readonly mail?;
|
|
168
|
+
private readonly integrations?;
|
|
169
|
+
private readonly files?;
|
|
170
|
+
private readonly ai?;
|
|
171
|
+
private readonly archon?;
|
|
172
|
+
constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone'>, options?: FoundryPlatformStepExecutorOptions);
|
|
173
|
+
blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
|
|
174
|
+
auth?: string;
|
|
175
|
+
files?: string;
|
|
176
|
+
integrations?: string;
|
|
177
|
+
ai?: string;
|
|
178
|
+
archon?: string;
|
|
179
|
+
mail?: string;
|
|
180
|
+
}>;
|
|
181
|
+
/** Whether Archon's live Access policy lets through anyone but the declared emails. */
|
|
182
|
+
driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
|
|
183
|
+
archon?: boolean;
|
|
184
|
+
}>;
|
|
185
|
+
execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
|
|
186
|
+
resourceId?: string;
|
|
187
|
+
} | undefined>;
|
|
188
|
+
private cloudflareZone;
|
|
189
|
+
private executeSfu;
|
|
190
|
+
private coolifyJobs;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The fixed description Stalwart's admin API key is minted under
|
|
194
|
+
* (`StalwartClient.createApiKey`'s own signature returns only the secret,
|
|
195
|
+
* never an id) -- what a human revokes it by in Stalwart's own admin UI if
|
|
196
|
+
* it is ever lost between mint and delivery, and what
|
|
197
|
+
* {@link PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY} records in that case.
|
|
198
|
+
*/
|
|
199
|
+
export declare const PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION = "foundry:platform:admin";
|
|
200
|
+
/**
|
|
201
|
+
* Stalwart's own management API, scoped to exactly what this step needs from
|
|
202
|
+
* the instance it just deployed -- mirrors {@link PlatformAuthDeps}'s
|
|
203
|
+
* `zitadel`/`zitadelForInstance` split: a host builds this from the
|
|
204
|
+
* instance's own URL and the recovery-admin credential this step generated
|
|
205
|
+
* ({@link PlatformMailDeps.stalwartForInstance}), the same way
|
|
206
|
+
* `zitadelForInstance` is built from a freshly issued instance's issuer URL.
|
|
207
|
+
* The real implementation wraps `@wtfalch/email/postmaster`'s `openInstance`
|
|
208
|
+
* (for the `StalwartClient`), a hand-built settings write (see
|
|
209
|
+
* `provisionPlatformMail`'s own doc comment for why not `mailObjects`) and
|
|
210
|
+
* `StalwartClient.createApiKey`.
|
|
211
|
+
*/
|
|
212
|
+
export interface PlatformMailAdminPort {
|
|
213
|
+
/**
|
|
214
|
+
* Sets the instance's default hostname and its OIDC directory
|
|
215
|
+
* (`x:SystemSettings`/`x:Directory`/`x:Domain`/`x:Authentication`) --
|
|
216
|
+
* idempotent, safe to call on every apply once the instance is up.
|
|
217
|
+
*/
|
|
218
|
+
applySettings(input: {
|
|
219
|
+
mailHost: string;
|
|
220
|
+
oidc: {
|
|
221
|
+
issuerUrl: string;
|
|
222
|
+
audience: string;
|
|
223
|
+
};
|
|
224
|
+
}): Promise<void>;
|
|
225
|
+
/** `StalwartClient.createApiKey`'s own signature. */
|
|
226
|
+
createApiKey(description: string, permissions: readonly string[] | 'inherit'): Promise<string>;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* #169: everything {@link provisionPlatformMail} needs beyond
|
|
230
|
+
* {@link PlatformStepExecutionContext}, mirroring {@link PlatformAuthDeps}'s
|
|
231
|
+
* own shape.
|
|
232
|
+
*/
|
|
233
|
+
export interface PlatformMailDeps {
|
|
234
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
|
|
235
|
+
cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'>;
|
|
236
|
+
/**
|
|
237
|
+
* The ZITADEL resource id an access token's `aud` claim must carry for
|
|
238
|
+
* Stalwart's OIDC directory to accept it -- a resource API registered
|
|
239
|
+
* against the platform's own sign-in (#168), which neither that step nor
|
|
240
|
+
* this one creates. See `provisionPlatformMail`'s own doc comment, "Known
|
|
241
|
+
* gaps".
|
|
242
|
+
*/
|
|
243
|
+
oidcAudience: string;
|
|
244
|
+
/**
|
|
245
|
+
* {@link PlatformMailAdminPort}, built from the instance this step just
|
|
246
|
+
* deployed and the recovery credential it just read back.
|
|
247
|
+
*/
|
|
248
|
+
stalwartForInstance(input: {
|
|
249
|
+
url: string;
|
|
250
|
+
user: string;
|
|
251
|
+
password: string;
|
|
252
|
+
}): PlatformMailAdminPort;
|
|
253
|
+
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
254
|
+
/** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
|
|
255
|
+
runtime?: PlatformMailRuntime;
|
|
256
|
+
}
|
|
257
|
+
export interface PlatformMailRuntime {
|
|
258
|
+
fetch?: typeof fetch;
|
|
259
|
+
sleep?: (ms: number) => Promise<void>;
|
|
260
|
+
now?: () => number;
|
|
261
|
+
/** How long to wait for the new instance to answer (default 5 minutes). */
|
|
262
|
+
readyTimeoutMs?: number;
|
|
263
|
+
/** Pause between readiness polls (default 5 seconds). */
|
|
264
|
+
readyIntervalMs?: number;
|
|
265
|
+
/** Tries at handing the admin key to the host (default 3). */
|
|
266
|
+
deliverAttempts?: number;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* #175: everything {@link provisionPlatformArchon} needs beyond
|
|
270
|
+
* {@link PlatformStepExecutionContext}, bundled like {@link PlatformAuthDeps}.
|
|
271
|
+
*/
|
|
272
|
+
export interface PlatformArchonDeps {
|
|
273
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
|
|
274
|
+
cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord' | 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
|
|
275
|
+
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
276
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp'>;
|
|
277
|
+
/** {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client. */
|
|
278
|
+
zitadelForInstance?: (issuerUrl: string) => PlatformArchonDeps['zitadel'];
|
|
279
|
+
/**
|
|
280
|
+
* Who the Cloudflare Access app in front of Archon lets through. Host
|
|
281
|
+
* config, not `DeclaredPlatform`: the platform record names services, not
|
|
282
|
+
* people. Empty blocks the step (`blockedSteps`) rather than building an
|
|
283
|
+
* Access app nobody can pass.
|
|
284
|
+
*/
|
|
285
|
+
accessEmails: readonly string[];
|
|
286
|
+
/**
|
|
287
|
+
* Reads a secret an earlier step handed to the host through
|
|
288
|
+
* `deliverSecret`, or `null` when none is held under `ref` yet -- the same
|
|
289
|
+
* direction `CallsSfuPort.resolveCredentials` reads held keys back. Every
|
|
290
|
+
* value it returns goes straight to `bulkUpsertEnvs` and nowhere else.
|
|
291
|
+
* Optional so a host without it still compiles; while a service is enabled
|
|
292
|
+
* without it (or without a key it holds) Archon is blocked, not skipped.
|
|
293
|
+
*/
|
|
294
|
+
heldKeys?: {
|
|
295
|
+
resolve(ref: string): Promise<string | null>;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The name of foundry's own Cloudflare Access app in front of Archon. Same
|
|
300
|
+
* `foundry:<key>` convention `executor.ts`'s `cloudflareAccess` uses for an
|
|
301
|
+
* org (`foundry:<orgId>`), with the platform's step key in the org id's place.
|
|
302
|
+
*/
|
|
303
|
+
export declare const PLATFORM_ARCHON_ACCESS_APP_NAME = "foundry:platform:archon";
|
|
304
|
+
/** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
|
|
305
|
+
export declare const PLATFORM_ZITADEL_ORG_SCOPE_KEY = "zitadel_platform_org";
|
|
306
|
+
/** Archon's OIDC client id: public, not a secret, so a plain scope id. */
|
|
307
|
+
export declare const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = "zitadel_platform_archon_client_id";
|
|
308
|
+
/**
|
|
309
|
+
* #172: mints foundry's AI management key -- the plain bearer key
|
|
310
|
+
* (`wtfk_...`) `AiOrganisationPort` (`executor.ts`) authenticates with,
|
|
311
|
+
* holding {@link PLATFORM_AI_MANAGEMENT_PERMISSIONS} in ai's platform
|
|
312
|
+
* (operator) tenant. The secret comes back once; {@link provisionPlatformAi}
|
|
313
|
+
* hands it to `deliverSecret` and records only the credential id.
|
|
314
|
+
*
|
|
315
|
+
* No implementation ships with this package, because none can be built from
|
|
316
|
+
* it today: see {@link provisionPlatformAi}'s "Known gaps". A host that has
|
|
317
|
+
* a way to run ai's `keys:issue-archon` (or a future HTTP bootstrap route)
|
|
318
|
+
* supplies one; without it the planner reports the step as blocked
|
|
319
|
+
* ({@link PLATFORM_AI_MINTER_BLOCKED}) and plans nothing.
|
|
320
|
+
*
|
|
321
|
+
* Requirement: before issuing anything, a minter must look up an existing
|
|
322
|
+
* management key named `foundry` in the platform tenant and return that one
|
|
323
|
+
* instead of issuing a new one. A crash after `mintManagementKey` returns and
|
|
324
|
+
* before the caller records the credential id leaves a live key nobody knows
|
|
325
|
+
* about; only the minter can find it again, so without this lookup the next
|
|
326
|
+
* apply mints a second key.
|
|
327
|
+
*/
|
|
328
|
+
export interface AiManagementKeyMinter {
|
|
329
|
+
mintManagementKey(input: {
|
|
330
|
+
/** Where the ai host answers on the Coolify network, e.g. `http://platform-ai:3000`. */
|
|
331
|
+
internalUrl: string;
|
|
332
|
+
name: string;
|
|
333
|
+
expiresDays: number;
|
|
334
|
+
permissions: readonly string[];
|
|
335
|
+
}): Promise<{
|
|
336
|
+
credentialId: string;
|
|
337
|
+
organisationId: string;
|
|
338
|
+
secret: string;
|
|
339
|
+
}>;
|
|
340
|
+
}
|
|
341
|
+
/** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
|
|
342
|
+
export interface PlatformAiDeps {
|
|
343
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
|
|
344
|
+
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
345
|
+
/** Optional: see {@link AiManagementKeyMinter}. Absent, the planner blocks the step until the key is delivered. */
|
|
346
|
+
managementKeyMinter?: AiManagementKeyMinter;
|
|
347
|
+
runtime?: PlatformAiRuntime;
|
|
348
|
+
}
|
|
349
|
+
export interface PlatformAiRuntime {
|
|
350
|
+
sleep?: (ms: number) => Promise<void>;
|
|
351
|
+
now?: () => number;
|
|
352
|
+
/** How long to wait for the migrate job to report healthy (default 10 minutes). */
|
|
353
|
+
migrateTimeoutMs?: number;
|
|
354
|
+
/** Pause between migrate-job status polls (default 5 seconds). */
|
|
355
|
+
migrateIntervalMs?: number;
|
|
356
|
+
/** Tries at handing the management key to the host (default 3). */
|
|
357
|
+
deliverAttempts?: number;
|
|
358
|
+
}
|
|
359
|
+
/** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
|
|
360
|
+
export declare const PLATFORM_AI_MANAGEMENT_KEY_REF = "ai:platform:managementKey";
|
|
361
|
+
/**
|
|
362
|
+
* What foundry's management key may do: exactly the grants `ai`'s README
|
|
363
|
+
* ("Archon's first key", by-hand form) gives archon's key, which are the
|
|
364
|
+
* ones `ai:organisation`, `ai:budget`, `ai:key` and the org destroy call.
|
|
365
|
+
*/
|
|
366
|
+
export declare const PLATFORM_AI_MANAGEMENT_PERMISSIONS: readonly string[];
|
|
367
|
+
/** The message {@link provisionPlatformAi} throws when no {@link AiManagementKeyMinter} was supplied. */
|
|
368
|
+
export declare const PLATFORM_AI_MINTER_MISSING = "platform:ai: the AI host is deployed but foundry's AI management key cannot be minted: no AiManagementKeyMinter was supplied (PlatformAiDeps.managementKeyMinter). ai mints its first platform credential only through its keys:issue-archon script run against the database (apps/host/scripts/authz/bootstrap-archon.ts); it has no HTTP route for that, and CoolifyPort cannot run a command inside a container (#172)";
|
|
369
|
+
/**
|
|
370
|
+
* #171: everything {@link provisionPlatformFiles} needs beyond
|
|
371
|
+
* {@link PlatformStepExecutionContext}, bundled for the same reason as
|
|
372
|
+
* {@link PlatformAuthDeps}.
|
|
373
|
+
*/
|
|
374
|
+
export interface PlatformFilesDeps {
|
|
375
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
|
|
376
|
+
/**
|
|
377
|
+
* Must target the platform's own sign-in (the instance `platform:auth`
|
|
378
|
+
* built), or {@link zitadelForInstance} must be set.
|
|
379
|
+
*/
|
|
380
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createApiApp' | 'listApps' | 'deleteApp'>;
|
|
381
|
+
/**
|
|
382
|
+
* Builds the ZITADEL port for the platform's sign-in from its issuer URL
|
|
383
|
+
* (`https://<auth.hostname>`); used instead of {@link zitadel} when set, the
|
|
384
|
+
* same seam {@link PlatformAuthDeps.zitadelForInstance} is.
|
|
385
|
+
*/
|
|
386
|
+
zitadelForInstance?: (issuerUrl: string) => PlatformFilesDeps['zitadel'];
|
|
387
|
+
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
388
|
+
/**
|
|
389
|
+
* The keys worker the files host verifies and seals with, from the host's
|
|
390
|
+
* held keys (`KEYS_WORKER_URL`, `KEYS_CREDENTIAL`). The `files-migrate` app
|
|
391
|
+
* never becomes healthy without them, so the step sets both on both apps,
|
|
392
|
+
* and the executor reports the step blocked while this is unset. Never
|
|
393
|
+
* recorded or logged.
|
|
394
|
+
*/
|
|
395
|
+
keys?: {
|
|
396
|
+
workerUrl: string;
|
|
397
|
+
credential: string;
|
|
398
|
+
};
|
|
399
|
+
/** Clock and sleep for the migration wait and delivery retries; real ones by default. */
|
|
400
|
+
runtime?: PlatformFilesRuntime;
|
|
401
|
+
}
|
|
402
|
+
export interface PlatformFilesRuntime {
|
|
403
|
+
sleep?: (ms: number) => Promise<void>;
|
|
404
|
+
now?: () => number;
|
|
405
|
+
/** How long to wait for `files-migrate` to report healthy (default 15 minutes: it builds first). */
|
|
406
|
+
migrateTimeoutMs?: number;
|
|
407
|
+
/** Pause between `files-migrate` status polls (default 10 seconds). */
|
|
408
|
+
migrateIntervalMs?: number;
|
|
409
|
+
/** Tries at handing each secret to the host (default 3). */
|
|
410
|
+
deliverAttempts?: number;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* #171: how the host signs a files management request with the key this step
|
|
414
|
+
* delivers under {@link FILES_MANAGEMENT_KEY_REF} (files'
|
|
415
|
+
* `signManagementRequest`'s `SigningIdentity`): `issuer` is
|
|
416
|
+
* {@link FILES_MANAGEMENT_ISSUER}, `keyId` is {@link FILES_MANAGEMENT_KEY_ID},
|
|
417
|
+
* `audience` is `https://<platform.files.hostname>` (the files host's
|
|
418
|
+
* `APP_URL`, which this step sets), and `privateKey` is the delivered PKCS#8
|
|
419
|
+
* PEM. Neither constant is a secret. The files host pins the public half
|
|
420
|
+
* through `MANAGEMENT_ISSUER`/`MANAGEMENT_PUBLIC_KEYS`, which this step sets.
|
|
421
|
+
*/
|
|
422
|
+
export declare const FILES_MANAGEMENT_ISSUER = "foundry";
|
|
423
|
+
export declare const FILES_MANAGEMENT_KEY_ID = "foundry-1";
|
|
424
|
+
/** The ref the management private key (PKCS#8 PEM) is delivered under. */
|
|
425
|
+
export declare const FILES_MANAGEMENT_KEY_REF = "files:platform:managementKey";
|
|
426
|
+
/** The ref the files host's introspection client secret is delivered under. */
|
|
427
|
+
export declare const FILES_INTROSPECTION_SECRET_REF = "zitadel:platform:files:introspection";
|
|
428
|
+
/**
|
|
429
|
+
* #174: everything {@link provisionPlatformIntegrations} needs beyond
|
|
430
|
+
* {@link PlatformStepExecutionContext} -- a bundle, like
|
|
431
|
+
* {@link PlatformAuthDeps}, so every earlier caller of
|
|
432
|
+
* {@link FoundryPlatformStepExecutor} keeps compiling with it omitted.
|
|
433
|
+
*/
|
|
434
|
+
export interface PlatformIntegrationsDeps {
|
|
435
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'raw'>;
|
|
436
|
+
/** Hands the management private key to the host, once (`FoundryStepExecutorPorts.deliverSecret`'s contract). */
|
|
437
|
+
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
438
|
+
/**
|
|
439
|
+
* The host's own `@wtfalch/keys` settings, written to the apps' env (the
|
|
440
|
+
* migrate app takes the first two). Without every value the planner reports
|
|
441
|
+
* the step blocked: the runner's health check needs them, so the step would
|
|
442
|
+
* never finish. Secrets: only ever passed to `bulkUpsertEnvs`.
|
|
443
|
+
*/
|
|
444
|
+
keysEnv: Readonly<Partial<Record<IntegrationsKeysEnvName, string>>>;
|
|
445
|
+
runtime?: PlatformIntegrationsRuntime;
|
|
446
|
+
}
|
|
447
|
+
export declare const INTEGRATIONS_KEYS_ENV_NAMES: readonly ["KEYS_WORKER_URL", "KEYS_CREDENTIAL", "KEYS_VERIFY_KEYS", "KEYS_AUDIT_TOKEN"];
|
|
448
|
+
export type IntegrationsKeysEnvName = (typeof INTEGRATIONS_KEYS_ENV_NAMES)[number];
|
|
449
|
+
export interface PlatformIntegrationsRuntime {
|
|
450
|
+
sleep?: (ms: number) => Promise<void>;
|
|
451
|
+
now?: () => number;
|
|
452
|
+
/** How long to wait for the migrate app to report healthy (default 20 minutes: it builds the whole repo first). */
|
|
453
|
+
migrateTimeoutMs?: number;
|
|
454
|
+
/** Pause between polls of the migrate app's status (default 10 seconds). */
|
|
455
|
+
migratePollMs?: number;
|
|
456
|
+
/** Tries at handing the management key to the host (default 3). */
|
|
457
|
+
deliverAttempts?: number;
|
|
458
|
+
}
|
|
459
|
+
/** The ref the management private key (PKCS#8 PEM) is delivered under. */
|
|
460
|
+
export declare const INTEGRATIONS_MANAGEMENT_KEY_REF = "integrations:management:privateKey";
|
|
461
|
+
/** `MANAGEMENT_ISSUER` on the integrations host: whoever signs with the delivered key signs as this issuer. */
|
|
462
|
+
export declare const INTEGRATIONS_MANAGEMENT_ISSUER = "foundry";
|
|
463
|
+
/**
|
|
464
|
+
* The key id the public half is pinned under in `MANAGEMENT_PUBLIC_KEYS`
|
|
465
|
+
* (the host's keyring allows `^[A-Za-z0-9_-]{1,100}$`). A signer names it in
|
|
466
|
+
* its signed request; fixed, since there is one key until rotation is built.
|
|
467
|
+
*/
|
|
468
|
+
export declare const INTEGRATIONS_MANAGEMENT_KEY_ID = "foundry-1";
|
|
469
|
+
/**
|
|
470
|
+
* The real {@link PlatformCloudflareReader}: the platform's apex zone as
|
|
471
|
+
* `planApexZone` expects to see it, and nothing else. The zone's DNS records
|
|
472
|
+
* and tunnels belong to no declared platform step yet, so listing them would
|
|
473
|
+
* only report them unmanaged.
|
|
474
|
+
*
|
|
475
|
+
* The zone is fetched by the id the last apply recorded when there is one,
|
|
476
|
+
* because Cloudflare's zone list can lag a create; with no recorded id it is
|
|
477
|
+
* found by apex.
|
|
478
|
+
*/
|
|
479
|
+
export declare class ProvidersPlatformCloudflareReader implements PlatformCloudflareReader {
|
|
480
|
+
private readonly client;
|
|
481
|
+
private readonly platformStore;
|
|
482
|
+
constructor(client: Pick<CloudflareClient, 'findZoneByApex' | 'getZone'>, platformStore: Pick<PlatformStore, 'get' | 'getServiceIds'>);
|
|
483
|
+
listResources(): Promise<LiveResource[]>;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* #173: the real {@link PlatformCoolifyReader} for the jobs scheduler app --
|
|
487
|
+
* the recorded-id half of `ProvidersPlatformCloudflareReader`'s own strategy
|
|
488
|
+
* above, with no list-by-name fallback: unlike a Cloudflare zone (which
|
|
489
|
+
* exists before foundry ever touches it), the scheduler app has no identity
|
|
490
|
+
* to adopt until this package creates it, so "nothing recorded yet" and
|
|
491
|
+
* "nothing live yet" are the same state here. A recorded id Coolify no
|
|
492
|
+
* longer has (deleted, or moved) reads as not-live, the same "stale id never
|
|
493
|
+
* blocks planning for good" rule {@link recordedZone} follows.
|
|
494
|
+
*/
|
|
495
|
+
export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyReader {
|
|
496
|
+
private readonly client;
|
|
497
|
+
private readonly platformStore;
|
|
498
|
+
constructor(client: Pick<CoolifyPort, 'getApplication'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
|
|
499
|
+
listResources(): Promise<LiveResource[]>;
|
|
500
|
+
}
|
|
501
|
+
export {};
|