@wtfalch/foundry 0.34.0 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +1 -1
  2. package/dist/bootstrap/bootstrap.js +49 -11
  3. package/dist/bootstrap/declare.d.ts +12 -5
  4. package/dist/bootstrap/declare.js +33 -18
  5. package/dist/bootstrap/versions.d.ts +20 -0
  6. package/dist/bootstrap/versions.js +46 -0
  7. package/dist/calls-sfu.js +3 -3
  8. package/dist/index.d.ts +9 -4
  9. package/dist/index.js +5 -2
  10. package/dist/migrations/0053_heartbeats_provider.sql +9 -0
  11. package/dist/migrations/0053_org_version_pin.sql +6 -0
  12. package/dist/org.d.ts +60 -14
  13. package/dist/org.js +118 -28
  14. package/dist/plan/backup-gate.d.ts +93 -0
  15. package/dist/plan/backup-gate.js +261 -0
  16. package/dist/plan/broker-entries.d.ts +9 -1
  17. package/dist/plan/broker-entries.js +20 -3
  18. package/dist/plan/build-record.d.ts +56 -0
  19. package/dist/plan/build-record.js +109 -0
  20. package/dist/plan/calls-matrix.js +3 -3
  21. package/dist/plan/cloudflared-connector.js +2 -2
  22. package/dist/plan/company-backups.d.ts +31 -0
  23. package/dist/plan/company-backups.js +101 -0
  24. package/dist/plan/deploy-health.d.ts +12 -2
  25. package/dist/plan/deploy-health.js +3 -1
  26. package/dist/plan/destinations.d.ts +73 -0
  27. package/dist/plan/destinations.js +217 -0
  28. package/dist/plan/diff.d.ts +17 -8
  29. package/dist/plan/diff.js +131 -131
  30. package/dist/plan/edge-access.d.ts +15 -0
  31. package/dist/plan/edge-access.js +21 -2
  32. package/dist/plan/executor.d.ts +166 -7
  33. package/dist/plan/executor.js +547 -106
  34. package/dist/plan/health-targets.d.ts +1 -3
  35. package/dist/plan/health-targets.js +36 -36
  36. package/dist/plan/held-key-refs.d.ts +5 -0
  37. package/dist/plan/held-key-refs.js +5 -0
  38. package/dist/plan/hostnames.d.ts +10 -0
  39. package/dist/plan/hostnames.js +14 -0
  40. package/dist/plan/index.d.ts +5 -0
  41. package/dist/plan/index.js +3 -0
  42. package/dist/plan/live.d.ts +6 -0
  43. package/dist/plan/live.js +3 -2
  44. package/dist/plan/locked-hostnames.d.ts +2 -1
  45. package/dist/plan/locked-hostnames.js +16 -3
  46. package/dist/plan/login-host.d.ts +9 -20
  47. package/dist/plan/login-host.js +10 -25
  48. package/dist/plan/mail-relay.d.ts +48 -29
  49. package/dist/plan/mail-relay.js +145 -68
  50. package/dist/plan/network-move.d.ts +20 -0
  51. package/dist/plan/network-move.js +41 -0
  52. package/dist/plan/oikos-ingress.d.ts +111 -0
  53. package/dist/plan/oikos-ingress.js +231 -0
  54. package/dist/plan/org-names.d.ts +8 -2
  55. package/dist/plan/org-names.js +57 -1
  56. package/dist/plan/own-diff.d.ts +9 -8
  57. package/dist/plan/own-diff.js +42 -34
  58. package/dist/plan/own-hostnames.d.ts +27 -0
  59. package/dist/plan/own-hostnames.js +24 -0
  60. package/dist/plan/own-limits.d.ts +198 -7
  61. package/dist/plan/own-limits.js +163 -32
  62. package/dist/plan/own-services.js +3 -1
  63. package/dist/plan/own-sfu.js +3 -2
  64. package/dist/plan/plan-apply.d.ts +47 -7
  65. package/dist/plan/plan-apply.js +184 -38
  66. package/dist/plan/platform-diff.d.ts +133 -17
  67. package/dist/plan/platform-diff.js +231 -68
  68. package/dist/plan/platform-org.d.ts +79 -10
  69. package/dist/plan/platform-org.js +93 -13
  70. package/dist/plan/platform-providers.d.ts +118 -73
  71. package/dist/plan/platform-providers.js +770 -231
  72. package/dist/plan/proxy-labels.d.ts +57 -11
  73. package/dist/plan/proxy-labels.js +94 -14
  74. package/dist/plan/run-options.d.ts +19 -0
  75. package/dist/plan/run-options.js +46 -0
  76. package/dist/plan/zitadel-login-policy.d.ts +1 -1
  77. package/dist/plan/zitadel-login-policy.js +76 -8
  78. package/dist/platform.d.ts +32 -0
  79. package/dist/platform.js +16 -0
  80. package/dist/provision.d.ts +2 -0
  81. package/dist/provision.js +3 -1
  82. package/dist/slug.d.ts +3 -1
  83. package/dist/slug.js +25 -3
  84. package/dist/store/postgres.js +43 -1
  85. package/dist/store/schema.d.ts +36 -0
  86. package/dist/store/schema.js +4 -0
  87. package/package.json +3 -3
  88. package/versions.digests.json +11 -0
@@ -1,16 +1,55 @@
1
+ import { hasCapabilities, withCapabilities } from './run-options.js';
1
2
  /**
2
- * The hard limits every container of a company that runs its own services
3
- * gets: one flat default. Field names are Coolify's (`PATCH /applications/{uuid}`
3
+ * The hard limits of every container foundry creates, per step (`LIMITS`). Field names are Coolify's (`PATCH /applications/{uuid}`
4
4
  * and `PATCH /databases/{uuid}`: `limits_memory`, `limits_cpus`). Coolify has
5
5
  * no field for a process limit, and its `custom_docker_run_options` parser drops
6
6
  * `--pids-limit`, so 512 processes cannot be set through Coolify.
7
7
  */
8
8
  export const OWN_LIMITS_MEMORY = '1g';
9
9
  export const OWN_LIMITS_CPUS = '1';
10
- const OWN_LIMITS = { limits_memory: OWN_LIMITS_MEMORY, limits_cpus: OWN_LIMITS_CPUS };
10
+ const profile = (memory, dbMemory = memory, autoDeploy = false, capabilities = 'A') => ({ memory, dbMemory, autoDeploy, capabilities });
11
+ /**
12
+ * The one table of memory limits, per step. Every container foundry creates
13
+ * is in a step named here. The five `own*` rows are the flat 1g the own
14
+ * services already ran with. A Node or Next app gets 1g, a small Postgres
15
+ * 512m, the Go-sized helpers (relay, boule, edge, hc) less.
16
+ */
17
+ export const LIMITS = {
18
+ ownAuth: profile('1g'),
19
+ ownJobs: profile('1g'),
20
+ ownFiles: profile('1g'),
21
+ ownMail: profile('1g'),
22
+ ownSfu: profile('1g'),
23
+ /** A customer's hosting entry: capability tier B, unless the entry declares `capabilities: 'standard'`. */
24
+ companyApp: profile('1g', '1g', 'declared', 'B'),
25
+ companyAppStandard: profile('1g', '1g', 'declared'),
26
+ brokerEntry: profile('1g', '1g', 'declared'),
27
+ cms: profile('1g', '512m'),
28
+ chat: profile('1g', '512m'),
29
+ calls: profile('1g', '512m'),
30
+ agora: profile('1g', '512m'),
31
+ boule: profile('512m'),
32
+ companyOther: profile('1g', '512m'),
33
+ platformArchon: profile('1g'),
34
+ platformAi: profile('1g'),
35
+ platformIntegrations: profile('1g', '512m'),
36
+ platformMailRelay: profile('256m'),
37
+ platformIngress: profile('256m'),
38
+ connector: profile('256m'),
39
+ platformFiles: profile('1g'),
40
+ platformEdge: profile('512m'),
41
+ platformHc: profile('512m'),
42
+ platformMail: profile('1g'),
43
+ platformAuth: profile('1g'),
44
+ platformJobs: profile('1g'),
45
+ };
11
46
  /** The same bound the neighbouring Coolify calls in this package use for a call that may hang. */
12
47
  export const OWN_LIMITS_TIMEOUT_MS = 60_000;
13
- const lacks = (r) => Boolean(r) && (r?.limits_memory !== OWN_LIMITS_MEMORY || r?.limits_cpus !== OWN_LIMITS_CPUS);
48
+ const appLimits = (p) => ({ limits_memory: p.memory, limits_cpus: OWN_LIMITS_CPUS });
49
+ const dbLimits = (p) => ({ limits_memory: p.dbMemory, limits_cpus: OWN_LIMITS_CPUS });
50
+ const lacks = (r, want) => Boolean(r) && (r?.limits_memory !== want.limits_memory || r?.limits_cpus !== OWN_LIMITS_CPUS);
51
+ const capsLack = (r, p) => Boolean(r) &&
52
+ !hasCapabilities(typeof r?.custom_docker_run_options === 'string' ? r.custom_docker_run_options : null, p.capabilities);
14
53
  async function bounded(work, what) {
15
54
  let timer;
16
55
  try {
@@ -25,72 +64,158 @@ async function bounded(work, what) {
25
64
  clearTimeout(timer);
26
65
  }
27
66
  }
67
+ const wrapped = new WeakSet();
28
68
  /**
29
- * The Coolify port an own-services step uses, with the limits set on every
69
+ * The Coolify port a step uses, with the profile's memory limit set on every
30
70
  * application and database it creates, and set again on one it finds without
31
- * them, so a company provisioned before the limits existed converges on its
32
- * next apply. A failed limits call fails the calling step before it records
33
- * its applied marker; the next run finds the resource and sets them.
71
+ * it, so a company provisioned before the limits existed converges on its
72
+ * next apply. With `autoDeploy: false` a git application is created with
73
+ * auto-deploy off, and one found or updated with it on is switched off. A
74
+ * failed limits call fails the calling step before it records its applied
75
+ * marker; the next run finds the resource and sets them.
76
+ *
77
+ * `scope` is a profile, or a function that gives the profile of the step now
78
+ * running (none: the port is used as it is).
79
+ *
80
+ * Every limits call has a 60 second timeout. A step that creates a resource
81
+ * and then fails on the limits call leaves a resource without limits; the
82
+ * next run finds it by name and sets them.
34
83
  */
35
- export function limited(coolify) {
36
- const patchApp = (uuid) => {
84
+ export function limited(coolify, scope = LIMITS.ownAuth) {
85
+ if (wrapped.has(coolify))
86
+ return coolify;
87
+ const current = () => (typeof scope === 'function' ? scope() : scope);
88
+ // `existing`: the application as just read, or the options just sent on its create. Absent, it is read
89
+ // here, because the capability options are written together with the `--entrypoint` already on the app.
90
+ const patchApp = async (uuid, p, autoDeploy, existing) => {
37
91
  if (!coolify.updateApplication)
38
92
  throw new Error('own limits: the Coolify port has no updateApplication');
39
- return bounded(coolify.updateApplication(uuid, OWN_LIMITS), `application ${uuid}`);
93
+ const current = existing ??
94
+ (coolify.getApplication
95
+ ? await bounded(coolify.getApplication(uuid), `application ${uuid}`)
96
+ : undefined);
97
+ const options = current === undefined
98
+ ? {}
99
+ : {
100
+ custom_docker_run_options: withCapabilities(typeof current?.custom_docker_run_options === 'string'
101
+ ? current.custom_docker_run_options
102
+ : null, p.capabilities),
103
+ };
104
+ return bounded(coolify.updateApplication(uuid, {
105
+ ...appLimits(p),
106
+ ...options,
107
+ ...(autoDeploy ? { is_auto_deploy_enabled: false } : {}),
108
+ }), `application ${uuid}`);
40
109
  };
41
- const patchDb = (uuid) => {
110
+ const patchDb = (uuid, p) => {
42
111
  if (!coolify.raw)
43
112
  throw new Error('own limits: the Coolify port has no raw');
44
- return bounded(coolify.raw('PATCH', `/databases/${uuid}`, OWN_LIMITS), `database ${uuid}`);
113
+ return bounded(coolify.raw('PATCH', `/databases/${uuid}`, dbLimits(p)), `database ${uuid}`);
45
114
  };
46
- const afterCreate = (create, patch) => async (input) => {
47
- const created = await create.call(coolify, input);
48
- await patch(created.uuid);
115
+ const afterCreate = (method, git) => async (input) => {
116
+ const p = current();
117
+ const noDeploy = git && p?.autoDeploy === false && input.is_auto_deploy_enabled === undefined
118
+ ? { ...input, is_auto_deploy_enabled: false }
119
+ : input;
120
+ const sent = p
121
+ ? {
122
+ ...noDeploy,
123
+ custom_docker_run_options: withCapabilities(typeof input.custom_docker_run_options === 'string'
124
+ ? input.custom_docker_run_options
125
+ : null, p.capabilities),
126
+ }
127
+ : noDeploy;
128
+ const create = coolify[method];
129
+ const created = await create.call(coolify, sent);
130
+ if (p) {
131
+ await patchApp(created.uuid, p, false, {
132
+ custom_docker_run_options: sent
133
+ .custom_docker_run_options,
134
+ });
135
+ settled.add(created.uuid);
136
+ }
49
137
  return created;
50
138
  };
51
139
  const over = {};
52
140
  if (coolify.createGithubAppApplication)
53
- over.createGithubAppApplication = afterCreate(coolify.createGithubAppApplication, patchApp);
141
+ over.createGithubAppApplication = afterCreate('createGithubAppApplication', true);
54
142
  if (coolify.createDockerImageApplication)
55
- over.createDockerImageApplication = afterCreate(coolify.createDockerImageApplication, patchApp);
143
+ over.createDockerImageApplication = afterCreate('createDockerImageApplication', false);
56
144
  if (coolify.createDockerfileApplication)
57
- over.createDockerfileApplication = afterCreate(coolify.createDockerfileApplication, patchApp);
145
+ over.createDockerfileApplication = afterCreate('createDockerfileApplication', false);
58
146
  if (coolify.createPostgresDatabase) {
59
- const create = coolify.createPostgresDatabase;
60
147
  over.createPostgresDatabase = async (input) => {
61
- const created = await create.call(coolify, input);
62
- await patchDb(created.uuid);
148
+ const created = await coolify.createPostgresDatabase.call(coolify, input);
149
+ const p = current();
150
+ if (p)
151
+ await patchDb(created.uuid, p);
63
152
  return created;
64
153
  };
65
154
  }
66
155
  const getApplication = coolify.getApplication;
156
+ const update = coolify.updateApplication;
157
+ // Applications already brought to the profile by this port: no second patch for them.
158
+ const settled = new Set();
159
+ const settle = async (uuid, p, app, git) => {
160
+ if (settled.has(uuid))
161
+ return;
162
+ const deploys = git && p.autoDeploy === false && app?.is_auto_deploy_enabled !== false;
163
+ if (!app || lacks(app, appLimits(p)) || capsLack(app, p) || deploys) {
164
+ await patchApp(uuid, p, deploys, app);
165
+ }
166
+ settled.add(uuid);
167
+ };
168
+ if (update) {
169
+ over.updateApplication = async (uuid, patch) => {
170
+ const p = current();
171
+ // A step that sends its own run options (the entrypoint) must not take the capability options off.
172
+ const sent = p && typeof patch.custom_docker_run_options === 'string'
173
+ ? {
174
+ ...patch,
175
+ custom_docker_run_options: withCapabilities(patch.custom_docker_run_options, p.capabilities),
176
+ }
177
+ : patch;
178
+ const out = await coolify.updateApplication?.call(coolify, uuid, sent);
179
+ if (p) {
180
+ const git = 'git_branch' in patch || 'is_auto_deploy_enabled' in patch;
181
+ await settle(uuid, p, null, git);
182
+ }
183
+ return out;
184
+ };
185
+ }
67
186
  if (getApplication) {
68
187
  over.getApplication = async (uuid) => {
69
- const app = await getApplication.call(coolify, uuid);
70
- if (lacks(app))
71
- await patchApp(uuid);
188
+ const app = await coolify.getApplication?.call(coolify, uuid);
189
+ const p = current();
190
+ if (p && app)
191
+ await settle(uuid, p, app, Boolean(app.git_repository));
72
192
  return app;
73
193
  };
74
194
  }
75
195
  const find = coolify.findApplicationByName;
76
196
  if (find && getApplication) {
77
197
  over.findApplicationByName = async (...args) => {
78
- const found = await find.apply(coolify, args);
79
- if (found && lacks(await getApplication.call(coolify, found.uuid)))
80
- await patchApp(found.uuid);
198
+ const found = await coolify.findApplicationByName?.apply(coolify, args);
199
+ const p = current();
200
+ if (found && p) {
201
+ const app = await coolify.getApplication?.call(coolify, found.uuid);
202
+ if (app)
203
+ await settle(found.uuid, p, app, Boolean(app.git_repository));
204
+ }
81
205
  return found;
82
206
  };
83
207
  }
84
208
  const getDatabase = coolify.getDatabase;
85
209
  if (getDatabase) {
86
210
  over.getDatabase = async (uuid) => {
87
- const db = await getDatabase.call(coolify, uuid);
88
- if (lacks(db))
89
- await patchDb(uuid);
211
+ const db = await coolify.getDatabase?.call(coolify, uuid);
212
+ const p = current();
213
+ if (p && lacks(db, dbLimits(p)))
214
+ await patchDb(uuid, p);
90
215
  return db;
91
216
  };
92
217
  }
93
- return new Proxy(coolify, {
218
+ const proxy = new Proxy(coolify, {
94
219
  get(target, prop) {
95
220
  if (typeof prop === 'string' && prop in over)
96
221
  return over[prop];
@@ -98,4 +223,10 @@ export function limited(coolify) {
98
223
  return typeof value === 'function' ? value.bind(target) : value;
99
224
  },
100
225
  });
226
+ wrapped.add(proxy);
227
+ return proxy;
228
+ }
229
+ /** The deps of a step with their Coolify port limited by `profile`. */
230
+ export function limitedDeps(deps, profile) {
231
+ return { ...deps, coolify: limited(deps.coolify, profile) };
101
232
  }
@@ -1,3 +1,4 @@
1
+ import { NO_DOMAIN_ZONE } from './own-hostnames.js';
1
2
  import { apexZoneKey, exactZone, platformDns, platformTunnels } from './platform-providers.js';
2
3
  /** The key the org's zone step records its Cloudflare zone id under. */
3
4
  const ORG_ZONE_SCOPE_KEY = 'cloudflare_zone';
@@ -143,7 +144,8 @@ export function orgNetwork(ports, scope, step) {
143
144
  return ports.tunnels;
144
145
  },
145
146
  zoneId: async (apex) => {
146
- const recorded = scope.recordedIds()[ORG_ZONE_SCOPE_KEY];
147
+ // The recorded zone is the company's own; `oikos.host` is never it.
148
+ const recorded = apex === NO_DOMAIN_ZONE ? undefined : scope.recordedIds()[ORG_ZONE_SCOPE_KEY];
147
149
  if (recorded)
148
150
  return recorded;
149
151
  return ports.zones ? (await exactZone(ports.zones, apex))?.id : undefined;
@@ -1,3 +1,4 @@
1
+ import { pinnedTagFor } from '../bootstrap/versions.js';
1
2
  import { mintLivekitCredentials } from '../calls-sfu-keys.js';
2
3
  import { LIVEKIT_SIGNALLING_PORT, sfuSlotPorts } from '../calls-sfu.js';
3
4
  import { ensureCloudflaredConnector } from './cloudflared-connector.js';
@@ -105,7 +106,7 @@ export async function provisionSfuApp(scope, deps, target, input) {
105
106
  environment_name: environment.name,
106
107
  environment_uuid: environment.uuid,
107
108
  docker_registry_image_name: OWN_SFU_IMAGE,
108
- docker_registry_image_tag: input.version,
109
+ docker_registry_image_tag: pinnedTagFor('livekit', input.version),
109
110
  ports_exposes: String(LIVEKIT_SIGNALLING_PORT),
110
111
  ports_mappings: sfuPortsMappings(input.slot),
111
112
  name: target.names.sfuApp,
@@ -121,7 +122,7 @@ export async function provisionSfuApp(scope, deps, target, input) {
121
122
  // Set on every run: a declared version or slot reaches an existing app here.
122
123
  await coolify.updateApplication(appId, {
123
124
  custom_internal_name: target.internal.sfu,
124
- docker_registry_image_tag: input.version,
125
+ docker_registry_image_tag: pinnedTagFor('livekit', input.version),
125
126
  ports_exposes: String(LIVEKIT_SIGNALLING_PORT),
126
127
  ports_mappings: sfuPortsMappings(input.slot),
127
128
  ...healthCheckSettings('/', LIVEKIT_SIGNALLING_PORT),
@@ -7,11 +7,12 @@ import type { AppliedState } from '../state.js';
7
7
  import { type AppliedVersionsStore, type DeclaredOrgStore, type PlatformStore } from '../store.js';
8
8
  import { type AdoptCandidate, type AdoptInput, type AdoptPlan } from './adopt.js';
9
9
  import { type AgoraHostPort } from './agora.js';
10
+ import { type MoveGatePorts } from './backup-gate.js';
10
11
  import { type UnconfirmedUpgrade } from './confirm-upgrade.js';
11
12
  import { type StepAction } from './diff.js';
12
13
  import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
13
14
  import { type LoginHostDiscoveryPort } from './login-host.js';
14
- import { type EdgePortName, type HeldKeyResolver } from './platform-org.js';
15
+ import { type EdgePortName, type HeldKeyResolver, type PlatformOwnerInput } from './platform-org.js';
15
16
  import type { CallsSfuConnectionPort } from './platform-sfu.js';
16
17
  /** Who did it, for `@wtfalch/audit`'s "who, when, the plan, the result" (#9, #19). */
17
18
  export interface AuditActor {
@@ -124,6 +125,11 @@ export interface AuthzGate {
124
125
  */
125
126
  export interface StepExecutionContext {
126
127
  readonly org: DeclaredOrg;
128
+ /**
129
+ * True when `org` is the platform's own company (`DeclaredPlatform.ownerOrgId`). It stays on the
130
+ * platform's Docker network, not on `co-<slug>`, so Archon reaches its database. Absent: false.
131
+ */
132
+ readonly ownerCompany?: boolean;
127
133
  /**
128
134
  * Every id foundry has recorded for this org so far — both `system:kind:key`
129
135
  * resource ids (`runSteps` records these itself from `execute`'s own return
@@ -199,6 +205,13 @@ export interface StepExecutor {
199
205
  * this says false. Optional: an executor without it is taken to have the port.
200
206
  */
201
207
  ownFilesOrganisationPort?(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
208
+ /**
209
+ * Whether the host's scoped ports for this own org include a broker port, the company's own
210
+ * broker. An own org's broker entries are planned on this answer, not on
211
+ * {@link FoundryPlanApplyOptions.broker}, which names the shared broker. Optional: an executor
212
+ * without it leaves the option as the answer.
213
+ */
214
+ ownBrokerPort?(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
202
215
  /**
203
216
  * Every secret this executor minted or read so far (peer password, bridge
204
217
  * secret, invite code and link). The host's run adds them to its redactor
@@ -219,6 +232,8 @@ export interface StepExecutor {
219
232
  */
220
233
  export interface PlatformStepExecutionContext {
221
234
  readonly platform: DeclaredPlatform;
235
+ /** The owner company's own sign-in and files host, when applied. Archon and the Access provider read them. */
236
+ readonly owner?: PlatformOwnerInput;
222
237
  /** {@link StepExecutionContext.recordedIds}, for the platform's own run. */
223
238
  recordedIds(): Readonly<Record<string, string>>;
224
239
  /** {@link StepExecutionContext.recordScopeId}, for the platform's own run. */
@@ -254,7 +269,7 @@ export interface PlatformStepExecutor {
254
269
  * unmanaged item and plans no step for it, the way a blocked Agora is
255
270
  * reported, instead of planning a step that throws on every apply.
256
271
  */
257
- blockedSteps?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<{
272
+ blockedSteps?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>, owner?: PlatformOwnerInput): Promise<{
258
273
  auth?: string;
259
274
  /** #211 round 6: see `PlanPlatformOptions.blocked.authExecutorMissing`. */
260
275
  authExecutorMissing?: boolean;
@@ -269,6 +284,8 @@ export interface PlatformStepExecutor {
269
284
  relay?: string;
270
285
  /** #604: see `PlanPlatformOptions.blocked.accessProvider` (`platform-diff.ts`). */
271
286
  accessProvider?: string;
287
+ /** See `PlanPlatformOptions.blocked.oikosIngress` (`platform-diff.ts`). */
288
+ oikosIngress?: string;
272
289
  /** #257: see `PlanPlatformOptions.blocked.backupExecutorMissing` (`platform-diff.ts`). */
273
290
  backupExecutorMissing?: boolean;
274
291
  /** #540: see `PlanPlatformOptions.blocked.hc` (`platform-diff.ts`). */
@@ -291,7 +308,7 @@ export interface PlatformStepExecutor {
291
308
  * the token and the edge step does not clear the flag, so the host must stop
292
309
  * reporting it once edge holds the new token and the old one is gone.
293
310
  */
294
- driftedSteps?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<PlatformDriftedSteps>;
311
+ driftedSteps?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>, owner?: PlatformOwnerInput): Promise<PlatformDriftedSteps>;
295
312
  /**
296
313
  * Gives the platform files runner a changed share-domain set and touches
297
314
  * nothing else. Returns the scope ids to record (only the share-domains
@@ -513,6 +530,15 @@ export interface FoundryPlanApplyOptions {
513
530
  * every existing caller of this constructor is unaffected.
514
531
  */
515
532
  appliedVersions?: AppliedVersionsStore;
533
+ /**
534
+ * F6: the Coolify slice that reads a company database's backups. With it, an
535
+ * org's databases get backup steps and a confirmed stateful upgrade is
536
+ * refused without a fresh finished dump (`backup-gate.ts`). Without it
537
+ * neither happens, so an existing caller is unaffected.
538
+ */
539
+ backupGate?: {
540
+ coolify: MoveGatePorts['coolify'];
541
+ };
516
542
  /**
517
543
  * #167: dependencies for planning/applying the shared platform (#166)
518
544
  * alongside an org. Optional so an org-only caller — every #19-era caller,
@@ -566,6 +592,7 @@ export declare class FoundryPlanApply implements PlanApply {
566
592
  private readonly now;
567
593
  private readonly leaseTtlMs;
568
594
  private readonly appliedVersions;
595
+ private readonly backupGate;
569
596
  private readonly platformDeps;
570
597
  private readonly runner;
571
598
  constructor(options: FoundryPlanApplyOptions);
@@ -726,8 +753,19 @@ export declare class FoundryPlanApply implements PlanApply {
726
753
  private ownMailKeyHeld;
727
754
  /** Which of an own org's undelivered-secret refs hold a value now, so a rerun resumes instead of blocking. */
728
755
  private ownResumableRefs;
756
+ /**
757
+ * The host port an own org's Stalwart publishes for the relay: the one it already has recorded, else
758
+ * the lowest port in the range no other org has recorded. Two plans made at the same moment could pick
759
+ * the same port; the second Stalwart then fails to bind it and the step fails, with nothing recorded.
760
+ */
761
+ private ownMailSmtpPort;
729
762
  /** The relay is declared and has recorded the routes it last applied: port 25 reaches the mail servers. */
730
763
  private mailRelayApplied;
764
+ /**
765
+ * The relay's last applied routes (a fingerprint, recorded when it deploys) are the routes the
766
+ * orgs' recorded SMTP addresses give now, and one of them is this org's mail domain.
767
+ */
768
+ private mailRelayHoldsRoute;
731
769
  private sharedPlatformReadiness;
732
770
  /**
733
771
  * #177: `AgoraHostValues` from the applied platform, the same
@@ -751,14 +789,16 @@ export declare class FoundryPlanApply implements PlanApply {
751
789
  /** #167: `FoundryPlanApplyOptions.platform`, or a plain `Error` naming what's missing. */
752
790
  private requirePlatformDeps;
753
791
  private computeFreshPlatformPlan;
792
+ /** The owner company's own sign-in and files host, read from its applied ids; `null` members until applied. */
793
+ private platformOwnerInput;
754
794
  /**
755
795
  * What the shared mail relay needs from the orgs. A route exists for an org
756
796
  * that owns its services, serves mail on its own domain and has recorded its
757
797
  * mail server's SMTP address (`mail_smtp_internal`, written by the company
758
- * mail step). Every other org with mail on its own domain, the platform
759
- * apex and the shared no-domain apex are platform domains: the platform's Stalwart receives them. An org
760
- * that owns its services but has no mail server yet appears in neither list,
761
- * so the relay refuses its mail until the server exists.
798
+ * mail step), on its mail domain: its own domain, or `<slug>.oikos.host`.
799
+ * There is no platform mail server: a domain with no route is refused. An org
800
+ * that owns its services but has no mail server yet has no route, so the
801
+ * relay refuses its mail until the server exists.
762
802
  */
763
803
  private mailRelayInputs;
764
804
  private sfuBlockedReason;