@wtfalch/foundry 0.33.1 → 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 (106) 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 +24 -0
  6. package/dist/bootstrap/versions.js +46 -0
  7. package/dist/calls-sfu.d.ts +10 -0
  8. package/dist/calls-sfu.js +44 -3
  9. package/dist/index.d.ts +21 -8
  10. package/dist/index.js +11 -4
  11. package/dist/migrations/0051_org_services.sql +13 -0
  12. package/dist/migrations/0052_platform_mail_relay.sql +8 -0
  13. package/dist/migrations/0053_heartbeats_provider.sql +9 -0
  14. package/dist/migrations/0053_org_version_pin.sql +6 -0
  15. package/dist/org.d.ts +76 -12
  16. package/dist/org.js +136 -20
  17. package/dist/plan/backup-gate.d.ts +93 -0
  18. package/dist/plan/backup-gate.js +261 -0
  19. package/dist/plan/broker-entries.d.ts +9 -1
  20. package/dist/plan/broker-entries.js +20 -3
  21. package/dist/plan/build-record.d.ts +56 -0
  22. package/dist/plan/build-record.js +109 -0
  23. package/dist/plan/calls-matrix.d.ts +9 -0
  24. package/dist/plan/calls-matrix.js +18 -1
  25. package/dist/plan/cloudflared-connector.d.ts +7 -1
  26. package/dist/plan/cloudflared-connector.js +15 -2
  27. package/dist/plan/company-backups.d.ts +31 -0
  28. package/dist/plan/company-backups.js +101 -0
  29. package/dist/plan/deploy-health.d.ts +12 -2
  30. package/dist/plan/deploy-health.js +3 -1
  31. package/dist/plan/destinations.d.ts +73 -0
  32. package/dist/plan/destinations.js +217 -0
  33. package/dist/plan/diff.d.ts +46 -8
  34. package/dist/plan/diff.js +367 -126
  35. package/dist/plan/edge-access.d.ts +15 -0
  36. package/dist/plan/edge-access.js +21 -2
  37. package/dist/plan/executor.d.ts +276 -5
  38. package/dist/plan/executor.js +1111 -109
  39. package/dist/plan/health-check-settings.d.ts +28 -0
  40. package/dist/plan/health-check-settings.js +36 -0
  41. package/dist/plan/health-gaps.d.ts +33 -0
  42. package/dist/plan/health-gaps.js +89 -0
  43. package/dist/plan/health-targets.d.ts +1 -3
  44. package/dist/plan/health-targets.js +36 -36
  45. package/dist/plan/held-key-refs.d.ts +15 -0
  46. package/dist/plan/held-key-refs.js +15 -0
  47. package/dist/plan/hostnames.d.ts +10 -0
  48. package/dist/plan/hostnames.js +14 -0
  49. package/dist/plan/index.d.ts +19 -6
  50. package/dist/plan/index.js +10 -3
  51. package/dist/plan/live.d.ts +11 -2
  52. package/dist/plan/live.js +16 -2
  53. package/dist/plan/locked-hostnames.d.ts +7 -3
  54. package/dist/plan/locked-hostnames.js +25 -4
  55. package/dist/plan/login-host.d.ts +61 -12
  56. package/dist/plan/login-host.js +99 -11
  57. package/dist/plan/mail-relay.d.ts +139 -0
  58. package/dist/plan/mail-relay.js +400 -0
  59. package/dist/plan/network-move.d.ts +20 -0
  60. package/dist/plan/network-move.js +41 -0
  61. package/dist/plan/oikos-ingress.d.ts +111 -0
  62. package/dist/plan/oikos-ingress.js +231 -0
  63. package/dist/plan/org-names.d.ts +30 -0
  64. package/dist/plan/org-names.js +108 -0
  65. package/dist/plan/own-diff.d.ts +170 -0
  66. package/dist/plan/own-diff.js +440 -0
  67. package/dist/plan/own-dns.d.ts +6 -0
  68. package/dist/plan/own-dns.js +15 -0
  69. package/dist/plan/own-hostnames.d.ts +27 -0
  70. package/dist/plan/own-hostnames.js +24 -0
  71. package/dist/plan/own-limits.d.ts +213 -0
  72. package/dist/plan/own-limits.js +232 -0
  73. package/dist/plan/own-services.d.ts +193 -0
  74. package/dist/plan/own-services.js +169 -0
  75. package/dist/plan/own-sfu.d.ts +69 -0
  76. package/dist/plan/own-sfu.js +256 -0
  77. package/dist/plan/plan-apply.d.ts +121 -10
  78. package/dist/plan/plan-apply.js +357 -18
  79. package/dist/plan/platform-diff.d.ts +155 -11
  80. package/dist/plan/platform-diff.js +271 -66
  81. package/dist/plan/platform-org.d.ts +107 -4
  82. package/dist/plan/platform-org.js +175 -3
  83. package/dist/plan/platform-providers.d.ts +474 -81
  84. package/dist/plan/platform-providers.js +1407 -560
  85. package/dist/plan/proxy-labels.d.ts +57 -11
  86. package/dist/plan/proxy-labels.js +94 -14
  87. package/dist/plan/run-options.d.ts +19 -0
  88. package/dist/plan/run-options.js +46 -0
  89. package/dist/plan/sign-in-org.d.ts +1 -1
  90. package/dist/plan/sign-in-org.js +20 -2
  91. package/dist/plan/zitadel-login-policy.d.ts +1 -1
  92. package/dist/plan/zitadel-login-policy.js +77 -9
  93. package/dist/plan/zitadel-system-api.d.ts +22 -1
  94. package/dist/plan/zitadel-system-api.js +49 -8
  95. package/dist/platform.d.ts +40 -0
  96. package/dist/platform.js +16 -0
  97. package/dist/provision.d.ts +2 -0
  98. package/dist/provision.js +3 -1
  99. package/dist/slug.d.ts +3 -1
  100. package/dist/slug.js +26 -3
  101. package/dist/store/postgres.js +93 -2
  102. package/dist/store/schema.d.ts +87 -0
  103. package/dist/store/schema.js +9 -0
  104. package/package.json +3 -3
  105. package/versions.digests.json +11 -0
  106. package/versions.json +3 -1
@@ -47,7 +47,7 @@ export interface BootstrapCompanyDeps {
47
47
  readers: LiveReaders;
48
48
  executor: StepExecutor;
49
49
  /** The same flags `FoundryPlanApply` takes: a step whose port the host did not build is reported blocked. */
50
- options?: Pick<FoundryPlanApplyOptions, 'ownerInviteMail' | 'broker'>;
50
+ options?: Pick<FoundryPlanApplyOptions, 'ownerInviteMail' | 'broker' | 'ownServices'>;
51
51
  }
52
52
  export interface BootstrapPlatformDeps {
53
53
  /** Foundry's tables live here, already migrated by the host; the store, the audit log and the plan all use it. */
@@ -59,13 +59,18 @@ function allowBootstrapApply(withCompany) {
59
59
  };
60
60
  }
61
61
  /**
62
- * The org steps phase two applies: the sign-in org and its domain, the owner
62
+ * The org steps phase two applies: the owner's own sign-in server and its zone,
63
+ * the sign-in org and its domain, the owner
63
64
  * (whose user id Boule's `AUTHZ_FIRST_OWNER` carries), Boule's sign-in client,
64
65
  * and Boule with its database. Everything else the company declares (chat,
65
66
  * mail, Boule's broker entry, which waits for the org's mail domain, ...) is
66
67
  * for a later, ordinary org apply.
67
68
  */
68
69
  const COMPANY_SLICE_STEPS = new Set([
70
+ // The owner's own sign-in server, before anyone can sign in. Own mail, files,
71
+ // jobs and sfu follow after sign-in, as for every company.
72
+ 'cloudflare:zone',
73
+ 'coolify:own-auth',
69
74
  'zitadel:issuer',
70
75
  'zitadel:registration-closed',
71
76
  'zitadel:domain',
@@ -77,8 +82,10 @@ const companySliceOnly = {
77
82
  step: (action) => COMPANY_SLICE_STEPS.has(`${action.system}:${action.kind}`) ||
78
83
  COMPANY_SLICE_STEPS.has(`${action.system}:${action.kind}:${action.key}`),
79
84
  // `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}:`)),
85
+ // `own-auth` is the unmanaged key `planOwnAuth` uses for a blocked sign-in server.
86
+ blocked: (key) => key === 'own-auth' ||
87
+ (key !== 'coolify:boule:mail' &&
88
+ [...COMPANY_SLICE_STEPS].some((step) => key === step || key.startsWith(`${step}:`))),
82
89
  };
83
90
  /** 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
91
  const COMPANY_NOT_FINISHED = "the company's own slice did not finish, so Archon has no people-store keys yet; run onboarding again";
@@ -118,6 +125,10 @@ function trackedOrgExecutor(inner, progress, redactor) {
118
125
  }
119
126
  },
120
127
  ...(inner.ownEdgePorts ? { ownEdgePorts: () => inner.ownEdgePorts?.() ?? {} } : {}),
128
+ // The own sign-in step plans without a held Lettermint key only when the executor can mint one.
129
+ ...(inner.ownMailKeyMintable
130
+ ? { ownMailKeyMintable: () => inner.ownMailKeyMintable?.() ?? false }
131
+ : {}),
121
132
  };
122
133
  }
123
134
  /** The audit log with every free-text field run through the redactor before it is stored. */
@@ -334,10 +345,14 @@ export async function bootstrapPlatform(input, deps) {
334
345
  return result;
335
346
  },
336
347
  ...(inner.blockedSteps
337
- ? { blockedSteps: (platform, ids) => inner.blockedSteps?.(platform, ids) ?? {} }
348
+ ? {
349
+ blockedSteps: (platform, ids, owner) => inner.blockedSteps?.(platform, ids, owner) ?? {},
350
+ }
338
351
  : {}),
339
352
  ...(inner.driftedSteps
340
- ? { driftedSteps: (platform, ids) => inner.driftedSteps?.(platform, ids) ?? {} }
353
+ ? {
354
+ driftedSteps: (platform, ids, owner) => inner.driftedSteps?.(platform, ids, owner) ?? {},
355
+ }
341
356
  : {}),
342
357
  };
343
358
  let orgProgress = { started: 0, completed: 0 };
@@ -392,10 +407,11 @@ export async function bootstrapPlatform(input, deps) {
392
407
  const blockedRaw = (driven.failed ? [] : (driven.lastPlan?.unmanaged ?? []))
393
408
  .filter((item) => item.key !== undefined)
394
409
  .map((item) => ({ step: item.key, message: item.description }));
410
+ const platformBlockedCount = blockedRaw.length;
395
411
  if (companyDeps && !failed) {
396
412
  // Phase two: the platform's own org. Its Boule needs the platform's
397
413
  // sign-in, and Archon's people-store keys need that Boule.
398
- const company = await driveApply({
414
+ const driveCompany = (label) => driveApply({
399
415
  plan: () => planApply.plan(COMPANY_INCLUDED_ORG_ID),
400
416
  apply: (plan, stepProgress) => {
401
417
  orgProgress = stepProgress;
@@ -406,27 +422,30 @@ export async function bootstrapPlatform(input, deps) {
406
422
  phase: 'plan',
407
423
  system: roundSystem(plan),
408
424
  round,
409
- message: `company round ${round}: ${plan.steps.length} step(s) planned`,
425
+ message: `${label}company round ${round}: ${plan.steps.length} step(s) planned`,
410
426
  steps: plan.steps.map((s) => s.description),
411
427
  }),
412
428
  onApplied: (round, plan) => emit({
413
429
  phase: 'apply',
414
430
  system: roundSystem(plan),
415
431
  round,
416
- message: `company round ${round}: applied`,
432
+ message: `${label}company round ${round}: applied`,
417
433
  steps: plan.steps.map((s) => s.description),
418
434
  }),
419
435
  });
436
+ const company = await driveCompany('');
420
437
  applied.push(...company.applied);
421
438
  if (company.failed) {
422
439
  failed = company.failed;
423
440
  blockedRaw.push({ step: 'company', message: COMPANY_NOT_FINISHED });
424
441
  }
425
442
  else {
443
+ const companyBlockedStart = blockedRaw.length;
426
444
  for (const item of company.lastPlan?.unmanaged ?? []) {
427
445
  if (item.key !== undefined)
428
446
  blockedRaw.push({ step: item.key, message: item.description });
429
447
  }
448
+ const companyBlockedCount = blockedRaw.length - companyBlockedStart;
430
449
  // Boule's broker entry needs the org's mail domain, which only the org's mail step
431
450
  // records, so it is outside this slice. Until it is applied nobody can sign in to Boule:
432
451
  // say so, and never report the onboarding complete.
@@ -444,12 +463,31 @@ export async function bootstrapPlatform(input, deps) {
444
463
  failed = again.failed;
445
464
  lastPlan = again.lastPlan;
446
465
  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 });
466
+ // Archon's registration put the first entry on the owner's broker. Only now can the
467
+ // owner's sign-in mint its login client and start that broker, so its plan runs once more.
468
+ const signIn = await driveCompany('after Archon: ');
469
+ applied.push(...signIn.applied);
470
+ if (signIn.failed) {
471
+ failed = signIn.failed;
472
+ blockedRaw.push({ step: 'company', message: COMPANY_NOT_FINISHED });
473
+ }
474
+ else {
475
+ const fresh = [];
476
+ for (const item of signIn.lastPlan?.unmanaged ?? []) {
477
+ if (item.key !== undefined)
478
+ fresh.push({ step: item.key, message: item.description });
450
479
  }
480
+ blockedRaw.splice(companyBlockedStart, companyBlockedCount, ...fresh);
451
481
  }
452
482
  }
483
+ if (!failed) {
484
+ // Archon was blocked on the owner's sign-in in phase one; this plan is the current truth.
485
+ blockedRaw.splice(0, platformBlockedCount);
486
+ const platformBlocked = (again.lastPlan?.unmanaged ?? [])
487
+ .filter((item) => item.key !== undefined)
488
+ .map((item) => ({ step: item.key, message: item.description }));
489
+ blockedRaw.unshift(...platformBlocked);
490
+ }
453
491
  }
454
492
  }
455
493
  const blocked = blockedRaw.map((item) => ({
@@ -38,6 +38,11 @@ export interface BootstrapCompany {
38
38
  name: string;
39
39
  slug: string;
40
40
  apex: string;
41
+ /**
42
+ * The owner company's own domain: absent means `apex`, `null` means none (it
43
+ * then runs on oikos.host). `apex` stays the platform's.
44
+ */
45
+ domain?: string | null;
41
46
  }
42
47
  export interface BootstrapOwner {
43
48
  givenName: string;
@@ -54,13 +59,15 @@ export interface BootstrapOwner {
54
59
  */
55
60
  export declare function ownerIdentity(owner: BootstrapOwner): OwnerIdentity;
56
61
  /**
57
- * Company Included's declared org: the platform's owner, tier 3 on the
58
- * platform's own apex from the start (#512) -- it runs on that domain with
59
- * hosted mail, not a level-1 org whose domain gets set by hand afterward.
62
+ * Company Included's declared org: the platform's owner, a company like any
63
+ * other (`services.own`: its own sign-in, mail, files and jobs). Tier 3 on the
64
+ * platform's apex from the start (#512) -- it runs on that domain with
65
+ * hosted mail, not a level-1 org whose domain gets set by hand afterward --
66
+ * unless `company.domain` is `null`, which runs it on oikos.host.
60
67
  * `issuer`/`calls`/`site` go on the domain too, the same tier-3 defaults
61
68
  * archon#388 gives every company declared there. `calls` and `site` stay
62
- * disabled (calls is not in v1; no site is declared), so `usesDomain` is
63
- * inert on both until one is switched on. Chat stays off the domain: its
69
+ * disabled (calls is not in v1; no site is declared). Without a domain
70
+ * `calls.usesDomain` is off, as `validateCalls` requires. Chat stays off the domain: its
64
71
  * re-key onto a per-org homeserver is not built yet (#463). The org's own
65
72
  * sign-in still never collides with the shared ZITADEL instance's host --
66
73
  * `loginHostnameFor` (#488) falls back to `<slug>-auth.oikos.host` whenever
@@ -36,10 +36,11 @@ export const DEFAULT_MAIL_IMAGE_TAG = VERSIONS.stalwart;
36
36
  */
37
37
  export const DEFAULT_HC_IMAGE_TAG = VERSIONS.hc;
38
38
  /**
39
- * Every platform service except `sfu` is enabled, like the other steps: a
40
- * service whose dependencies the host did not supply is reported blocked,
41
- * not planned. `sfu` stays off because it needs a held SSH key and host
42
- * fingerprint, a separate human step (issue 170). `edge` is on because every
39
+ * Every platform service is enabled except `auth`, `mail`, `files`, `jobs`
40
+ * and `sfu`: the owner company runs its own copies of those like any
41
+ * company (`validatePlatform` refuses them on), so the platform keeps only
42
+ * what no company owns. A service whose dependencies the host did not supply
43
+ * is reported blocked, not planned. `edge` is on because every
43
44
  * platform DNS record is written through it (F4.dns); its own Cloudflare API
44
45
  * token is a held key (`cloudflare:edge:apiToken`), and until a human holds
45
46
  * it the planner reports edge blocked, and auth, mail and archon with it.
@@ -47,16 +48,23 @@ export const DEFAULT_HC_IMAGE_TAG = VERSIONS.hc;
47
48
  * the same as mail and archon.
48
49
  */
49
50
  const SERVICES_ENABLED_BY_DEFAULT = new Set([
50
- 'auth',
51
- 'mail',
52
- 'files',
53
51
  'ai',
54
- 'jobs',
55
52
  'integrations',
56
53
  'archon',
57
54
  'edge',
58
55
  'hc',
59
56
  ]);
57
+ /**
58
+ * The services every company deploys its own copy of. Their platform entries
59
+ * are off but still carry the release the own steps deploy
60
+ * (`serviceGitRefsOf`); with an empty one `own-auth` is blocked.
61
+ */
62
+ const OWN_SERVICE_KEYS = new Set([
63
+ 'auth',
64
+ 'mail',
65
+ 'files',
66
+ 'jobs',
67
+ ]);
60
68
  /**
61
69
  * The owner as `OwnerIdentity`, trimmed. Throws a plain error naming the
62
70
  * field when one is empty or an address is not an address. The work address
@@ -75,33 +83,36 @@ export function ownerIdentity(owner) {
75
83
  return identity;
76
84
  }
77
85
  /**
78
- * Company Included's declared org: the platform's owner, tier 3 on the
79
- * platform's own apex from the start (#512) -- it runs on that domain with
80
- * hosted mail, not a level-1 org whose domain gets set by hand afterward.
86
+ * Company Included's declared org: the platform's owner, a company like any
87
+ * other (`services.own`: its own sign-in, mail, files and jobs). Tier 3 on the
88
+ * platform's apex from the start (#512) -- it runs on that domain with
89
+ * hosted mail, not a level-1 org whose domain gets set by hand afterward --
90
+ * unless `company.domain` is `null`, which runs it on oikos.host.
81
91
  * `issuer`/`calls`/`site` go on the domain too, the same tier-3 defaults
82
92
  * archon#388 gives every company declared there. `calls` and `site` stay
83
- * disabled (calls is not in v1; no site is declared), so `usesDomain` is
84
- * inert on both until one is switched on. Chat stays off the domain: its
93
+ * disabled (calls is not in v1; no site is declared). Without a domain
94
+ * `calls.usesDomain` is off, as `validateCalls` requires. Chat stays off the domain: its
85
95
  * re-key onto a per-org homeserver is not built yet (#463). The org's own
86
96
  * sign-in still never collides with the shared ZITADEL instance's host --
87
97
  * `loginHostnameFor` (#488) falls back to `<slug>-auth.oikos.host` whenever
88
98
  * `auth.<domain>` would equal the platform's own auth hostname.
89
99
  */
90
100
  export function declaredCompanyOrg(company, owner) {
101
+ const domain = company.domain === undefined ? company.apex : company.domain;
91
102
  return {
92
103
  id: COMPANY_INCLUDED_ORG_ID,
93
104
  slug: company.slug,
94
105
  displayName: company.name,
95
- domain: company.apex,
106
+ domain,
96
107
  mail: {
97
108
  mode: 'hosted',
98
- usesDomain: true,
109
+ usesDomain: domain !== null,
99
110
  ownerMailboxAddress: null,
100
111
  noReplyLocalPart: 'no-reply',
101
112
  },
102
113
  chat: { enabled: true, usesDomain: false },
103
- calls: { enabled: false, usesDomain: true },
104
- issuer: { usesDomain: true },
114
+ calls: { enabled: false, usesDomain: domain !== null },
115
+ issuer: { usesDomain: domain !== null },
105
116
  site: { enabled: false, usesDomain: true },
106
117
  hosting: {
107
118
  enabled: false,
@@ -119,6 +130,7 @@ export function declaredCompanyOrg(company, owner) {
119
130
  buyer: { legalName: null, orgNumber: null, address: null, vatRegistered: null },
120
131
  bespokeAppRepo: null,
121
132
  adoptedApp: null,
133
+ services: { own: true },
122
134
  owners: [ownerIdentity(owner)],
123
135
  test: false,
124
136
  };
@@ -134,7 +146,7 @@ export function declaredPlatformFor(apex, serverIpv4) {
134
146
  services[key] = {
135
147
  enabled,
136
148
  hostname: defaultPlatformHostname(key, apex),
137
- gitRef: !enabled
149
+ gitRef: !enabled && !OWN_SERVICE_KEYS.has(key)
138
150
  ? ''
139
151
  : key === 'mail'
140
152
  ? DEFAULT_MAIL_IMAGE_TAG
@@ -148,9 +160,12 @@ export function declaredPlatformFor(apex, serverIpv4) {
148
160
  apex,
149
161
  ...(serverIpv4 ? { serverIpv4 } : {}),
150
162
  ...services,
163
+ // The platform runs no mail server, but every company's mail goes through the shared relay.
164
+ mail: { ...services.mail, relay: true },
151
165
  // Off, like `sfu`: the pin is an Archon release tag and digest, which exist
152
166
  // only after Archon runs. Declared on with no pin, it would be reported
153
167
  // blocked and the bootstrap would never be complete.
154
168
  uptime: { enabled: false, tag: '', sha256: '' },
169
+ heartbeatsProvider: 'self-hosted',
155
170
  };
156
171
  }
@@ -13,9 +13,31 @@ 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;
23
+ /**
24
+ * The multi-arch index digest of each tag in `versions.json`, in
25
+ * `versions.digests.json` beside it. Kept apart so every tag comparison
26
+ * (applied refs, version-change lines) stays on the tag. Validated on read:
27
+ * a malformed digest, or a key of `versions.json` without one, throws.
28
+ */
29
+ export declare function loadDigests(): PinnedVersions;
30
+ export declare const DIGESTS: PinnedVersions;
31
+ /**
32
+ * `<tag>@<digest>` for an image string handed to Coolify or a Dockerfile.
33
+ * Only the image string uses it; the recorded applied ref stays the tag.
34
+ */
35
+ export declare function pinnedRef(key: keyof PinnedVersions): string;
36
+ /**
37
+ * A declared tag that equals the pinned one gets its digest; any other
38
+ * declared tag (an override) is passed through unchanged.
39
+ */
40
+ export declare function pinnedTagFor(key: keyof PinnedVersions, tag: string): string;
19
41
  /**
20
42
  * Why an image tag cannot be used, or `null`. #248: generalised out of what
21
43
  * was `platform-providers.ts`'s `mailImageTagBlocked`, the one guard
@@ -24,3 +46,5 @@ export declare const VERSIONS: PinnedVersions;
24
46
  * `"ZITADEL"`, `"LiveKit"`, ...); `example` is a real pinned tag to suggest.
25
47
  */
26
48
  export declare function floatingTagBlocked(label: string, tag: string, example: string): string | null;
49
+ /** {@link floatingTagBlocked} for a git ref: it also refuses `main`, `master` and `HEAD`. */
50
+ export declare function floatingRefBlocked(label: string, ref: string, example: string): string | null;
@@ -10,6 +10,43 @@ export function loadVersions() {
10
10
  return JSON.parse(readFileSync(VERSIONS_PATH, 'utf8'));
11
11
  }
12
12
  export const VERSIONS = loadVersions();
13
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
14
+ const DIGESTS_PATH = new URL('../../versions.digests.json', import.meta.url);
15
+ /**
16
+ * The multi-arch index digest of each tag in `versions.json`, in
17
+ * `versions.digests.json` beside it. Kept apart so every tag comparison
18
+ * (applied refs, version-change lines) stays on the tag. Validated on read:
19
+ * a malformed digest, or a key of `versions.json` without one, throws.
20
+ */
21
+ export function loadDigests() {
22
+ const raw = JSON.parse(readFileSync(DIGESTS_PATH, 'utf8'));
23
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
24
+ throw new Error('versions.digests.json must be a JSON object');
25
+ }
26
+ const record = raw;
27
+ for (const key of Object.keys(loadVersions())) {
28
+ const value = record[key];
29
+ if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
30
+ throw new Error(`versions.digests.json: "${key}" must be a sha256:<64 hex> digest`);
31
+ }
32
+ }
33
+ return record;
34
+ }
35
+ export const DIGESTS = loadDigests();
36
+ /**
37
+ * `<tag>@<digest>` for an image string handed to Coolify or a Dockerfile.
38
+ * Only the image string uses it; the recorded applied ref stays the tag.
39
+ */
40
+ export function pinnedRef(key) {
41
+ return `${VERSIONS[key]}@${DIGESTS[key]}`;
42
+ }
43
+ /**
44
+ * A declared tag that equals the pinned one gets its digest; any other
45
+ * declared tag (an override) is passed through unchanged.
46
+ */
47
+ export function pinnedTagFor(key, tag) {
48
+ return tag === VERSIONS[key] ? pinnedRef(key) : tag;
49
+ }
13
50
  /**
14
51
  * Tag spellings that resolve to whatever the registry currently has, not a
15
52
  * fixed image -- a re-apply of the same declaration can silently run a
@@ -41,3 +78,12 @@ export function floatingTagBlocked(label, tag, example) {
41
78
  }
42
79
  return null;
43
80
  }
81
+ /** Branch names that move, refused for a git ref on top of the floating image tags. */
82
+ const FLOATING_GIT_REFS = new Set(['main', 'master', 'head']);
83
+ /** {@link floatingTagBlocked} for a git ref: it also refuses `main`, `master` and `HEAD`. */
84
+ export function floatingRefBlocked(label, ref, example) {
85
+ if (FLOATING_GIT_REFS.has(ref.trim().toLowerCase())) {
86
+ return `the ${label} image tag "${ref}" floats; declare a pinned release (for example ${example})`;
87
+ }
88
+ return floatingTagBlocked(label, ref, example);
89
+ }
@@ -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
@@ -52,7 +52,8 @@
52
52
  * `calls-sfu-deploy-support.ts`'s `fileWriteMode`), never logged, scrubbed
53
53
  * from any error `calls-sfu-deploy.mjs` prints.
54
54
  */
55
- import { VERSIONS } from './bootstrap/versions.js';
55
+ import { VERSIONS, pinnedRef } 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
@@ -187,19 +214,33 @@ export function hasUnscopedSignallingRule(status) {
187
214
  export function dockerComposeYaml() {
188
215
  return `services:
189
216
  livekit:
190
- image: ${LIVEKIT_IMAGE_REF}
217
+ image: livekit/livekit-server:${pinnedRef('livekit')}
191
218
  restart: unless-stopped
192
219
  network_mode: host
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
- image: ${CLOUDFLARED_IMAGE_REF}
230
+ image: cloudflare/cloudflared:${pinnedRef('cloudflared')}
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,7 +1,7 @@
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';
3
- export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, } from './platform.js';
4
- export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.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, VersionPin, VersionPinPart, } 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, VERSION_PIN_PARTS, appEntry, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateVersionPin, validateSite, } from './org.js';
3
+ export type { DeclaredPlatform, PlatformService, PlatformServiceKey, PlatformSfuService, HeartbeatsProvider, } from './platform.js';
4
+ export { PLATFORM_SERVICE_KEYS, HEARTBEATS_PROVIDERS, defaultPlatformHostname, validatePlatform, } from './platform.js';
5
5
  import { type PinnedVersions } from './bootstrap/versions.js';
6
6
  export type { PinnedVersions };
7
7
  export declare const PINNED_VERSIONS: Readonly<PinnedVersions>;
@@ -18,17 +18,28 @@ 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
+ export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
22
23
  export { AdoptRefusedError } from './plan/index.js';
24
+ export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
25
+ export type { BackupGatePorts } from './plan/index.js';
23
26
  export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
24
27
  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
28
  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
29
  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';
30
+ export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
28
31
  export type { PlatformOrgClientFactories } from './plan/index.js';
29
32
  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';
33
+ export type { FilesKeysMint, FilesKeysMinted, OwnFilesDeps, OwnFilesR2Key, OwnFilesR2Keys, OwnServicePorts, OwnServicesPort, OwnServicesProvision, OwnSfuDeps, ServiceTarget, } from './plan/index.js';
34
+ export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgTarget, } from './plan/index.js';
35
+ export type { FilesHostInput, JobsHostDeps, JobsHostInput, OwnAuthInstance, PlanOwnAuthOptions, PlanOwnFilesOptions, PlanOwnFilesR2Options, PlanOwnJobsOptions, } from './plan/index.js';
36
+ 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';
37
+ export type { PlanOwnSfuOptions, SfuAppDeps, SfuAppInput } from './plan/index.js';
38
+ export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuPortsMappings, } from './plan/index.js';
39
+ export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, LoginHostDiscoveryPort, } from './plan/index.js';
31
40
  export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
41
+ export { NO_DOMAIN_ZONE, ownHostnames } from './plan/index.js';
42
+ export type { OwnHostnames } from './plan/index.js';
32
43
  export type { EnsureSignInOrgOptions } from './plan/index.js';
33
44
  export { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ensureSignInOrg, registerSignInDomain, signInOrgDomain, } from './plan/index.js';
34
45
  export { HttpJobsSchedulerClient } from './plan/index.js';
@@ -42,7 +53,7 @@ export type { MailIdentityApp, MailIdentityAudienceCheck } from './plan/index.js
42
53
  export { checkMailIdentityAudience } from './plan/index.js';
43
54
  export type { MailAllowlistApp } from './plan/index.js';
44
55
  export { checkMailAppsAllowlist } from './plan/index.js';
45
- export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
56
+ 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';
46
57
  export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformUptimeAccessFactory, PlatformUptimeAccessPort, PlatformUptimeDeps, PlatformWorkersFactory, PlatformWorkersPort, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './plan/index.js';
47
58
  export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
48
59
  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';
@@ -68,3 +79,5 @@ export type { RunnerDb } from './runner/db.js';
68
79
  export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
69
80
  export { createJobRunner } from './runner/runner.js';
70
81
  export type { CreateJobRunnerOptions, JobRunner, RunFailure, RunRecord, RunStatus, RunStepsOptions, RunnerStep, } from './runner/types.js';
82
+ export type { StoredApp } from './plan/health-gaps.js';
83
+ export { EXIT_JOBS, REPO_DOCKERFILE_CHECKED, hasRealCheck, healthGaps, } from './plan/health-gaps.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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';
2
- export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
1
+ export { APP_ENTRY_KEY, DEFAULT_GATE_LIMIT, FILES_ENTRY_PERMISSIONS, AI_ENTRY_PERMISSIONS, DEFAULT_MAIL_APPS, ORG_SERVICE_SCHEMAS, SFU_SLOT_MAX, VERSION_PIN_PARTS, appEntry, ownerMailboxes, ownsServices, validateBuyerIdentity, validateHosting, validateMail, validateOwners, validateServices, validateVersionPin, validateSite, } from './org.js';
2
+ export { PLATFORM_SERVICE_KEYS, HEARTBEATS_PROVIDERS, 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
5
5
  // object, not a copy.
@@ -11,12 +11,18 @@ export { StaleLeaseError } from './store.js';
11
11
  export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
12
12
  export { createPostgresAuditLog } from './store/audit.js';
13
13
  export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
14
+ export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
14
15
  export { AdoptRefusedError } from './plan/index.js';
16
+ export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
15
17
  export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
16
18
  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';
19
+ export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesForOwn, agoraHostValuesFromPlatform, heldOwnKeyRefs, heldPlatformKeyRefs, orgPlatformReadinessFor, orgPlatformReadinessFrom, } from './plan/index.js';
18
20
  export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
21
+ export { OWN_FILES_R2_SCOPE_KEY, OWN_FILES_R2_UNDELIVERED_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, OwnServicesBlockedError, orgTarget, } from './plan/index.js';
22
+ 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';
23
+ export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuPortsMappings, } from './plan/index.js';
19
24
  export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
25
+ export { NO_DOMAIN_ZONE, ownHostnames } from './plan/index.js';
20
26
  export { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ensureSignInOrg, registerSignInDomain, signInOrgDomain, } from './plan/index.js';
21
27
  export { HttpJobsSchedulerClient } from './plan/index.js';
22
28
  export { healthTargets, platformHealthTargets } from './plan/index.js';
@@ -24,7 +30,7 @@ export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, Provi
24
30
  export { adaptZitadelListApps } from './plan/index.js';
25
31
  export { checkMailIdentityAudience } from './plan/index.js';
26
32
  export { checkMailAppsAllowlist } from './plan/index.js';
27
- export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelLoginPolicy, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
33
+ 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';
28
34
  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';
29
35
  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';
30
36
  export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './plan/index.js';
@@ -40,3 +46,4 @@ export { bootstrapPlatform } from './bootstrap/index.js';
40
46
  export { readPlatformVersionStatus, wtfalchVersionsFromPnpmLockfile, providersStatusCoolifyPort, ghCliStatusGithubPort, } from './plan/index.js';
41
47
  export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
42
48
  export { createJobRunner } from './runner/runner.js';
49
+ 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;