@wtfalch/foundry 0.34.0 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +1 -1
  2. package/dist/bootstrap/bootstrap.js +49 -11
  3. package/dist/bootstrap/declare.d.ts +12 -5
  4. package/dist/bootstrap/declare.js +33 -18
  5. package/dist/bootstrap/versions.d.ts +20 -0
  6. package/dist/bootstrap/versions.js +46 -0
  7. package/dist/calls-sfu.js +3 -3
  8. package/dist/index.d.ts +9 -4
  9. package/dist/index.js +5 -2
  10. package/dist/migrations/0053_heartbeats_provider.sql +9 -0
  11. package/dist/migrations/0053_org_version_pin.sql +6 -0
  12. package/dist/org.d.ts +60 -14
  13. package/dist/org.js +118 -28
  14. package/dist/plan/backup-gate.d.ts +93 -0
  15. package/dist/plan/backup-gate.js +261 -0
  16. package/dist/plan/broker-entries.d.ts +9 -1
  17. package/dist/plan/broker-entries.js +20 -3
  18. package/dist/plan/build-record.d.ts +56 -0
  19. package/dist/plan/build-record.js +109 -0
  20. package/dist/plan/calls-matrix.js +3 -3
  21. package/dist/plan/cloudflared-connector.js +2 -2
  22. package/dist/plan/company-backups.d.ts +31 -0
  23. package/dist/plan/company-backups.js +101 -0
  24. package/dist/plan/deploy-health.d.ts +12 -2
  25. package/dist/plan/deploy-health.js +3 -1
  26. package/dist/plan/destinations.d.ts +73 -0
  27. package/dist/plan/destinations.js +217 -0
  28. package/dist/plan/diff.d.ts +17 -8
  29. package/dist/plan/diff.js +131 -131
  30. package/dist/plan/edge-access.d.ts +15 -0
  31. package/dist/plan/edge-access.js +21 -2
  32. package/dist/plan/executor.d.ts +166 -7
  33. package/dist/plan/executor.js +547 -106
  34. package/dist/plan/health-targets.d.ts +1 -3
  35. package/dist/plan/health-targets.js +36 -36
  36. package/dist/plan/held-key-refs.d.ts +5 -0
  37. package/dist/plan/held-key-refs.js +5 -0
  38. package/dist/plan/hostnames.d.ts +10 -0
  39. package/dist/plan/hostnames.js +14 -0
  40. package/dist/plan/index.d.ts +5 -0
  41. package/dist/plan/index.js +3 -0
  42. package/dist/plan/live.d.ts +6 -0
  43. package/dist/plan/live.js +3 -2
  44. package/dist/plan/locked-hostnames.d.ts +2 -1
  45. package/dist/plan/locked-hostnames.js +16 -3
  46. package/dist/plan/login-host.d.ts +9 -20
  47. package/dist/plan/login-host.js +10 -25
  48. package/dist/plan/mail-relay.d.ts +48 -29
  49. package/dist/plan/mail-relay.js +145 -68
  50. package/dist/plan/network-move.d.ts +20 -0
  51. package/dist/plan/network-move.js +41 -0
  52. package/dist/plan/oikos-ingress.d.ts +111 -0
  53. package/dist/plan/oikos-ingress.js +231 -0
  54. package/dist/plan/org-names.d.ts +8 -2
  55. package/dist/plan/org-names.js +57 -1
  56. package/dist/plan/own-diff.d.ts +9 -8
  57. package/dist/plan/own-diff.js +42 -34
  58. package/dist/plan/own-hostnames.d.ts +27 -0
  59. package/dist/plan/own-hostnames.js +24 -0
  60. package/dist/plan/own-limits.d.ts +198 -7
  61. package/dist/plan/own-limits.js +163 -32
  62. package/dist/plan/own-services.js +3 -1
  63. package/dist/plan/own-sfu.js +3 -2
  64. package/dist/plan/plan-apply.d.ts +47 -7
  65. package/dist/plan/plan-apply.js +184 -38
  66. package/dist/plan/platform-diff.d.ts +133 -17
  67. package/dist/plan/platform-diff.js +231 -68
  68. package/dist/plan/platform-org.d.ts +79 -10
  69. package/dist/plan/platform-org.js +93 -13
  70. package/dist/plan/platform-providers.d.ts +118 -73
  71. package/dist/plan/platform-providers.js +770 -231
  72. package/dist/plan/proxy-labels.d.ts +57 -11
  73. package/dist/plan/proxy-labels.js +94 -14
  74. package/dist/plan/run-options.d.ts +19 -0
  75. package/dist/plan/run-options.js +46 -0
  76. package/dist/plan/zitadel-login-policy.d.ts +1 -1
  77. package/dist/plan/zitadel-login-policy.js +76 -8
  78. package/dist/platform.d.ts +32 -0
  79. package/dist/platform.js +16 -0
  80. package/dist/provision.d.ts +2 -0
  81. package/dist/provision.js +3 -1
  82. package/dist/slug.d.ts +3 -1
  83. package/dist/slug.js +25 -3
  84. package/dist/store/postgres.js +43 -1
  85. package/dist/store/schema.d.ts +36 -0
  86. package/dist/store/schema.js +4 -0
  87. package/package.json +3 -3
  88. 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, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_EDGE_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';
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
- aiUrl: aiApplied ? (platformRecordedIds[PLATFORM_AI_INTERNAL_URL_SCOPE_KEY] ?? null) : null,
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 && org.domain ? `https://auth.${org.domain}` : null,
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
- mailHost: mailApplied && org.domain && base.mailHost
212
- ? { apex: org.domain, mailHost: base.mailHost.mailHost }
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 && org.domain ? `https://auth.${org.domain}` : null,
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
- blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
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>>): Promise<PlatformDriftedSteps>;
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
- /** {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client. */
847
- zitadelForInstance?: (issuerUrl: string) => PlatformArchonDeps['zitadel'];
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;