@wtfalch/foundry 0.34.0 → 0.35.2
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 +1 -1
- package/dist/bootstrap/bootstrap.js +49 -11
- package/dist/bootstrap/declare.d.ts +12 -5
- package/dist/bootstrap/declare.js +33 -18
- package/dist/bootstrap/versions.d.ts +20 -0
- package/dist/bootstrap/versions.js +46 -0
- package/dist/calls-sfu.js +3 -3
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/migrations/0053_heartbeats_provider.sql +9 -0
- package/dist/migrations/0053_org_version_pin.sql +6 -0
- package/dist/org.d.ts +60 -14
- package/dist/org.js +118 -28
- package/dist/plan/backup-gate.d.ts +93 -0
- package/dist/plan/backup-gate.js +261 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +20 -3
- package/dist/plan/build-record.d.ts +56 -0
- package/dist/plan/build-record.js +109 -0
- package/dist/plan/calls-matrix.js +3 -3
- package/dist/plan/cloudflared-connector.js +2 -2
- package/dist/plan/company-backups.d.ts +31 -0
- package/dist/plan/company-backups.js +101 -0
- package/dist/plan/deploy-health.d.ts +12 -2
- package/dist/plan/deploy-health.js +3 -1
- package/dist/plan/destinations.d.ts +73 -0
- package/dist/plan/destinations.js +217 -0
- package/dist/plan/diff.d.ts +17 -8
- package/dist/plan/diff.js +131 -131
- package/dist/plan/edge-access.d.ts +15 -0
- package/dist/plan/edge-access.js +21 -2
- package/dist/plan/executor.d.ts +166 -7
- package/dist/plan/executor.js +547 -106
- package/dist/plan/health-targets.d.ts +1 -3
- package/dist/plan/health-targets.js +36 -36
- package/dist/plan/held-key-refs.d.ts +5 -0
- package/dist/plan/held-key-refs.js +5 -0
- package/dist/plan/hostnames.d.ts +10 -0
- package/dist/plan/hostnames.js +14 -0
- package/dist/plan/index.d.ts +5 -0
- package/dist/plan/index.js +3 -0
- package/dist/plan/live.d.ts +6 -0
- package/dist/plan/live.js +3 -2
- package/dist/plan/locked-hostnames.d.ts +2 -1
- package/dist/plan/locked-hostnames.js +16 -3
- package/dist/plan/login-host.d.ts +9 -20
- package/dist/plan/login-host.js +10 -25
- package/dist/plan/mail-relay.d.ts +48 -29
- package/dist/plan/mail-relay.js +145 -68
- package/dist/plan/network-move.d.ts +20 -0
- package/dist/plan/network-move.js +41 -0
- package/dist/plan/oikos-ingress.d.ts +111 -0
- package/dist/plan/oikos-ingress.js +231 -0
- package/dist/plan/org-names.d.ts +8 -2
- package/dist/plan/org-names.js +57 -1
- package/dist/plan/own-diff.d.ts +9 -8
- package/dist/plan/own-diff.js +42 -34
- package/dist/plan/own-hostnames.d.ts +27 -0
- package/dist/plan/own-hostnames.js +24 -0
- package/dist/plan/own-limits.d.ts +198 -7
- package/dist/plan/own-limits.js +163 -32
- package/dist/plan/own-services.js +3 -1
- package/dist/plan/own-sfu.js +3 -2
- package/dist/plan/plan-apply.d.ts +47 -7
- package/dist/plan/plan-apply.js +184 -38
- package/dist/plan/platform-diff.d.ts +133 -17
- package/dist/plan/platform-diff.js +231 -68
- package/dist/plan/platform-org.d.ts +79 -10
- package/dist/plan/platform-org.js +93 -13
- package/dist/plan/platform-providers.d.ts +118 -73
- package/dist/plan/platform-providers.js +799 -241
- package/dist/plan/proxy-labels.d.ts +57 -11
- package/dist/plan/proxy-labels.js +94 -14
- package/dist/plan/run-options.d.ts +19 -0
- package/dist/plan/run-options.js +46 -0
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +76 -8
- package/dist/platform.d.ts +32 -0
- package/dist/platform.js +16 -0
- package/dist/provision.d.ts +2 -0
- package/dist/provision.js +3 -1
- package/dist/slug.d.ts +3 -1
- package/dist/slug.js +25 -3
- package/dist/store/postgres.js +43 -1
- package/dist/store/schema.d.ts +36 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -3
- package/versions.digests.json +11 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { ownsServices } from '../org.js';
|
|
2
|
+
import { ZITADEL_ORG_SCOPE_KEY } from './agora.js';
|
|
2
3
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
3
4
|
import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
5
|
+
import { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, ownAuthApplied } from './login-host.js';
|
|
6
|
+
import { oikosIngressApplied } from './oikos-ingress.js';
|
|
7
|
+
import { ownHostnames } from './own-hostnames.js';
|
|
4
8
|
import { OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget } from './own-services.js';
|
|
5
|
-
import { JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY,
|
|
9
|
+
import { JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_EDGE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
|
|
6
10
|
/**
|
|
7
11
|
* #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
|
|
8
12
|
* fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
|
|
@@ -128,18 +132,33 @@ edgePortPresent = false) {
|
|
|
128
132
|
// enabled or applied.
|
|
129
133
|
platformApex: platform.apex,
|
|
130
134
|
platformOwnerOrgId: platform.ownerOrgId || null,
|
|
131
|
-
platformOwnerUserId: platformRecordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY] || null,
|
|
132
135
|
platformLettermintProjectId: platform.mail.lettermintProjectId ?? null,
|
|
133
136
|
filesReady: filesApplied && missingKeys.files === null,
|
|
134
137
|
filesUrl: filesApplied
|
|
135
138
|
? (platformRecordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY] ?? null)
|
|
136
139
|
: null,
|
|
137
140
|
aiReady: aiApplied && missingKeys.ai === null,
|
|
138
|
-
|
|
141
|
+
// An entry runs on its company's own network and cannot dial the platform's container name, so it
|
|
142
|
+
// is given the host's public origin.
|
|
143
|
+
// Only once the route (tunnel, connector, DNS record) is recorded: before it the hostname answers nothing.
|
|
144
|
+
aiUrl: aiApplied && platformRecordedIds[PLATFORM_AI_TUNNEL_SCOPE_KEY]
|
|
145
|
+
? `https://${platform.ai.hostname}`
|
|
146
|
+
: null,
|
|
139
147
|
callsReady: platform.sfu.enabled,
|
|
148
|
+
oikosIngressApplied: oikosIngressApplied(platformRecordedIds),
|
|
149
|
+
archonBrokerAppId: platformRecordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ?? null,
|
|
150
|
+
platformZitadelIds: [
|
|
151
|
+
PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY,
|
|
152
|
+
PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY,
|
|
153
|
+
PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY,
|
|
154
|
+
PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY,
|
|
155
|
+
].flatMap((key) => platformRecordedIds[key] || []),
|
|
140
156
|
...(platformRecordedIds[ACCESS_PROVIDER_RECORD_KEY]
|
|
141
157
|
? { accessProviderId: platformRecordedIds[ACCESS_PROVIDER_RECORD_KEY] }
|
|
142
158
|
: {}),
|
|
159
|
+
...(coolifyAppApplied(platformRecordedIds, PLATFORM_AUTH_KEY)
|
|
160
|
+
? { platformAuthRecordedHost: platform.auth.hostname }
|
|
161
|
+
: {}),
|
|
143
162
|
authReady,
|
|
144
163
|
authIssuer: authReady ? `https://${platform.auth.hostname}` : null,
|
|
145
164
|
jobsReady: jobsApplied && jobsUrl !== undefined && missingKeys.jobs === null,
|
|
@@ -150,13 +169,13 @@ edgePortPresent = false) {
|
|
|
150
169
|
...(noKeyReader ? { noKeyReader } : {}),
|
|
151
170
|
};
|
|
152
171
|
}
|
|
153
|
-
/** The git refs the platform declares for the services an own org deploys. */
|
|
154
|
-
export function serviceGitRefsOf(platform) {
|
|
172
|
+
/** The git refs the platform declares for the services an own org deploys; a company's `pin` wins per part. */
|
|
173
|
+
export function serviceGitRefsOf(platform, pin) {
|
|
155
174
|
return {
|
|
156
|
-
auth: platform.auth.gitRef,
|
|
157
|
-
mail: platform.mail.gitRef,
|
|
158
|
-
files: platform.files.gitRef,
|
|
159
|
-
jobs: platform.jobs.gitRef,
|
|
175
|
+
auth: pin?.auth ?? platform.auth.gitRef,
|
|
176
|
+
mail: pin?.mail ?? platform.mail.gitRef,
|
|
177
|
+
files: pin?.files ?? platform.files.gitRef,
|
|
178
|
+
jobs: pin?.jobs ?? platform.jobs.gitRef,
|
|
160
179
|
};
|
|
161
180
|
}
|
|
162
181
|
/** The runner app both the platform's and an own org's files step record. */
|
|
@@ -202,14 +221,19 @@ export function orgPlatformReadinessFor(org, base, orgRecordedIds, heldRefs) {
|
|
|
202
221
|
return {
|
|
203
222
|
...rest,
|
|
204
223
|
authReady,
|
|
205
|
-
authIssuer: authReady
|
|
224
|
+
authIssuer: authReady ? ownHostnames(org).issuerUrl : null,
|
|
206
225
|
filesReady: filesApplied && filesUrl !== undefined && missingKeys.files === null,
|
|
207
226
|
filesUrl: filesApplied ? (filesUrl ?? null) : null,
|
|
208
227
|
jobsReady: jobsApplied && jobsUrl !== undefined && missingKeys.jobs === null,
|
|
209
228
|
...(jobsApplied && jobsUrl === undefined ? { jobsUrlMissing: true } : {}),
|
|
210
229
|
callsReady: ownStepRecorded(orgRecordedIds, org, 'own-sfu'),
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
// The MX name is the shared relay's, `mx.<platform apex>`: no platform mail step is needed.
|
|
231
|
+
// The apex is the company's mail zone: its domain when mail runs on it, else oikos.host.
|
|
232
|
+
mailHost: mailApplied && base.platformApex
|
|
233
|
+
? {
|
|
234
|
+
apex: org.domain && org.mail.usesDomain ? org.domain : NO_DOMAIN_MAIL_APEX,
|
|
235
|
+
mailHost: platformMailMxHost(base.platformApex),
|
|
236
|
+
}
|
|
213
237
|
: null,
|
|
214
238
|
missingKeys,
|
|
215
239
|
};
|
|
@@ -232,7 +256,7 @@ export async function heldOwnKeyRefs(heldKeys, org) {
|
|
|
232
256
|
export function agoraHostValuesForOwn(org, orgRecordedIds) {
|
|
233
257
|
const authReady = ownAuthReady(orgRecordedIds);
|
|
234
258
|
return {
|
|
235
|
-
issuer: authReady
|
|
259
|
+
issuer: authReady ? ownHostnames(org).issuerUrl : null,
|
|
236
260
|
mailProjectId: orgRecordedIds[PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY] ?? null,
|
|
237
261
|
messagingProjectId: null,
|
|
238
262
|
mailUpstream: orgRecordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY] ?? null,
|
|
@@ -272,3 +296,59 @@ export function agoraHostValuesFromPlatform(platform, platformRecordedIds) {
|
|
|
272
296
|
: null,
|
|
273
297
|
};
|
|
274
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* The owner's sign-in from its declared org and the ids its own steps recorded.
|
|
301
|
+
* The host and issuer are the host the own-auth step last applied, and only
|
|
302
|
+
* while that is the declared `ownHostnames(org).auth`: a domain edit gives
|
|
303
|
+
* `null` until the step has applied the new host, so the owner's instance
|
|
304
|
+
* token is never sent to a host the step has not set up.
|
|
305
|
+
*/
|
|
306
|
+
export function ownerSignInFrom(owner, recorded) {
|
|
307
|
+
if (!owner || !ownsServices(owner) || !ownAuthApplied(owner, recorded))
|
|
308
|
+
return null;
|
|
309
|
+
const organisationId = recorded[ZITADEL_ORG_SCOPE_KEY];
|
|
310
|
+
const brokerAppId = recorded[PLATFORM_AUTH_BROKER_SCOPE_KEY];
|
|
311
|
+
if (!organisationId || !brokerAppId)
|
|
312
|
+
return null;
|
|
313
|
+
const applied = recorded[OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY];
|
|
314
|
+
if (!applied || applied !== ownHostnames(owner).auth)
|
|
315
|
+
return null;
|
|
316
|
+
const hostname = applied;
|
|
317
|
+
return {
|
|
318
|
+
issuerUrl: `https://${applied}`,
|
|
319
|
+
organisationId,
|
|
320
|
+
brokerAppId,
|
|
321
|
+
bearerRef: orgTarget(owner).refs.instanceBearer,
|
|
322
|
+
hostname,
|
|
323
|
+
firstOwnerUserId: platformOwnerUserIdFrom(owner, recorded),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The platform's operator identity for a company's files host: the owner company's first owner,
|
|
328
|
+
* as the owner's `zitadel:owner` step recorded it in the owner company's own ZITADEL. `'none'`
|
|
329
|
+
* when the owner company declares no owner; `null` until the step has recorded the id, and
|
|
330
|
+
* when there is no owner company running its own services.
|
|
331
|
+
*/
|
|
332
|
+
export function platformOwnerUserIdFrom(owner, recorded) {
|
|
333
|
+
if (!owner || !ownsServices(owner))
|
|
334
|
+
return null;
|
|
335
|
+
const first = owner.owners[0];
|
|
336
|
+
if (!first)
|
|
337
|
+
return 'none';
|
|
338
|
+
return recorded[`zitadel:owner:${owner.id}:${first.email.toLowerCase()}`] || null;
|
|
339
|
+
}
|
|
340
|
+
/** The owner's files host once its own files step has applied; `null` before. */
|
|
341
|
+
export function ownerFilesFrom(owner, recorded) {
|
|
342
|
+
if (!owner || !ownsServices(owner) || !recorded[`coolify:own-files:${owner.id}`])
|
|
343
|
+
return null;
|
|
344
|
+
const internalUrl = recorded[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY];
|
|
345
|
+
const hostname = recorded[PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY];
|
|
346
|
+
if (!internalUrl || !hostname)
|
|
347
|
+
return null;
|
|
348
|
+
return {
|
|
349
|
+
internalUrl,
|
|
350
|
+
audience: `https://${hostname}`,
|
|
351
|
+
organisationId: owner.id,
|
|
352
|
+
managementKeyRef: orgTarget(owner).refs.filesManagement,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
@@ -6,13 +6,17 @@ import { type DeployHealthRuntime } from './deploy-health.js';
|
|
|
6
6
|
import type { StepAction } from './diff.js';
|
|
7
7
|
import type { AccessPort } from './edge-access.js';
|
|
8
8
|
import type { DnsPort } from './edge-dns.js';
|
|
9
|
+
import type { RateLimitPort } from './edge-ratelimits.js';
|
|
9
10
|
import type { TunnelPort } from './edge-tunnels.js';
|
|
11
|
+
import type { ZonePort } from './edge-zones.js';
|
|
10
12
|
import type { CloudflarePort, CoolifyPort, FoundryStepExecutorPorts, ZitadelPort } from './executor.js';
|
|
11
13
|
export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
12
14
|
import type { LiveResource, PlatformCloudflareReader, PlatformCoolifyReader } from './live.js';
|
|
15
|
+
import { type OikosIngressPorts } from './oikos-ingress.js';
|
|
13
16
|
import { type FilesKeysMinted, type ProjectSource, type ServiceNetwork, type ServiceTarget, type StepScope } from './own-services.js';
|
|
14
17
|
import type { PlatformDriftedSteps, PlatformStepExecutionContext, PlatformStepExecutor } from './plan-apply.js';
|
|
15
18
|
import { type BrokerDeployRecord, PLATFORM_ZITADEL_ORG_SCOPE_KEY } from './platform-diff.js';
|
|
19
|
+
import type { PlatformOwnerInput } from './platform-org.js';
|
|
16
20
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
17
21
|
import { type CallsSfuPlatformDeps } from './platform-sfu.js';
|
|
18
22
|
import { type PlatformUptimeDeps } from './uptime-worker.js';
|
|
@@ -64,7 +68,7 @@ export declare function platformTunnels(deps: {
|
|
|
64
68
|
*/
|
|
65
69
|
export type PlatformAccessFactory = (edgeUrl: string) => PlatformAccessPort | Promise<PlatformAccessPort>;
|
|
66
70
|
/** The Access calls a platform step makes. */
|
|
67
|
-
export type PlatformAccessPort = Pick<AccessPort, 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
|
|
71
|
+
export type PlatformAccessPort = Pick<AccessPort, 'listAccessApps' | 'createAccessApp' | 'ensureOneTimePinProvider' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
|
|
68
72
|
/**
|
|
69
73
|
* The SMTP call `provisionPlatformAuth` makes on the instance's ZITADEL.
|
|
70
74
|
* `@wtfalch/providers` gains it in 0.7.2; stated here so foundry compiles
|
|
@@ -108,7 +112,7 @@ export interface PlatformAuthDeps {
|
|
|
108
112
|
tunnels: PlatformTunnelsFactory;
|
|
109
113
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
110
114
|
/** Must target the platform's own new instance (its issuer), or `zitadelForInstance` must be set. */
|
|
111
|
-
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'inviteOrgOwner' | 'addOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp;
|
|
115
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName' | 'closeSelfRegistration' | 'enforceSecondFactor' | 'inviteOrgOwner' | 'addOrgDomain' | 'setPrimaryOrgDomain' | 'setUserMetadata' | 'getUserMetadata'> & PlatformZitadelSmtp;
|
|
112
116
|
/**
|
|
113
117
|
* Mails the owner's first-sign-in link to their personal address (the same
|
|
114
118
|
* port as `FoundryStepExecutorPorts.ownerInviteMail`). The link carries the
|
|
@@ -261,71 +265,12 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
261
265
|
* gets the step reported as blocked.
|
|
262
266
|
*/
|
|
263
267
|
accessProvider?: PlatformAccessProviderDeps;
|
|
268
|
+
/**
|
|
269
|
+
* The shared oikos.host ingress step. Optional like `auth`; with edge enabled and this
|
|
270
|
+
* unset, the step is reported blocked.
|
|
271
|
+
*/
|
|
272
|
+
oikosIngress?: PlatformOikosIngressDeps;
|
|
264
273
|
}
|
|
265
|
-
/**
|
|
266
|
-
* The real {@link PlatformStepExecutor} for the two steps built so far:
|
|
267
|
-
* `cloudflare:zone` for the platform's apex (#167) and `coolify:jobs` for the
|
|
268
|
-
* jobs scheduler (#173). `coolify` is optional so every #167-era caller --
|
|
269
|
-
* anything that only ever plans the apex zone -- keeps constructing this with
|
|
270
|
-
* two arguments; a `coolify:jobs` step reaching this executor with no
|
|
271
|
-
* `coolify` port configured throws, naming what is missing, rather than
|
|
272
|
-
* silently doing nothing.
|
|
273
|
-
*
|
|
274
|
-
* `cloudflareZone` mirrors `executor.ts`'s `cloudflareZone`: find the zone by
|
|
275
|
-
* apex and adopt it, create it only when absent. companyincluded.com already
|
|
276
|
-
* exists on Cloudflare, empty, so the first apply adopts it. The owner org
|
|
277
|
-
* (Company Included, whose own domain is the apex) may record the same zone
|
|
278
|
-
* under its own applied state, so that owner is allowed. Any other org that
|
|
279
|
-
* recorded it makes this refuse to adopt. There is no delete here and
|
|
280
|
-
* `PlatformStepExecutor` has none: neither path ever removes the zone.
|
|
281
|
-
*
|
|
282
|
-
* `coolifyJobs` mirrors `executor.ts`'s `coolifyApp`/`coolifyDatabase`,
|
|
283
|
-
* simplified for one platform-level singleton rather than one per org: find
|
|
284
|
-
* or create a dedicated `platform` Coolify project, find or create the
|
|
285
|
-
* scheduler's own database, find or create the scheduler app itself (from
|
|
286
|
-
* https://github.com/wtfalch/jobs, one Dockerfile at its repo root, on the
|
|
287
|
-
* declared `gitRef`), then push the database's `internal_db_url` in as
|
|
288
|
-
* `DATABASE_URL`. There is no park branch yet, matching `planJobs`'s own
|
|
289
|
-
* scope note -- `platform.jobs.enabled` going from true to false plans
|
|
290
|
-
* nothing, so this method is never asked to stop or delete anything.
|
|
291
|
-
*
|
|
292
|
-
* #170 adds `sfu:livekit`, the platform's shared calls SFU. `sfu` is
|
|
293
|
-
* optional: an existing host that constructs this executor for the zone step
|
|
294
|
-
* alone keeps compiling, and a plan that includes an `sfu:livekit` step
|
|
295
|
-
* without it configured refuses clearly rather than silently doing nothing.
|
|
296
|
-
* #152: the step mints one LiveKit key per org with calls enabled rather
|
|
297
|
-
* than one shared pair, so it also needs `declaredOrgStore.list()` -- the
|
|
298
|
-
* same store this class already takes for `findOrgIdForZone`.
|
|
299
|
-
*
|
|
300
|
-
* #168 adds `coolify:app` keyed {@link PLATFORM_AUTH_KEY}, the platform's own
|
|
301
|
-
* sign-in ({@link provisionPlatformAuth}), once a host passes
|
|
302
|
-
* {@link PlatformAuthDeps}.
|
|
303
|
-
*
|
|
304
|
-
* #169 adds `coolify:app` keyed {@link PLATFORM_MAIL_KEY}, the platform's own
|
|
305
|
-
* mail server ({@link provisionPlatformMail}), once a host passes
|
|
306
|
-
* {@link PlatformMailDeps}.
|
|
307
|
-
*
|
|
308
|
-
* #174 adds `coolify:app` keyed {@link PLATFORM_INTEGRATIONS_KEY},
|
|
309
|
-
* the integrations host ({@link provisionPlatformIntegrations}), once a host
|
|
310
|
-
* passes {@link PlatformIntegrationsDeps}.
|
|
311
|
-
*
|
|
312
|
-
* #171 adds `coolify:app` keyed {@link PLATFORM_FILES_KEY}, the files host
|
|
313
|
-
* ({@link provisionPlatformFiles}), once a host passes {@link PlatformFilesDeps}.
|
|
314
|
-
*
|
|
315
|
-
* #172 adds `coolify:app` keyed {@link PLATFORM_AI_KEY}, the platform's AI
|
|
316
|
-
* host ({@link provisionPlatformAi}), once a host passes {@link PlatformAiDeps}.
|
|
317
|
-
*
|
|
318
|
-
* #478 adds `coolify:app` keyed {@link PLATFORM_EDGE_KEY}, the edge host
|
|
319
|
-
* ({@link provisionPlatformEdge}), once a host passes {@link PlatformEdgeDeps}.
|
|
320
|
-
*
|
|
321
|
-
* #175 adds `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, Archon itself
|
|
322
|
-
* ({@link provisionPlatformArchon}), once a host passes
|
|
323
|
-
* {@link PlatformArchonDeps}.
|
|
324
|
-
*
|
|
325
|
-
* #257 adds `coolify:backup:<service>`, a nightly R2 backup of each
|
|
326
|
-
* platform database ({@link provisionPlatformBackup}), once a host passes
|
|
327
|
-
* {@link PlatformBackupDeps}.
|
|
328
|
-
*/
|
|
329
274
|
export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor {
|
|
330
275
|
private readonly cloudflare;
|
|
331
276
|
private readonly declaredOrgStore;
|
|
@@ -345,8 +290,14 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
345
290
|
private readonly heartbeats?;
|
|
346
291
|
private readonly uptime?;
|
|
347
292
|
private readonly accessProvider?;
|
|
293
|
+
private readonly oikosIngress?;
|
|
348
294
|
constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list' | 'get'>, options?: FoundryPlatformStepExecutorOptions);
|
|
349
|
-
|
|
295
|
+
private readonly platformPorts;
|
|
296
|
+
/** The port placed on the platform's destination, made once per port so a server is listed once. */
|
|
297
|
+
private onPlatform;
|
|
298
|
+
/** `deps` with its Coolify port placed on the platform destination, then limited by `profile`. */
|
|
299
|
+
private limitedOnPlatform;
|
|
300
|
+
blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>, owner?: PlatformOwnerInput): Promise<{
|
|
350
301
|
auth?: string;
|
|
351
302
|
authExecutorMissing?: boolean;
|
|
352
303
|
authOtherInputsMissing?: string[];
|
|
@@ -359,6 +310,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
359
310
|
mail?: string;
|
|
360
311
|
relay?: string;
|
|
361
312
|
accessProvider?: string;
|
|
313
|
+
oikosIngress?: string;
|
|
362
314
|
backupExecutorMissing?: boolean;
|
|
363
315
|
hc?: string;
|
|
364
316
|
heartbeatsExecutorMissing?: boolean;
|
|
@@ -374,6 +326,8 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
374
326
|
archonDeployRunning?: string;
|
|
375
327
|
archonIdTokenUserinfo?: boolean;
|
|
376
328
|
authSignInPolicy?: boolean;
|
|
329
|
+
archonOrg?: string;
|
|
330
|
+
archonOrgInvite?: string;
|
|
377
331
|
}>;
|
|
378
332
|
/**
|
|
379
333
|
* foundry#542: why the uptime Worker cannot run yet, which optional secrets
|
|
@@ -390,7 +344,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
390
344
|
* login-client token is no longer the sole live PAT on its ZITADEL machine
|
|
391
345
|
* user.
|
|
392
346
|
*/
|
|
393
|
-
driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string
|
|
347
|
+
driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>, owner?: PlatformOwnerInput): Promise<PlatformDriftedSteps>;
|
|
394
348
|
/**
|
|
395
349
|
* Gives the files runner a changed share-domain set and nothing else: its
|
|
396
350
|
* Coolify `domains`, `FILES_PUBLIC_HOSTS` and one redeploy of the runner.
|
|
@@ -578,6 +532,9 @@ export interface AuthStackInput {
|
|
|
578
532
|
export declare function provisionAuthStack(scope: StepScope, deps: PlatformAuthDeps, target: ServiceTarget, input: AuthStackInput): Promise<{
|
|
579
533
|
resourceId?: string;
|
|
580
534
|
}>;
|
|
535
|
+
export declare const PLATFORM_MAIL_INTERNAL_NAME = "platform-mail-stalwart";
|
|
536
|
+
/** Where the server the app was created on is recorded, so the MX record follows the app, not a later pick. */
|
|
537
|
+
export declare const PLATFORM_MAIL_SERVER_SCOPE_KEY = "coolify_platform_mail_server";
|
|
581
538
|
/**
|
|
582
539
|
* The fixed description Stalwart's admin API key is minted under
|
|
583
540
|
* (`StalwartClient.createApiKey`'s own signature returns only the secret,
|
|
@@ -653,7 +610,7 @@ export declare function defaultStalwartForInstance(input: {
|
|
|
653
610
|
* own shape.
|
|
654
611
|
*/
|
|
655
612
|
export interface PlatformMailDeps {
|
|
656
|
-
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
|
|
613
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'restart' | 'getDeployment'>;
|
|
657
614
|
/** F4.dns: every DNS record call, through edge at its recorded address (see {@link PlatformDnsFactory}). */
|
|
658
615
|
dns: PlatformDnsFactory;
|
|
659
616
|
/** F4.tunnels: every tunnel call, through edge at its recorded address (see {@link PlatformTunnelsFactory}). */
|
|
@@ -706,6 +663,8 @@ export interface MailServerInput {
|
|
|
706
663
|
hostname: string;
|
|
707
664
|
/** The domain the server's admin account and default domain belong to. */
|
|
708
665
|
apex: string;
|
|
666
|
+
/** The Cloudflare zone `hostname` goes in. Absent: `apex`. A company without a domain has mail domain `<slug>.oikos.host` in zone `oikos.host`. */
|
|
667
|
+
zone?: string;
|
|
709
668
|
/** The MX name: the A record `mxRecord` publishes. The server announces `hostname`, not this. */
|
|
710
669
|
mailHost: string;
|
|
711
670
|
/** The Stalwart image tag. */
|
|
@@ -789,6 +748,20 @@ export interface MailServerInput {
|
|
|
789
748
|
export declare function provisionMailServer(scope: StepScope, deps: PlatformMailDeps, target: ServiceTarget, input: MailServerInput): Promise<{
|
|
790
749
|
resourceId?: string;
|
|
791
750
|
}>;
|
|
751
|
+
/**
|
|
752
|
+
* The shared oikos.host ingress step's ports: its Coolify calls, and the tunnel, DNS,
|
|
753
|
+
* rate limit and zone calls, which go through edge at its recorded address like every
|
|
754
|
+
* other platform step's.
|
|
755
|
+
*/
|
|
756
|
+
export interface PlatformOikosIngressDeps {
|
|
757
|
+
coolify: OikosIngressPorts['coolify'] & Pick<CoolifyPort, 'listServers'>;
|
|
758
|
+
dns: PlatformDnsFactory;
|
|
759
|
+
tunnels: PlatformTunnelsFactory;
|
|
760
|
+
ratelimits: (edgeUrl: string) => RateLimitPort | Promise<RateLimitPort>;
|
|
761
|
+
zones: (edgeUrl: string) => Pick<ZonePort, 'findZoneByApex'> | Promise<Pick<ZonePort, 'findZoneByApex'>>;
|
|
762
|
+
/** One outside call's limit in milliseconds; 60 s by default. */
|
|
763
|
+
callTimeoutMs?: number;
|
|
764
|
+
}
|
|
792
765
|
/**
|
|
793
766
|
* #211: the broker cannot start without `ZITADEL_SERVICE_USER_TOKEN`/`_FILE`,
|
|
794
767
|
* `LETTERMINT_API_TOKEN` and `AUTH_APPS`. `provisionPlatformAuth` already
|
|
@@ -843,8 +816,12 @@ export interface PlatformArchonDeps {
|
|
|
843
816
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
844
817
|
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
845
818
|
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'>;
|
|
846
|
-
/**
|
|
847
|
-
|
|
819
|
+
/**
|
|
820
|
+
* {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client:
|
|
821
|
+
* the owner company's own ZITADEL, by its issuer and the held-key reference of
|
|
822
|
+
* its instance bearer. Required to apply Archon; `zitadel` is never used for it.
|
|
823
|
+
*/
|
|
824
|
+
zitadelForInstance?: (issuerUrl: string, bearerRef?: string) => PlatformArchonDeps['zitadel'];
|
|
848
825
|
/**
|
|
849
826
|
* Who the Cloudflare Access app in front of Archon lets through. Host
|
|
850
827
|
* config, not `DeclaredPlatform`: the platform record edge services, not
|
|
@@ -912,6 +889,21 @@ export interface PlatformArchonRuntime {
|
|
|
912
889
|
* org (`foundry:<orgId>`), with the platform's step key in the org id's place.
|
|
913
890
|
*/
|
|
914
891
|
export declare const PLATFORM_ARCHON_ACCESS_APP_NAME = "foundry:platform:archon";
|
|
892
|
+
/**
|
|
893
|
+
* Archon paths a machine calls and Archon authenticates itself (its `proxy.ts`:
|
|
894
|
+
* `PUBLIC_EXACT` and `ORG_CHANGES_PATH_RE`). Each gets its own Access app with
|
|
895
|
+
* one bypass policy, because the hostname app would otherwise ask a machine for
|
|
896
|
+
* an emailed code. `/api/uptime/targets` and `/api/uptime/results` are left out
|
|
897
|
+
* on purpose: they pass the hostname app through the uptime service-token
|
|
898
|
+
* policy, and a bypass would make that token meaningless. `*` is one path
|
|
899
|
+
* segment (the organisation id).
|
|
900
|
+
*/
|
|
901
|
+
export declare const PLATFORM_ARCHON_PUBLIC_PATHS: ReadonlyArray<{
|
|
902
|
+
name: string;
|
|
903
|
+
path: string;
|
|
904
|
+
}>;
|
|
905
|
+
/** The scope key one bypass app's id is recorded under. */
|
|
906
|
+
export declare const archonPublicAccessScopeKey: (name: string) => string;
|
|
915
907
|
/** Archon's OIDC client id: public, not a secret, so a plain scope id. */
|
|
916
908
|
export declare const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = "zitadel_platform_archon_client_id";
|
|
917
909
|
/**
|
|
@@ -948,6 +940,24 @@ type BrokerAppEntry = Record<string, unknown> & {
|
|
|
948
940
|
* `TypeError` instead of this named error.
|
|
949
941
|
*/
|
|
950
942
|
export declare function validateBrokerAppEntry(entry: unknown, brokerAppId: string, index: number): BrokerAppEntry;
|
|
943
|
+
/**
|
|
944
|
+
* Who Archon's Access app lets through. Each person is let in on the personal
|
|
945
|
+
* address their invite was mailed to, never on a work address: the work
|
|
946
|
+
* mailbox is on the platform's own mail, which the platform's own sign-in
|
|
947
|
+
* reaches. So the owner company's first owner's work addresses leave the host's
|
|
948
|
+
* list and their personal address joins it; every other address stays as the
|
|
949
|
+
* host gave it. Case-insensitive, de-duplicated, order kept.
|
|
950
|
+
*/
|
|
951
|
+
export declare function archonAccessEmails(hostEmails: readonly string[], ownerCompany: DeclaredOrg | null): string[];
|
|
952
|
+
/**
|
|
953
|
+
* The names of the broker entries that sign in Archon's organisation without
|
|
954
|
+
* being Archon's own. There must be none: a key of another app would then
|
|
955
|
+
* reach Archon's people (passkeys, password reset, deactivation).
|
|
956
|
+
*/
|
|
957
|
+
export declare function archonOrgConflicts(entries: ReadonlyArray<{
|
|
958
|
+
name: string;
|
|
959
|
+
organizationId: string;
|
|
960
|
+
}>, archonOrgId: string | undefined): string[];
|
|
951
961
|
/**
|
|
952
962
|
* #172/#201: mints Archon's own AI management key -- the plain bearer key
|
|
953
963
|
* (`wtfk_...`) `AiOrganisationPort` (`executor.ts`) authenticates with,
|
|
@@ -1018,7 +1028,11 @@ export declare class AiManagementKeyUntrustedError extends Error {
|
|
|
1018
1028
|
}
|
|
1019
1029
|
/** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
|
|
1020
1030
|
export interface PlatformAiDeps {
|
|
1021
|
-
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
|
|
1031
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
|
|
1032
|
+
/** F4.dns: the DNS record of the AI hostname, through edge (see {@link PlatformDnsFactory}). */
|
|
1033
|
+
dns: PlatformDnsFactory;
|
|
1034
|
+
/** F4.tunnels: the AI hostname's tunnel, through edge (see {@link PlatformTunnelsFactory}). */
|
|
1035
|
+
tunnels: PlatformTunnelsFactory;
|
|
1022
1036
|
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
1023
1037
|
/**
|
|
1024
1038
|
* Reads back a secret this step delivered on an earlier apply -- needed
|
|
@@ -1381,13 +1395,21 @@ export interface PlatformAccessProviderDeps {
|
|
|
1381
1395
|
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
1382
1396
|
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'listApps' | 'deleteApp'> & Required<Pick<ZitadelPort, 'createOidcAppWithSecret'>>;
|
|
1383
1397
|
/** {@link PlatformArchonDeps.zitadelForInstance}: the ZITADEL client for the sign-in at `issuerUrl`. */
|
|
1384
|
-
zitadelForInstance?: (issuerUrl: string) => PlatformAccessProviderDeps['zitadel'];
|
|
1398
|
+
zitadelForInstance?: (issuerUrl: string, bearerRef?: string) => PlatformAccessProviderDeps['zitadel'];
|
|
1385
1399
|
/** Sleep and delivery tries for the one call that carries the secret; real ones by default. */
|
|
1386
1400
|
runtime?: {
|
|
1387
1401
|
sleep?: (ms: number) => Promise<void>;
|
|
1388
1402
|
deliverAttempts?: number;
|
|
1389
1403
|
};
|
|
1390
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* The only paths the AI host's public route serves; everything else answers 404 at the tunnel. A company entry's key
|
|
1407
|
+
* holds `ai.runs:create` and `ai.offerings:invoke` (`AI_ENTRY_PERMISSIONS`), and `@wtfalch/ai`'s client reaches them
|
|
1408
|
+
* through `POST /v1/runs` (also `?wait=`), `POST /v1/runs/batch`, `GET /v1/runs`, `GET /v1/runs/:id` and
|
|
1409
|
+
* `GET /v1/offerings`. The management routes (`/v1/organisations`, `/v1/keys`, `/v1/budgets`, `/v1/webhooks`,
|
|
1410
|
+
* `/v1/connections`, `/v1/platform/keys`, ...) and `/api/health` are not on the internet.
|
|
1411
|
+
*/
|
|
1412
|
+
export declare const AI_ENTRY_PATH_PATTERN = "^/v1/(runs(/|$)|offerings$)";
|
|
1391
1413
|
export interface PlatformHcRuntime {
|
|
1392
1414
|
fetch?: typeof fetch;
|
|
1393
1415
|
sleep?: (ms: number) => Promise<void>;
|
|
@@ -1467,6 +1489,8 @@ export declare const INTEGRATIONS_MANAGEMENT_ISSUER = "foundry";
|
|
|
1467
1489
|
* its signed request; fixed, since there is one key until rotation is built.
|
|
1468
1490
|
*/
|
|
1469
1491
|
export declare const INTEGRATIONS_MANAGEMENT_KEY_ID = "foundry-1";
|
|
1492
|
+
/** The uuid `provisionPlatformBackup` records once {@link PLATFORM_BACKUP_S3_STORAGE_NAME} is registered on Coolify, so a later apply never re-registers it. */
|
|
1493
|
+
export declare const PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY = "coolify_platform_backup_s3_storage";
|
|
1470
1494
|
/**
|
|
1471
1495
|
* #257: everything {@link provisionPlatformBackup} needs beyond
|
|
1472
1496
|
* {@link PlatformStepExecutionContext} -- the narrow Coolify slice for the
|
|
@@ -1500,6 +1524,27 @@ export interface PlatformBackupDeps {
|
|
|
1500
1524
|
/** A cron string or one of Coolify's names (`daily`, `weekly`, ...). Default `'daily'`. */
|
|
1501
1525
|
frequency?: string;
|
|
1502
1526
|
}
|
|
1527
|
+
/**
|
|
1528
|
+
* F6: one nightly schedule of one database to a registered S3 storage, the
|
|
1529
|
+
* body `provisionPlatformBackup` had and every company database now shares.
|
|
1530
|
+
* An existing enabled, `save_s3` schedule is reused, never doubled: the lookup
|
|
1531
|
+
* runs before the create, so a run that created a schedule and died before
|
|
1532
|
+
* the caller recorded its uuid finds it again on the next apply. The
|
|
1533
|
+
* `PATCH` names the database to dump, because `createDatabaseBackup` takes no
|
|
1534
|
+
* database name; it also repairs an existing schedule that dumps the wrong
|
|
1535
|
+
* one. Returns the schedule's uuid.
|
|
1536
|
+
*/
|
|
1537
|
+
export declare function provisionDatabaseBackup(coolify: Pick<CoolifyPort, 'listDatabaseBackups' | 'createDatabaseBackup' | 'raw'>, input: {
|
|
1538
|
+
databaseUuid: string;
|
|
1539
|
+
/** Names the database in an error, e.g. `platform jobs`. Never a URL or a secret. */
|
|
1540
|
+
label: string;
|
|
1541
|
+
storageUuid: string;
|
|
1542
|
+
frequency?: string;
|
|
1543
|
+
/** The database to dump. Absent: the resource's first database. */
|
|
1544
|
+
databaseName?: string;
|
|
1545
|
+
}): Promise<{
|
|
1546
|
+
uuid: string;
|
|
1547
|
+
}>;
|
|
1503
1548
|
/** One check as self-hosted Healthchecks' own Management API reports it. */
|
|
1504
1549
|
export interface HeartbeatCheck {
|
|
1505
1550
|
id: string;
|