@wtfalch/foundry 0.32.0 → 0.34.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.
Files changed (74) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +18 -1
  2. package/dist/bootstrap/bootstrap.js +167 -28
  3. package/dist/bootstrap/declare.d.ts +5 -1
  4. package/dist/bootstrap/declare.js +3 -1
  5. package/dist/bootstrap/index.d.ts +1 -1
  6. package/dist/bootstrap/versions.d.ts +4 -0
  7. package/dist/calls-sfu.d.ts +10 -0
  8. package/dist/calls-sfu.js +41 -0
  9. package/dist/index.d.ts +18 -8
  10. package/dist/index.js +8 -3
  11. package/dist/migrations/0051_org_services.sql +13 -0
  12. package/dist/migrations/0052_platform_mail_relay.sql +8 -0
  13. package/dist/org.d.ts +24 -2
  14. package/dist/org.js +42 -0
  15. package/dist/plan/boule.d.ts +24 -1
  16. package/dist/plan/boule.js +29 -1
  17. package/dist/plan/calls-matrix.d.ts +9 -0
  18. package/dist/plan/calls-matrix.js +17 -0
  19. package/dist/plan/cloudflared-connector.d.ts +7 -1
  20. package/dist/plan/cloudflared-connector.js +14 -1
  21. package/dist/plan/diff.d.ts +32 -1
  22. package/dist/plan/diff.js +383 -26
  23. package/dist/plan/executor.d.ts +177 -7
  24. package/dist/plan/executor.js +738 -51
  25. package/dist/plan/health-check-settings.d.ts +28 -0
  26. package/dist/plan/health-check-settings.js +36 -0
  27. package/dist/plan/health-gaps.d.ts +33 -0
  28. package/dist/plan/health-gaps.js +89 -0
  29. package/dist/plan/health-targets.js +1 -1
  30. package/dist/plan/held-key-refs.d.ts +10 -0
  31. package/dist/plan/held-key-refs.js +10 -0
  32. package/dist/plan/index.d.ts +16 -6
  33. package/dist/plan/index.js +8 -3
  34. package/dist/plan/live.d.ts +5 -2
  35. package/dist/plan/live.js +15 -2
  36. package/dist/plan/locked-hostnames.d.ts +5 -2
  37. package/dist/plan/locked-hostnames.js +9 -1
  38. package/dist/plan/login-host.d.ts +62 -2
  39. package/dist/plan/login-host.js +104 -1
  40. package/dist/plan/mail-relay.d.ts +120 -0
  41. package/dist/plan/mail-relay.js +323 -0
  42. package/dist/plan/org-names.d.ts +24 -0
  43. package/dist/plan/org-names.js +52 -0
  44. package/dist/plan/own-diff.d.ts +169 -0
  45. package/dist/plan/own-diff.js +432 -0
  46. package/dist/plan/own-dns.d.ts +6 -0
  47. package/dist/plan/own-dns.js +15 -0
  48. package/dist/plan/own-limits.d.ts +22 -0
  49. package/dist/plan/own-limits.js +101 -0
  50. package/dist/plan/own-services.d.ts +193 -0
  51. package/dist/plan/own-services.js +167 -0
  52. package/dist/plan/own-sfu.d.ts +69 -0
  53. package/dist/plan/own-sfu.js +255 -0
  54. package/dist/plan/plan-apply.d.ts +101 -7
  55. package/dist/plan/plan-apply.js +216 -12
  56. package/dist/plan/platform-diff.d.ts +61 -12
  57. package/dist/plan/platform-diff.js +140 -34
  58. package/dist/plan/platform-org.d.ts +36 -0
  59. package/dist/plan/platform-org.js +96 -2
  60. package/dist/plan/platform-providers.d.ts +407 -12
  61. package/dist/plan/platform-providers.js +947 -422
  62. package/dist/plan/sign-in-org.d.ts +99 -0
  63. package/dist/plan/sign-in-org.js +151 -0
  64. package/dist/plan/zitadel-login-policy.js +1 -1
  65. package/dist/plan/zitadel-system-api.d.ts +22 -1
  66. package/dist/plan/zitadel-system-api.js +49 -8
  67. package/dist/plan.d.ts +18 -0
  68. package/dist/platform.d.ts +8 -0
  69. package/dist/slug.js +1 -0
  70. package/dist/store/postgres.js +51 -2
  71. package/dist/store/schema.d.ts +51 -0
  72. package/dist/store/schema.js +5 -0
  73. package/package.json +1 -1
  74. package/versions.json +3 -1
@@ -1,5 +1,5 @@
1
1
  import type { Plan } from '../plan.js';
2
- import type { PlatformLiveReaders, PlatformStepExecutor, StepSystem } from '../plan/index.js';
2
+ import type { FoundryPlanApplyOptions, LiveReaders, PlatformLiveReaders, PlatformStepExecutor, StepExecutor, StepSystem } from '../plan/index.js';
3
3
  import type { FoundryPlatformStepExecutorOptions } from '../plan/platform-providers.js';
4
4
  import type { FoundryStoreDb } from '../store/postgres.js';
5
5
  import type { BootstrapCompany, BootstrapOwner } from './declare.js';
@@ -38,6 +38,17 @@ export interface BootstrapProgress {
38
38
  }
39
39
  /** Everything foundry's per-step executors take besides the two clients and `deliverSecret`. */
40
40
  export type BootstrapExecutorDeps = Omit<FoundryPlatformStepExecutorOptions, 'coolify' | 'deliverSecret'>;
41
+ /**
42
+ * What phase two needs to apply the platform's own org (Company Included):
43
+ * the org-side readers and step executor a host already builds for any org.
44
+ * Without it the run applies the platform only.
45
+ */
46
+ export interface BootstrapCompanyDeps {
47
+ readers: LiveReaders;
48
+ executor: StepExecutor;
49
+ /** The same flags `FoundryPlanApply` takes: a step whose port the host did not build is reported blocked. */
50
+ options?: Pick<FoundryPlanApplyOptions, 'ownerInviteMail' | 'broker'>;
51
+ }
41
52
  export interface BootstrapPlatformDeps {
42
53
  /** Foundry's tables live here, already migrated by the host; the store, the audit log and the plan all use it. */
43
54
  db: FoundryStoreDb;
@@ -54,6 +65,12 @@ export interface BootstrapPlatformDeps {
54
65
  };
55
66
  /** Per-step dependencies (auth, mail, files, ai, integrations, archon, sfu). A step without its own is reported blocked. */
56
67
  executor?: BootstrapExecutorDeps;
68
+ /**
69
+ * Phase two: after the platform, apply the platform's own org's database,
70
+ * sign-in client, owner and Boule, then the platform once more so Archon
71
+ * picks up the keys that slice produced. Absent: platform only.
72
+ */
73
+ company?: BootstrapCompanyDeps;
57
74
  /**
58
75
  * Passed to `createPostgresAuditLog`. Omit it in a real deployment: `db` is
59
76
  * the runtime role the host made with `ensureRuntimeRole`, and the default
@@ -1,9 +1,12 @@
1
1
  import { CloudflareClient } from '@wtfalch/providers/cloudflare';
2
2
  import { CoolifyClient } from '@wtfalch/providers/coolify';
3
+ import { validateMail } from '../org.js';
4
+ import { BOULE_BROKER_ENTRY_KEY } from '../plan/boule.js';
5
+ import { brokerEntryAppliedScopeKey } from '../plan/broker-entries.js';
3
6
  import { FoundryPlanApply, FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, } from '../plan/index.js';
4
7
  import { createPostgresAuditLog } from '../store/audit.js';
5
8
  import { createPostgresDeclaredOrgStore, createPostgresPlatformStore } from '../store/postgres.js';
6
- import { COMPANY_INCLUDED_ORG_ID, declaredCompanyOrg, declaredPlatformFor, ownerIdentity, } from './declare.js';
9
+ import { COMPANY_INCLUDED_ORG_ID, declaredCompanyOrg, declaredPlatformFor } from './declare.js';
7
10
  import { driveApply } from './drive.js';
8
11
  const REDACTED = '<redacted>';
9
12
  /** Shorter strings in `deps.executor` are settings ("main", ports), not secrets, and would mangle every message. */
@@ -41,17 +44,47 @@ export class Redactor {
41
44
  return out;
42
45
  }
43
46
  }
44
- const allowPlatformApply = {
45
- async canApply() {
46
- return false;
47
- },
48
- async canDestroy() {
49
- return false;
50
- },
51
- async canApplyPlatform() {
52
- return true;
53
- },
47
+ /** The platform, and the platform's own org when the host gave phase two its dependencies. */
48
+ function allowBootstrapApply(withCompany) {
49
+ return {
50
+ async canApply(_actor, orgId) {
51
+ return withCompany && orgId === COMPANY_INCLUDED_ORG_ID;
52
+ },
53
+ async canDestroy() {
54
+ return false;
55
+ },
56
+ async canApplyPlatform() {
57
+ return true;
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * The org steps phase two applies: the sign-in org and its domain, the owner
63
+ * (whose user id Boule's `AUTHZ_FIRST_OWNER` carries), Boule's sign-in client,
64
+ * and Boule with its database. Everything else the company declares (chat,
65
+ * mail, Boule's broker entry, which waits for the org's mail domain, ...) is
66
+ * for a later, ordinary org apply.
67
+ */
68
+ const COMPANY_SLICE_STEPS = new Set([
69
+ 'zitadel:issuer',
70
+ 'zitadel:registration-closed',
71
+ 'zitadel:domain',
72
+ 'zitadel:owner',
73
+ 'zitadel:app:boule',
74
+ 'coolify:boule',
75
+ ]);
76
+ const companySliceOnly = {
77
+ step: (action) => COMPANY_SLICE_STEPS.has(`${action.system}:${action.kind}`) ||
78
+ COMPANY_SLICE_STEPS.has(`${action.system}:${action.kind}:${action.key}`),
79
+ // `coolify:boule:mail` only says Boule runs without mail for now.
80
+ blocked: (key) => key !== 'coolify:boule:mail' &&
81
+ [...COMPANY_SLICE_STEPS].some((step) => key === step || key.startsWith(`${step}:`)),
54
82
  };
83
+ /** Shown instead of a phase-two error that could carry a value; the failing step's own name and redacted text are in `failed`. */
84
+ const COMPANY_NOT_FINISHED = "the company's own slice did not finish, so Archon has no people-store keys yet; run onboarding again";
85
+ /** The step key and fixed text of the blocked item for Boule's missing broker entry. */
86
+ const BOULE_BROKER_ENTRY_BLOCKED_STEP = 'coolify:boule-broker-entry';
87
+ const BOULE_SIGN_IN_BLOCKED = "Boule is deployed but nobody can sign in to it yet: its broker entry waits for the company's mail domain, which the company's mail step records. Apply the company's mail, then its Boule broker entry (an ordinary org apply)";
55
88
  /** `FoundryPlanApply` needs org-scoped parts this run never uses; each refuses if called. */
56
89
  const unusedOrgExecutor = {
57
90
  async execute() {
@@ -68,6 +101,25 @@ export function roundSystem(plan) {
68
101
  return systems.size === 1 && only ? only : 'foundry';
69
102
  }
70
103
  const unusedOrgReader = { listResources: async () => [] };
104
+ /** Counts the org steps a round starts and ends, as the platform executor is wrapped below. */
105
+ function trackedOrgExecutor(inner, progress, redactor) {
106
+ return {
107
+ execute: async (action, context) => {
108
+ const p = progress();
109
+ p.started++;
110
+ try {
111
+ const result = await inner.execute(action, context);
112
+ p.completed++;
113
+ return result;
114
+ }
115
+ finally {
116
+ // After a failure too: the secrets it minted before failing must be masked in its error.
117
+ redactor.add(...(inner.secretsHandled?.() ?? []));
118
+ }
119
+ },
120
+ ...(inner.ownEdgePorts ? { ownEdgePorts: () => inner.ownEdgePorts?.() ?? {} } : {}),
121
+ };
122
+ }
71
123
  /** The audit log with every free-text field run through the redactor before it is stored. */
72
124
  function redactingAudit(inner, redactor) {
73
125
  const plan = (p) => ({
@@ -115,6 +167,16 @@ export function guardStepDeps(executor, redactor, deliverSecret, heldKeys) {
115
167
  const copy = { ...deps };
116
168
  if ('deliverSecret' in copy)
117
169
  copy.deliverSecret = deliverSecret;
170
+ const inviteMail = copy.ownerInviteMail;
171
+ if (inviteMail) {
172
+ // The link carries the invite code; both are masked from the first moment they exist.
173
+ copy.ownerInviteMail = {
174
+ send: (to, link) => {
175
+ redactor.add(link, new URL(link).searchParams.get('code'));
176
+ return inviteMail.send(to, link);
177
+ },
178
+ };
179
+ }
118
180
  // #201: ai's own default minter needs heldKeys too, to re-sign a mint
119
181
  // request against its signing key when an earlier run pinned it but
120
182
  // crashed before the management key itself was minted and delivered.
@@ -170,8 +232,14 @@ export async function bootstrapPlatform(input, deps) {
170
232
  });
171
233
  };
172
234
  try {
173
- ownerIdentity(input.owner);
235
+ // Refused here, before a credential is read or a row written: the work and
236
+ // personal addresses must be valid and not clash on the company's domain.
237
+ const mailProblem = validateMail(declaredCompanyOrg(input.company, input.owner));
238
+ if (mailProblem)
239
+ throw new Error(`bootstrapPlatform: ${mailProblem}`);
174
240
  redactor.addDeep(deps.executor);
241
+ // The company executor's own tokens (its ports hold the Coolify and ZITADEL credentials).
242
+ redactor.addDeep(deps.company?.executor);
175
243
  const credentials = await deps.rootCredentials();
176
244
  redactor.add(credentials.coolifyToken, credentials.cloudflareApiToken, credentials.cloudflareAccountId, ...Object.values(credentials.accessHeaders ?? {}));
177
245
  const deliverSecret = async (ref, secret) => {
@@ -272,9 +340,11 @@ export async function bootstrapPlatform(input, deps) {
272
340
  ? { driftedSteps: (platform, ids) => inner.driftedSteps?.(platform, ids) ?? {} }
273
341
  : {}),
274
342
  };
343
+ let orgProgress = { started: 0, completed: 0 };
344
+ const companyDeps = deps.company;
275
345
  const planApply = new FoundryPlanApply({
276
346
  store: orgStore,
277
- readers: {
347
+ readers: companyDeps?.readers ?? {
278
348
  zitadel: unusedOrgReader,
279
349
  coolify: unusedOrgReader,
280
350
  cloudflare: unusedOrgReader,
@@ -282,14 +352,17 @@ export async function bootstrapPlatform(input, deps) {
282
352
  ai: unusedOrgReader,
283
353
  cms: unusedOrgReader,
284
354
  },
285
- executor: unusedOrgExecutor,
355
+ executor: companyDeps
356
+ ? trackedOrgExecutor(companyDeps.executor, () => orgProgress, redactor)
357
+ : unusedOrgExecutor,
286
358
  destroyer: unusedOrgExecutor,
287
359
  audit: redactingAudit(createPostgresAuditLog(deps.db, { checkRuntimeRole: deps.checkRuntimeRole }), redactor),
288
- authz: allowPlatformApply,
360
+ authz: allowBootstrapApply(Boolean(companyDeps)),
289
361
  actor: { id: 'bootstrap-platform', display: 'bootstrapPlatform' },
290
362
  platform: { store: platformStore, readers: runtime.readers, executor },
363
+ ...(companyDeps ? { ...companyDeps.options, onlySteps: companySliceOnly } : {}),
291
364
  });
292
- const driven = await driveApply({
365
+ const drivePlatform = (label) => driveApply({
293
366
  plan: () => planApply.planPlatform(),
294
367
  apply: (plan, stepProgress) => {
295
368
  progress = stepProgress;
@@ -300,32 +373,98 @@ export async function bootstrapPlatform(input, deps) {
300
373
  phase: 'plan',
301
374
  system: roundSystem(plan),
302
375
  round,
303
- message: `round ${round}: ${plan.steps.length} step(s) planned`,
376
+ message: `${label}round ${round}: ${plan.steps.length} step(s) planned`,
304
377
  steps: plan.steps.map((s) => s.description),
305
378
  }),
306
379
  onApplied: (round, plan) => emit({
307
380
  phase: 'apply',
308
381
  system: roundSystem(plan),
309
382
  round,
310
- message: `round ${round}: applied`,
383
+ message: `${label}round ${round}: applied`,
311
384
  steps: plan.steps.map((s) => s.description),
312
385
  }),
313
386
  });
314
- const blocked = (driven.failed ? [] : (driven.lastPlan?.unmanaged ?? []))
387
+ // Phase one: the platform.
388
+ const driven = await drivePlatform('');
389
+ const applied = [...driven.applied];
390
+ let failed = driven.failed;
391
+ let lastPlan = driven.lastPlan;
392
+ const blockedRaw = (driven.failed ? [] : (driven.lastPlan?.unmanaged ?? []))
315
393
  .filter((item) => item.key !== undefined)
316
- .map((item) => ({
317
- step: item.key,
318
- message: redactor.text(item.description),
394
+ .map((item) => ({ step: item.key, message: item.description }));
395
+ if (companyDeps && !failed) {
396
+ // Phase two: the platform's own org. Its Boule needs the platform's
397
+ // sign-in, and Archon's people-store keys need that Boule.
398
+ const company = await driveApply({
399
+ plan: () => planApply.plan(COMPANY_INCLUDED_ORG_ID),
400
+ apply: (plan, stepProgress) => {
401
+ orgProgress = stepProgress;
402
+ return planApply.apply(plan, '');
403
+ },
404
+ maxRounds: deps.maxRounds,
405
+ onPlan: (round, plan) => emit({
406
+ phase: 'plan',
407
+ system: roundSystem(plan),
408
+ round,
409
+ message: `company round ${round}: ${plan.steps.length} step(s) planned`,
410
+ steps: plan.steps.map((s) => s.description),
411
+ }),
412
+ onApplied: (round, plan) => emit({
413
+ phase: 'apply',
414
+ system: roundSystem(plan),
415
+ round,
416
+ message: `company round ${round}: applied`,
417
+ steps: plan.steps.map((s) => s.description),
418
+ }),
419
+ });
420
+ applied.push(...company.applied);
421
+ if (company.failed) {
422
+ failed = company.failed;
423
+ blockedRaw.push({ step: 'company', message: COMPANY_NOT_FINISHED });
424
+ }
425
+ else {
426
+ for (const item of company.lastPlan?.unmanaged ?? []) {
427
+ if (item.key !== undefined)
428
+ blockedRaw.push({ step: item.key, message: item.description });
429
+ }
430
+ // Boule's broker entry needs the org's mail domain, which only the org's mail step
431
+ // records, so it is outside this slice. Until it is applied nobody can sign in to Boule:
432
+ // say so, and never report the onboarding complete.
433
+ const companyApplied = await orgStore.getAppliedState(COMPANY_INCLUDED_ORG_ID);
434
+ if (!companyApplied?.serviceIds[brokerEntryAppliedScopeKey(BOULE_BROKER_ENTRY_KEY)]) {
435
+ blockedRaw.push({
436
+ step: BOULE_BROKER_ENTRY_BLOCKED_STEP,
437
+ message: BOULE_SIGN_IN_BLOCKED,
438
+ });
439
+ }
440
+ // Phase three: the same platform plan again. Archon's step now sees
441
+ // the keys Boule holds and writes its own copies.
442
+ const again = await drivePlatform('after the company: ');
443
+ applied.push(...again.applied);
444
+ failed = again.failed;
445
+ lastPlan = again.lastPlan;
446
+ if (!again.failed) {
447
+ for (const item of again.lastPlan?.unmanaged ?? []) {
448
+ if (item.key !== undefined && !blockedRaw.some((b) => b.step === item.key)) {
449
+ blockedRaw.push({ step: item.key, message: item.description });
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ const blocked = blockedRaw.map((item) => ({
456
+ step: item.step,
457
+ message: redactor.text(item.message),
319
458
  }));
320
459
  const result = {
321
- applied: driven.applied.map((s) => redactor.text(s)),
460
+ applied: applied.map((s) => redactor.text(s)),
322
461
  blocked,
323
- complete: !driven.failed && blocked.length === 0 && driven.lastPlan?.steps.length === 0,
324
- ...(driven.failed
462
+ complete: !failed && blocked.length === 0 && lastPlan?.steps.length === 0,
463
+ ...(failed
325
464
  ? {
326
465
  failed: {
327
- step: redactor.text(driven.failed.step),
328
- message: redactor.text(driven.failed.message),
466
+ step: redactor.text(failed.step),
467
+ message: redactor.text(failed.message),
329
468
  },
330
469
  }
331
470
  : {}),
@@ -333,7 +472,7 @@ export async function bootstrapPlatform(input, deps) {
333
472
  emit({
334
473
  phase: 'done',
335
474
  system: 'foundry',
336
- message: driven.failed
475
+ message: failed
337
476
  ? `stopped: ${result.failed?.message}`
338
477
  : `${result.applied.length} step(s) applied, ${blocked.length} blocked`,
339
478
  });
@@ -42,11 +42,15 @@ export interface BootstrapCompany {
42
42
  export interface BootstrapOwner {
43
43
  givenName: string;
44
44
  familyName: string;
45
+ /** The owner's personal address: the first-sign-in link is mailed here. */
45
46
  email: string;
47
+ /** The owner's work address on the company's domain; their sign-in name is its local part. */
48
+ mailboxAddress: string;
46
49
  }
47
50
  /**
48
51
  * The owner as `OwnerIdentity`, trimmed. Throws a plain error naming the
49
- * field when one is empty or the email is not an address.
52
+ * field when one is empty or an address is not an address. The work address
53
+ * is required: the owner signs in as its local part.
50
54
  */
51
55
  export declare function ownerIdentity(owner: BootstrapOwner): OwnerIdentity;
52
56
  /**
@@ -59,13 +59,15 @@ const SERVICES_ENABLED_BY_DEFAULT = new Set([
59
59
  ]);
60
60
  /**
61
61
  * The owner as `OwnerIdentity`, trimmed. Throws a plain error naming the
62
- * field when one is empty or the email is not an address.
62
+ * field when one is empty or an address is not an address. The work address
63
+ * is required: the owner signs in as its local part.
63
64
  */
64
65
  export function ownerIdentity(owner) {
65
66
  const identity = {
66
67
  email: owner.email?.trim() ?? '',
67
68
  givenName: owner.givenName?.trim() ?? '',
68
69
  familyName: owner.familyName?.trim() ?? '',
70
+ mailboxAddress: owner.mailboxAddress?.trim() ?? '',
69
71
  };
70
72
  const problem = validateOwners([identity]);
71
73
  if (problem)
@@ -1,3 +1,3 @@
1
1
  export type { BootstrapCompany, BootstrapOwner, } from './declare.js';
2
- export type { BootstrapExecutorDeps, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap.js';
2
+ export type { BootstrapCompanyDeps, BootstrapExecutorDeps, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap.js';
3
3
  export { bootstrapPlatform } from './bootstrap.js';
@@ -13,6 +13,10 @@ export interface PinnedVersions {
13
13
  cloudflared: string;
14
14
  stalwart: string;
15
15
  hc: string;
16
+ /** The mail relay's base image (an exact Alpine release); Postfix comes from its package repository. */
17
+ alpine: string;
18
+ /** A static busybox, copied into the MAS image so its health check has an HTTP client. */
19
+ busybox: string;
16
20
  }
17
21
  export declare function loadVersions(): PinnedVersions;
18
22
  export declare const VERSIONS: PinnedVersions;
@@ -129,6 +129,16 @@ export declare const LIVEKIT_SIGNALLING_PORT = 7880;
129
129
  export declare const LIVEKIT_MEDIA_TCP_PORT = 7881;
130
130
  /** RTP media. Matches #122's "Decided" section exactly; not the wider range `docs/research/2026-09-19-meetings-research.md` lists for embedded TURN, which #122 does not turn on. */
131
131
  export declare const LIVEKIT_MEDIA_UDP_RANGE = "50000:60000";
132
+ /**
133
+ * The ports of one slice of the Coolify server's media ports (`services.sfuSlot`):
134
+ * slot n holds UDP `50000 + (n-1)*500` to `50000 + n*500 - 1` and TCP `7881 + n`.
135
+ * Throws for a slot outside 1..{@link SFU_SLOT_MAX}.
136
+ */
137
+ export declare function sfuSlotPorts(slot: number): {
138
+ udpStart: number;
139
+ udpEnd: number;
140
+ tcp: number;
141
+ };
132
142
  /**
133
143
  * The host's private network on `vmbr1`, where the calls service reaches
134
144
  * LiveKit's API directly (#127's "Human steps": VM 105 gets a second NIC on
package/dist/calls-sfu.js CHANGED
@@ -53,6 +53,7 @@
53
53
  * from any error `calls-sfu-deploy.mjs` prints.
54
54
  */
55
55
  import { VERSIONS } from './bootstrap/versions.js';
56
+ import { SFU_SLOT_MAX } from './org.js';
56
57
  // ---------------------------------------------------------------------------
57
58
  // The guard
58
59
  // ---------------------------------------------------------------------------
@@ -119,6 +120,27 @@ export const LIVEKIT_SIGNALLING_PORT = 7880;
119
120
  export const LIVEKIT_MEDIA_TCP_PORT = 7881;
120
121
  /** RTP media. Matches #122's "Decided" section exactly; not the wider range `docs/research/2026-09-19-meetings-research.md` lists for embedded TURN, which #122 does not turn on. */
121
122
  export const LIVEKIT_MEDIA_UDP_RANGE = '50000:60000';
123
+ /** The first UDP port of slot 1 and the width of every slot's UDP range (contract section 1). */
124
+ const SFU_SLOT_UDP_BASE = 50000;
125
+ const SFU_SLOT_UDP_WIDTH = 500;
126
+ /** Slot n's TCP fallback port is this plus n. */
127
+ const SFU_SLOT_TCP_BASE = 7881;
128
+ /**
129
+ * The ports of one slice of the Coolify server's media ports (`services.sfuSlot`):
130
+ * slot n holds UDP `50000 + (n-1)*500` to `50000 + n*500 - 1` and TCP `7881 + n`.
131
+ * Throws for a slot outside 1..{@link SFU_SLOT_MAX}.
132
+ */
133
+ export function sfuSlotPorts(slot) {
134
+ if (!Number.isInteger(slot) || slot < 1 || slot > SFU_SLOT_MAX) {
135
+ throw new Error(`sfu slot must be an integer from 1 to ${SFU_SLOT_MAX}`);
136
+ }
137
+ const udpStart = SFU_SLOT_UDP_BASE + (slot - 1) * SFU_SLOT_UDP_WIDTH;
138
+ return {
139
+ udpStart,
140
+ udpEnd: udpStart + SFU_SLOT_UDP_WIDTH - 1,
141
+ tcp: SFU_SLOT_TCP_BASE + slot,
142
+ };
143
+ }
122
144
  /**
123
145
  * The host's private network on `vmbr1`, where the calls service reaches
124
146
  * LiveKit's API directly (#127's "Human steps": VM 105 gets a second NIC on
@@ -176,6 +198,11 @@ const UNSCOPED_SIGNALLING_RULE_PATTERN = new RegExp(`^${LIVEKIT_SIGNALLING_PORT}
176
198
  export function hasUnscopedSignallingRule(status) {
177
199
  return UNSCOPED_SIGNALLING_RULE_PATTERN.test(status);
178
200
  }
201
+ /**
202
+ * Where `cloudflared` serves its metrics, which include `/ready`. The image has no shell, so its check is
203
+ * the binary's own `tunnel ready` in exec form, and that command asks this address.
204
+ */
205
+ const CLOUDFLARED_METRICS_ADDRESS = '127.0.0.1:20241';
179
206
  /**
180
207
  * `network_mode: host`: LiveKit's own self-hosting guide recommends this
181
208
  * over mapping ten thousand individual UDP ports, and the SFU VM has no
@@ -193,13 +220,27 @@ export function dockerComposeYaml() {
193
220
  volumes:
194
221
  - ${LIVEKIT_CONFIG_PATH}:/etc/livekit.yaml:ro
195
222
  command: --config /etc/livekit.yaml
223
+ healthcheck:
224
+ test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${LIVEKIT_SIGNALLING_PORT}/"]
225
+ interval: 30s
226
+ timeout: 5s
227
+ retries: 3
228
+ start_period: 30s
196
229
  cloudflared:
197
230
  image: ${CLOUDFLARED_IMAGE_REF}
198
231
  restart: unless-stopped
199
232
  network_mode: host
200
233
  env_file:
201
234
  - ${CLOUDFLARED_ENV_PATH}
235
+ environment:
236
+ TUNNEL_METRICS: ${CLOUDFLARED_METRICS_ADDRESS}
202
237
  command: tunnel --no-autoupdate run
238
+ healthcheck:
239
+ test: ["CMD", "cloudflared", "tunnel", "--metrics", "${CLOUDFLARED_METRICS_ADDRESS}", "ready"]
240
+ interval: 30s
241
+ timeout: 5s
242
+ retries: 3
243
+ start_period: 30s
203
244
  `;
204
245
  }
205
246
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceSchema, } from './org.js';
2
- export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
1
+ export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, HostingEntry, EntryGate, HostingEntryUses, EntryAuth, EntryFiles, EntryAi, EnvValue, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, OwnerMailbox, DeclaredOrg, OrgServiceSchema, ServicesConfig, } from './org.js';
2
+ export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, appEntry, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateSite, } from './org.js';
3
3
  export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, } from './platform.js';
4
4
  export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
5
5
  import { type PinnedVersions } from './bootstrap/versions.js';
@@ -10,7 +10,7 @@ export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requ
10
10
  export type { SlugRejectionReason, SlugRejection } from './slug.js';
11
11
  export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
12
12
  export type { Checkpoint, LastApplied, AppliedState, DestroyRecord, DestroyStepRecord, } from './state.js';
13
- export type { PlanStep, UnmanagedItem, Plan, VersionChangeItem } from './plan.js';
13
+ export type { PlanStep, UnmanagedItem, PlanNote, Plan, VersionChangeItem } from './plan.js';
14
14
  export type { AppliedVersion, AppliedVersionsStore, DeclaredOrgStore, HeldLease, MutationLease, PlatformStore, RequestedChangeStore, StaleUpdate, UpdateOptions, } from './store.js';
15
15
  export { StaleLeaseError } from './store.js';
16
16
  export type { PlanApply, DestroyProgress } from './apply.js';
@@ -18,17 +18,25 @@ export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, cre
18
18
  export { createPostgresAuditLog } from './store/audit.js';
19
19
  export type { FoundryStoreDb } from './store/postgres.js';
20
20
  export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
21
- export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, OrgPostgresPlan, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, UnconfirmedUpgrade, } from './plan/index.js';
21
+ export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, OrgPostgresPlan, PlatformPlanDetail, MailRoute, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, UnconfirmedUpgrade, } from './plan/index.js';
22
22
  export { AdoptRefusedError } from './plan/index.js';
23
23
  export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
24
24
  export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, DnsPort, EdgePlatformAccess, EdgePlatformDns, EdgePlatformRateLimits, EdgePlatformWorkers, EdgePlatformZones, EdgeZonesOptions, ZonePort, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, EdgeWorkersOptions, EdgePlatformTunnels, TunnelPort, } from './plan/index.js';
25
25
  export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
26
26
  export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
27
- export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
27
+ export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
28
28
  export type { PlatformOrgClientFactories } from './plan/index.js';
29
29
  export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
30
- export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, } from './plan/index.js';
30
+ export type { FilesKeysMint, FilesKeysMinted, OwnFilesDeps, OwnFilesR2Key, OwnFilesR2Keys, OwnServicePorts, OwnServicesPort, OwnServicesProvision, OwnSfuDeps, ServiceTarget, } from './plan/index.js';
31
+ export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgTarget, } from './plan/index.js';
32
+ export type { FilesHostInput, JobsHostDeps, JobsHostInput, OwnAuthInstance, PlanOwnAuthOptions, PlanOwnFilesOptions, PlanOwnFilesR2Options, PlanOwnJobsOptions, } from './plan/index.js';
33
+ export { OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY, OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, provisionFilesHost, provisionJobsHost, provisionMailServer, } from './plan/index.js';
34
+ export type { PlanOwnSfuOptions, SfuAppDeps, SfuAppInput } from './plan/index.js';
35
+ export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuPortsMappings, } from './plan/index.js';
36
+ export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, LoginHostDiscoveryPort, } from './plan/index.js';
31
37
  export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
38
+ export type { EnsureSignInOrgOptions } from './plan/index.js';
39
+ export { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ensureSignInOrg, registerSignInDomain, signInOrgDomain, } from './plan/index.js';
32
40
  export { HttpJobsSchedulerClient } from './plan/index.js';
33
41
  export type { HealthTarget, HealthTargetDepth, HealthTargetExpect } from './plan/index.js';
34
42
  export { healthTargets, platformHealthTargets } from './plan/index.js';
@@ -40,7 +48,7 @@ export type { MailIdentityApp, MailIdentityAudienceCheck } from './plan/index.js
40
48
  export { checkMailIdentityAudience } from './plan/index.js';
41
49
  export type { MailAllowlistApp } from './plan/index.js';
42
50
  export { checkMailAppsAllowlist } from './plan/index.js';
43
- export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
51
+ export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
44
52
  export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
45
53
  export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
46
54
  export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
@@ -58,7 +66,7 @@ export { mintLivekitCredentials } from './calls-sfu-keys.js';
58
66
  export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './plan/index.js';
59
67
  export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './plan/index.js';
60
68
  export { migrationsDir as MIGRATIONS_DIR } from './migrations-dir.js';
61
- export type { BootstrapCompany, BootstrapExecutorDeps, BootstrapOwner, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap/index.js';
69
+ export type { BootstrapCompany, BootstrapCompanyDeps, BootstrapExecutorDeps, BootstrapOwner, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap/index.js';
62
70
  export { bootstrapPlatform } from './bootstrap/index.js';
63
71
  export type { OrgAppVersionRef, OrgAppVersionStatus, WtfalchPackageDrift, PlatformVersionStatus, ReadPlatformVersionStatusOptions, StatusCoolifyPort, StatusGithubPort, StatusGhExec, } from './plan/index.js';
64
72
  export { readPlatformVersionStatus, wtfalchVersionsFromPnpmLockfile, providersStatusCoolifyPort, ghCliStatusGithubPort, } from './plan/index.js';
@@ -66,3 +74,5 @@ export type { RunnerDb } from './runner/db.js';
66
74
  export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
67
75
  export { createJobRunner } from './runner/runner.js';
68
76
  export type { CreateJobRunnerOptions, JobRunner, RunFailure, RunRecord, RunStatus, RunStepsOptions, RunnerStep, } from './runner/types.js';
77
+ export type { StoredApp } from './plan/health-gaps.js';
78
+ export { EXIT_JOBS, REPO_DOCKERFILE_CHECKED, hasRealCheck, healthGaps, } from './plan/health-gaps.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, appEntry, ownerMailboxes, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateSite, } from './org.js';
1
+ export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, appEntry, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateSite, } from './org.js';
2
2
  export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
3
3
  // archon#273: the pinned platform service versions the planner deploys, so a
4
4
  // host can show declared against running. Readonly: it is the planner's own
@@ -14,16 +14,20 @@ export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAudit
14
14
  export { AdoptRefusedError } from './plan/index.js';
15
15
  export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
16
16
  export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
17
- export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
17
+ export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
18
18
  export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
19
+ export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgTarget, } from './plan/index.js';
20
+ export { OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY, OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY, OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, provisionFilesHost, provisionJobsHost, provisionMailServer, } from './plan/index.js';
21
+ export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuPortsMappings, } from './plan/index.js';
19
22
  export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
23
+ export { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ensureSignInOrg, registerSignInDomain, signInOrgDomain, } from './plan/index.js';
20
24
  export { HttpJobsSchedulerClient } from './plan/index.js';
21
25
  export { healthTargets, platformHealthTargets } from './plan/index.js';
22
26
  export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeLoginHostResolver, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
23
27
  export { adaptZitadelListApps } from './plan/index.js';
24
28
  export { checkMailIdentityAudience } from './plan/index.js';
25
29
  export { checkMailAppsAllowlist } from './plan/index.js';
26
- export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
30
+ export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_RELAY_ROUTES_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
27
31
  export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
28
32
  export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
29
33
  export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './plan/index.js';
@@ -39,3 +43,4 @@ export { bootstrapPlatform } from './bootstrap/index.js';
39
43
  export { readPlatformVersionStatus, wtfalchVersionsFromPnpmLockfile, providersStatusCoolifyPort, ghCliStatusGithubPort, } from './plan/index.js';
40
44
  export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
41
45
  export { createJobRunner } from './runner/runner.js';
46
+ export { EXIT_JOBS, REPO_DOCKERFILE_CHECKED, hasRealCheck, healthGaps, } from './plan/health-gaps.js';
@@ -0,0 +1,13 @@
1
+ -- @wtfalch/foundry: a company's own services. `services_own` true gives the
2
+ -- org its own sign-in, mail, files, jobs and calls SFU; false (the default)
3
+ -- keeps the platform's shared ones. `services_sfu_slot` is the slice of the
4
+ -- server's SFU ports an own org's media server takes; no two orgs share one.
5
+ -- Mirrors src/store/schema.ts's `declaredOrgs` table additions.
6
+
7
+ alter table foundry_declared_orgs
8
+ add column if not exists services_own boolean not null default false,
9
+ add column if not exists services_sfu_slot integer;
10
+
11
+ create unique index if not exists foundry_declared_orgs_sfu_slot_idx
12
+ on foundry_declared_orgs (services_sfu_slot)
13
+ where services_sfu_slot is not null;
@@ -0,0 +1,8 @@
1
+ -- @wtfalch/foundry: `mail_relay` true puts one shared relay container on host
2
+ -- port 25, which routes each message by recipient domain to a company's own
3
+ -- mail server or to the platform's; false (the default) keeps the platform
4
+ -- Stalwart bound to port 25 directly. Mirrors src/store/schema.ts's
5
+ -- `foundryDeclaredPlatform` table addition.
6
+
7
+ alter table foundry_declared_platform
8
+ add column if not exists mail_relay boolean not null default false;