@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
@@ -31,6 +31,7 @@ export function createEdgeAccess(client, options = {}) {
31
31
  adopt: (input) => retrying(() => client.adopt(input)),
32
32
  readiness: () => retrying(() => client.readiness()),
33
33
  ensureProvider: (input) => retrying(() => client.ensureProvider(input)),
34
+ oneTimePinProvider: () => retrying(() => client.oneTimePinProvider()),
34
35
  createGate: (input) => retrying(() => client.createGate(input)),
35
36
  replaceGateHostnames: (id, input) => retrying(() => client.replaceGateHostnames(id, input)),
36
37
  deleteGate: (id) => retrying(() => client.deleteGate(id)),
@@ -80,13 +81,27 @@ export function createEdgeAccess(client, options = {}) {
80
81
  const listAccessApps = async () => {
81
82
  const { apps } = await access.list();
82
83
  remember(apps);
83
- return apps.map((a) => ({ id: a.id, name: a.name, domain: a.domain }));
84
+ return apps.map((a) => ({
85
+ id: a.id,
86
+ name: a.name,
87
+ domain: a.domain,
88
+ allowedIdps: a.allowedIdps,
89
+ autoRedirectToIdentity: a.autoRedirectToIdentity,
90
+ }));
84
91
  };
85
92
  return {
86
93
  listAccessApps,
87
94
  async createAccessApp(app) {
88
95
  try {
89
- const created = await writeSlot(() => access.create({ name: app.name, domain: app.domain, type: app.type }));
96
+ const created = await writeSlot(() => access.create({
97
+ name: app.name,
98
+ domain: app.domain,
99
+ type: app.type,
100
+ ...(app.allowedIdps === undefined ? {} : { allowedIdps: app.allowedIdps }),
101
+ ...(app.autoRedirectToIdentity === undefined
102
+ ? {}
103
+ : { autoRedirectToIdentity: app.autoRedirectToIdentity }),
104
+ }));
90
105
  return { id: created.id };
91
106
  }
92
107
  catch (error) {
@@ -155,6 +170,10 @@ export function createEdgeAccess(client, options = {}) {
155
170
  const { authDomain } = await access.readiness();
156
171
  return { authDomain };
157
172
  },
173
+ async ensureOneTimePinProvider() {
174
+ const { id } = await access.oneTimePinProvider();
175
+ return { id };
176
+ },
158
177
  async ensureProvider(input) {
159
178
  const { id } = await writeSlot(() => access.ensureProvider(input));
160
179
  return { id };
@@ -54,6 +54,8 @@ export interface CoolifyPort {
54
54
  */
55
55
  listResources?(projectUuid: string): Promise<Array<{
56
56
  kind: string;
57
+ key?: string;
58
+ config?: Record<string, unknown>;
57
59
  }>>;
58
60
  findOrCreateEnvironment(projectUuid: string, name: string): Promise<{
59
61
  environment: {
@@ -111,10 +113,16 @@ export interface CoolifyPort {
111
113
  /** Coolify's own record of the resource limits (`plan/own-limits.ts`). */
112
114
  limits_memory?: string;
113
115
  limits_cpus?: string;
116
+ git_repository?: string;
117
+ is_auto_deploy_enabled?: boolean;
118
+ /** Coolify's record of the run options, where the capability options live (`plan/run-options.ts`). */
119
+ custom_docker_run_options?: string | null;
114
120
  }>;
115
121
  createGithubAppApplication(input: {
116
122
  project_uuid: string;
117
123
  server_uuid: string;
124
+ /** The Docker network the app joins (`plan/destinations.ts`); absent: the server's default. */
125
+ destination_uuid?: string;
118
126
  environment_name: string;
119
127
  environment_uuid: string;
120
128
  github_app_uuid: string;
@@ -125,6 +133,8 @@ export interface CoolifyPort {
125
133
  domains?: string;
126
134
  /** Selects which of two Dockerfiles in one repo to build (`cmsSite`'s own site/cms apps). */
127
135
  dockerfile_location?: string;
136
+ /** Extra `docker run` options; the capability options are added by `plan/own-limits.ts`. */
137
+ custom_docker_run_options?: string;
128
138
  /** The port the app listens on (`HostingEntry.port`). */
129
139
  ports_exposes?: string;
130
140
  /** `false` for an internal entry: Coolify makes no domain for it. */
@@ -142,6 +152,7 @@ export interface CoolifyPort {
142
152
  createPostgresDatabase(input: {
143
153
  project_uuid: string;
144
154
  server_uuid: string;
155
+ destination_uuid?: string;
145
156
  environment_name: string;
146
157
  environment_uuid: string;
147
158
  name: string;
@@ -223,6 +234,16 @@ export interface CoolifyPort {
223
234
  deleteEnv(applicationUuid: string, envUuid: string): Promise<unknown>;
224
235
  /** Escape hatch (`CoolifyClient.raw`) — used for stop/start, which have no typed method yet. */
225
236
  raw(method: string, path: string, body?: unknown): Promise<unknown>;
237
+ /**
238
+ * F6: the dumps one scheduled backup has run, newest first. A host may wrap
239
+ * `raw('GET', '/databases/{uuid}/backups/{backup}/executions')`; absent, the
240
+ * backup gate (`backup-gate.ts`) makes that call through `raw` itself.
241
+ */
242
+ listDatabaseBackupExecutions?(databaseUuid: string, backupUuid: string): Promise<Array<{
243
+ uuid: string;
244
+ status: string;
245
+ created_at: string;
246
+ }>>;
226
247
  /**
227
248
  * Persistent (named volume) and file storages already attached to an
228
249
  * application (#75) — `ensureLogVolume` reads this before creating one, so
@@ -263,6 +284,7 @@ export interface CoolifyPort {
263
284
  createDockerImageApplication(input: {
264
285
  project_uuid: string;
265
286
  server_uuid: string;
287
+ destination_uuid?: string;
266
288
  environment_name: string;
267
289
  environment_uuid: string;
268
290
  docker_registry_image_name: string;
@@ -294,12 +316,14 @@ export interface CoolifyPort {
294
316
  createDockerfileApplication(input: {
295
317
  project_uuid: string;
296
318
  server_uuid: string;
319
+ destination_uuid?: string;
297
320
  environment_name: string;
298
321
  environment_uuid: string;
299
322
  dockerfile: string;
300
323
  name?: string;
301
324
  description?: string;
302
325
  instant_deploy?: boolean;
326
+ custom_docker_run_options?: string;
303
327
  }): Promise<{
304
328
  uuid: string;
305
329
  }>;
@@ -431,6 +455,17 @@ export interface CoolifyPort {
431
455
  getDeployment(deploymentUuid: string): Promise<{
432
456
  status: string;
433
457
  }>;
458
+ /**
459
+ * An application's deployment history, newest first -- matches
460
+ * `CoolifyClient.listApplicationDeployments`. `build-record.ts` finds the
461
+ * deployment a deploy started by its uuid and records its commit. Optional:
462
+ * a port without it records no built commit.
463
+ */
464
+ listApplicationDeployments?(uuid: string, take?: number): Promise<Array<{
465
+ deployment_uuid: string;
466
+ commit?: string;
467
+ status: string;
468
+ }>>;
434
469
  }
435
470
  /**
436
471
  * The Cloudflare zone calls the platform steps make directly, shaped to match
@@ -939,6 +974,21 @@ export interface ZitadelPort {
939
974
  closeSelfRegistration(orgId: string): Promise<{
940
975
  changed: boolean;
941
976
  }>;
977
+ /**
978
+ * Makes the org require a second factor at sign-in: `forceMfa` on, no
979
+ * registration, no external identity provider, username and password kept
980
+ * (the invite and a password reset need them), passkeys allowed, and
981
+ * `mfaInitSkipLifetime` zero so enrolment cannot be skipped. The second
982
+ * factors are an authenticator app and a security key, the multi factor a
983
+ * user-verified passkey; every other factor (email or SMS code) is removed.
984
+ * An org on the instance default gets its own policy. Idempotent: a policy
985
+ * already so set is no write. A failure halfway is repaired by a repeat,
986
+ * which reads the policy again. `zitadel-login-policy.ts` is the real
987
+ * implementation.
988
+ */
989
+ enforceSecondFactor(orgId: string): Promise<{
990
+ changed: boolean;
991
+ }>;
942
992
  /** Grants another ZITADEL org access to a project (plan gap A9). Idempotent. */
943
993
  findOrCreateProjectGrant(orgId: string, projectId: string, grantedOrgId: string): Promise<{
944
994
  created: boolean;
@@ -1596,7 +1646,7 @@ export interface FoundryStepExecutorPorts {
1596
1646
  deliverSecret(ref: string, secret: string): Promise<void>;
1597
1647
  /**
1598
1648
  * Archon's held keys, for an entry env from `sharedSecret` (read, else
1599
- * generate and `put`) and `platformMailKey` (read only). Optional: without
1649
+ * generate and `put`). Optional: without
1600
1650
  * it, or without `put`, such an env is omitted and the plan reports it
1601
1651
  * blocked. A value goes to the entry's env and nowhere else.
1602
1652
  */
@@ -1612,6 +1662,27 @@ export interface FoundryStepExecutorPorts {
1612
1662
  * passes the same store instance it constructs `FoundryPlanApply` with.
1613
1663
  */
1614
1664
  declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone'>;
1665
+ /**
1666
+ * The `<host>:<port>` the shared mail relay dials for a company's mail server, given the Coolify
1667
+ * server's address and the host port the server publishes. Absent: `<server address>:<port>`. A
1668
+ * host whose containers cannot reach the server by its address (the local stack, where it is a
1669
+ * loopback address) supplies its own. The answer is checked like any relay target
1670
+ * (`normalizeMailTarget`): a host name or an IPv4 address and a port, nothing else.
1671
+ */
1672
+ mailRelayTarget?(input: {
1673
+ serverAddress: string | undefined;
1674
+ port: number;
1675
+ }): string;
1676
+ /**
1677
+ * The address of the server's Docker bridge gateway (an IPv4 address, `172.17.0.1` unless the
1678
+ * daemon's `bip` says otherwise). With it a company's Stalwart publishes SMTP as
1679
+ * `<address>:<port>:25`, which Coolify 4.3.23 passes to Docker unchanged, so the port is reachable
1680
+ * from containers on the server but not on the server's public address; the relay is given
1681
+ * `<address>:<port>` unless `mailRelayTarget` says otherwise. Absent: `<port>:25` on every interface,
1682
+ * and the host firewall must close the range (`docs/runbooks/network-move.md`). The local stack
1683
+ * leaves it unset.
1684
+ */
1685
+ mailBindAddress?: string;
1615
1686
  /**
1616
1687
  * #543: clock, sleep and `fetch` for the health-check wait every company
1617
1688
  * app's own redeploy now goes through (`deploy-health.ts`'s
@@ -1638,9 +1709,8 @@ export declare class NotBuiltError extends Error {
1638
1709
  * `cms.<domain>` -- the cms studio's own hostname (`cmsSite` below), apart
1639
1710
  * from the public site itself at `org.domain` (used directly there). Called
1640
1711
  * from `cmsSite` and from `zitadelApp` for the fixed `cms` client (#139),
1641
- * both of which only run once `org.site.enabled` is true, so `org.domain`
1642
- * is non-null by the same `validateSite` guarantee
1643
- * {@link tunnelIngressHostnames}'s own doc comment cites.
1712
+ * both of which only run once `org.site.enabled` is true. A company without
1713
+ * a domain has its studio at `<slug>-cms.oikos.host` (`cmsStudioHostnameFor`).
1644
1714
  */
1645
1715
  export declare function cmsHostnameFor(org: DeclaredOrg): string;
1646
1716
  /**
@@ -1663,8 +1733,24 @@ export declare function callsHostnameFor(org: DeclaredOrg): string;
1663
1733
  * this file's hostname rules.
1664
1734
  */
1665
1735
  export declare function healthUrls(org: DeclaredOrg): string[];
1736
+ /**
1737
+ * Every app on the Coolify box shares one Traefik reverse proxy, and Coolify
1738
+ * itself points that proxy's routing at whatever `domains` an application
1739
+ * declares (`coolifyApp`'s `appHostname(org)`) -- so a tunnel's ingress only
1740
+ * has to reach the proxy, never a specific app container.
1741
+ * `wtfalch/app-template`'s bootstrap (`bootstrap/steps/cf-tunnel.ts`, prior
1742
+ * art for the exact org this file's `cloudflareTunnel` implements, cited by
1743
+ * https://github.com/wtfalch/foundry/issues/71) names this same constant and
1744
+ * value.
1745
+ */
1746
+ export declare const COOLIFY_PROXY_ORIGIN = "http://coolify-proxy:80";
1666
1747
  /** The one Access policy foundry puts on a platform Access app (#84). */
1667
1748
  export declare const ACCESS_POLICY_NAME = "foundry: allowed emails";
1749
+ export declare const LOG_DIR = "/app/logs";
1750
+ /** The company's Coolify destination (its Docker network `co-<slug>`), recorded when its first resource is created. */
1751
+ export declare const COOLIFY_DESTINATION_SCOPE_KEY = "coolify_destination";
1752
+ export declare const CHAT_REKEY_SYNAPSE_DB_SCOPE_KEY = "coolify_chat_rekey_synapse_database";
1753
+ export declare const CHAT_REKEY_MAS_DB_SCOPE_KEY = "coolify_chat_rekey_mas_database";
1668
1754
  /**
1669
1755
  * Whether an application's own Coolify `status` (`` `running:healthy`,
1670
1756
  * `exited:unhealthy` `` -- `@wtfalch/providers`' own doc comment) means it is
@@ -1705,7 +1791,6 @@ export declare const databaseStartWait: {
1705
1791
  graceMs: number;
1706
1792
  };
1707
1793
  export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer {
1708
- private readonly ports;
1709
1794
  private edgeDns;
1710
1795
  private edgeTunnels;
1711
1796
  private edgeAccess;
@@ -1714,6 +1799,8 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1714
1799
  private edgeWriteSlot;
1715
1800
  /** Secrets this executor minted or was handed; see `StepExecutor.secretsHandled`. */
1716
1801
  private readonly handledSecrets;
1802
+ private readonly ports;
1803
+ private get rawCoolify();
1717
1804
  constructor(ports: FoundryStepExecutorPorts);
1718
1805
  secretsHandled(): readonly string[];
1719
1806
  /**
@@ -1736,12 +1823,26 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1736
1823
  }>;
1737
1824
  /** Whether the host's scoped ports for this own org include a files-organisation port. A held key still missing counts as no port. */
1738
1825
  ownFilesOrganisationPort(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
1826
+ /** Whether the host's scoped ports for this own org include a broker port: the company's own broker, which its broker entries are written to. */
1827
+ ownBrokerPort(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
1739
1828
  /** #555: the edge-routed ports this executor was built with itself (they win over edge). */
1740
1829
  ownEdgePorts(): Partial<Record<EdgePortName, boolean>>;
1741
1830
  execute(action: StepAction, context: StepExecutionContext): Promise<{
1742
1831
  resourceId?: string;
1743
1832
  } | undefined>;
1744
1833
  private run;
1834
+ /**
1835
+ * The company's destination on `serverUuid`: its Docker network `co-<slug>`, made when absent and
1836
+ * recorded as {@link COOLIFY_DESTINATION_SCOPE_KEY}. Called before the first create of a step;
1837
+ * a server that already holds too many networks fails here (`DestinationCapacityError`), before
1838
+ * the destination or the resource exists. A crash after the destination is made and before it is
1839
+ * recorded leaves a destination the next run finds by its network name, so a re-run converges.
1840
+ * Each Coolify call has a 60 second timeout (`destinations.ts`).
1841
+ */
1842
+ private companyDestination;
1843
+ /** `coolify` for an own-services step: its creates go to the company's destination. */
1844
+ private placedForCompany;
1845
+ private dispatch;
1745
1846
  /**
1746
1847
  * `plan/diff.ts`'s `planAccess` (#246): puts a `hosting.access` rule's
1747
1848
  * hostname behind a Cloudflare Access app named
@@ -1965,6 +2066,17 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1965
2066
  private findRecreatedApp;
1966
2067
  private ensureZitadelProject;
1967
2068
  private ensureCoolifyProject;
2069
+ /**
2070
+ * `plan/network-move.ts`'s `planNetworkMove`: a company built on the shared network is moved to its
2071
+ * own. Coolify cannot move a resource in production, so every application and database in the
2072
+ * company's project that foundry did not adopt is deleted, applications first, and the ids recorded
2073
+ * for them are forgotten; the next apply builds them again on `co-<slug>`. Apply has already asked
2074
+ * for the typed company name and checked a fresh backup of every database. A failure part way
2075
+ * leaves what is left recorded and the marker unset, so the same step is planned again and goes on
2076
+ * from there (a 404 counts as deleted). The marker is recorded last. No new timeout: each delete
2077
+ * has the Coolify port's own.
2078
+ */
2079
+ private coolifyNetwork;
1968
2080
  /**
1969
2081
  * #445: one company lands on one Coolify server. The first call for an
1970
2082
  * org picks a usable, reachable server and records it
@@ -2003,6 +2115,15 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2003
2115
  * one).
2004
2116
  */
2005
2117
  private ensureLogVolume;
2118
+ /**
2119
+ * F6: a nightly dump of one of the company's databases to the platform's
2120
+ * backup bucket. The schedule is found before it is created
2121
+ * (`provisionDatabaseBackup`), so a run that died after the create and
2122
+ * before `backup_<db>` was recorded reuses the schedule on the next apply
2123
+ * instead of adding a second. The error carries the database's name, never
2124
+ * the connection URL or a response body.
2125
+ */
2126
+ private coolifyBackup;
2006
2127
  private coolifyDatabase;
2007
2128
  /**
2008
2129
  * Pushes the database's connection string into the app's environment,
@@ -2055,6 +2176,19 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2055
2176
  * provisioned, where this one is unconditional (#239).
2056
2177
  */
2057
2178
  private pushAppEnv;
2179
+ /**
2180
+ * The install's edge label token: read from the held keys, else minted and held first, so a crash
2181
+ * after the `put` hands the same value out on the next apply. Read once more after the `put`, so two
2182
+ * runs that mint at the same moment end on the value that was held last. An error names the ref,
2183
+ * never a value.
2184
+ */
2185
+ private edgeLabelToken;
2186
+ /**
2187
+ * The `custom_labels` patch of an app foundry routes through Traefik at `hostname`: the same http
2188
+ * router a `domains` entry gets, plus the edge token. Coolify uses these instead of the labels it
2189
+ * generates from `domains`. The router names use the app's own name, which is unique per company.
2190
+ */
2191
+ private routedLabels;
2058
2192
  /**
2059
2193
  * The org's shared secret `name`: read from the held keys, else generated
2060
2194
  * and held first, so a crash after the `put` and before the env write
@@ -2779,8 +2913,9 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2779
2913
  private ownAuth;
2780
2914
  /**
2781
2915
  * `plan/own-diff.ts`'s `planOwnMail`: the org's own Stalwart in the org's own
2782
- * project, with no host port (the shared relay reaches it by internal name)
2783
- * and a directory that trusts the org's own sign-in. The admin key goes to
2916
+ * project, publishing its port 25 on one host port of the server (the shared
2917
+ * relay is on another network and dials the server's address) and a directory
2918
+ * that trusts the org's own sign-in. The admin key goes to
2784
2919
  * `orgTarget(org).refs.mailAdminKey` and nowhere else. The address the relay
2785
2920
  * routes to is recorded last, and the step's marker after it.
2786
2921
  */
@@ -2795,6 +2930,17 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2795
2930
  /** `deliverSecret`, tried up to {@link JOBS_DELIVERY_ATTEMPTS} times; true once one attempt succeeds. */
2796
2931
  private deliverWithRetry;
2797
2932
  private cloudflareTunnel;
2933
+ /**
2934
+ * The entry names a company has in the platform's `oikos.host` zone (a company with no domain of its own):
2935
+ * one proxied CNAME each, to this company's tunnel, which routes them straight to the entry container. A
2936
+ * specific record beats the shared `*.oikos.host` wildcard, so the shared ingress stops carrying the name
2937
+ * without being edited. The company writes only its own `<slug>.oikos.host` ({@link ownsOikosName}); an
2938
+ * A or AAAA record, or a CNAME that points anywhere but this tunnel, an earlier id of it or the shared
2939
+ * wildcard's target, blocks the step. A name the company no longer declares has its record deleted when
2940
+ * it still points at this tunnel. The names are recorded only after every record is in place, so a failure
2941
+ * halfway is planned again.
2942
+ */
2943
+ private publishOikosEntryRecords;
2798
2944
  /**
2799
2945
  * #236: when this tunnel's DNS was last published under a domain that
2800
2946
  * differs from the one `org` now declares, deletes every CNAME in the
@@ -3007,6 +3153,19 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
3007
3153
  * publishes it.
3008
3154
  */
3009
3155
  private publishMailDnsRecords;
3156
+ /**
3157
+ * A company without a domain keeps its web address at `<slug>.oikos.host`,
3158
+ * which only the shared `*.oikos.host` wildcard answers. A wildcard applies
3159
+ * only to a name that holds no record of its own, so the MX and TXT records
3160
+ * of the company's mail domain on that same name would take the web address
3161
+ * away. This writes the name's own proxied CNAME, to wherever the wildcard
3162
+ * points, before any mail record. Found by name and type, so a rerun after a
3163
+ * half-finished apply never doubles it. Cloudflare lists only A, AAAA and
3164
+ * CNAME as clashing with a CNAME, so MX and TXT sit beside it.
3165
+ * https://developers.cloudflare.com/dns/manage-dns-records/reference/wildcard-dns-records/
3166
+ * https://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/records-with-same-name/
3167
+ */
3168
+ private ensureOwnWebAddressRecord;
3010
3169
  /**
3011
3170
  * Creates or updates one mail record (Stalwart's or Lettermint's).
3012
3171
  *