@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
@@ -10,6 +10,10 @@ export interface AccessAppSummary {
10
10
  id: string;
11
11
  name: string;
12
12
  domain: string;
13
+ /** The identity providers the app accepts; `[]` means Cloudflare lists none (any provider). */
14
+ allowedIdps: string[];
15
+ /** Whether a visitor goes straight to the one provider. */
16
+ autoRedirectToIdentity: boolean;
13
17
  }
14
18
  /**
15
19
  * Foundry's Cloudflare Access calls. Every Access call in foundry goes through
@@ -19,13 +23,24 @@ export interface AccessAppSummary {
19
23
  export interface AccessPort {
20
24
  /** Every live Access app foundry may use: the platform's own, and ones nobody adopted yet. */
21
25
  listAccessApps(): Promise<AccessAppSummary[]>;
26
+ /**
27
+ * Edge keeps `allowedIdps` of an app that already exists: a repeat create
28
+ * only reports the live value, so a caller compares it from
29
+ * {@link listAccessApps} and rebuilds the app on a difference.
30
+ */
22
31
  createAccessApp(app: {
23
32
  name: string;
24
33
  domain: string;
25
34
  type: 'self_hosted';
35
+ allowedIdps?: string[];
36
+ autoRedirectToIdentity?: boolean;
26
37
  }): Promise<{
27
38
  id: string;
28
39
  }>;
40
+ /** The account's one-time-PIN identity provider (edge 0.13.0 finds or makes it). Safe to repeat. */
41
+ ensureOneTimePinProvider(): Promise<{
42
+ id: string;
43
+ }>;
29
44
  listAccessPolicies(appId: string): Promise<Array<{
30
45
  id: string;
31
46
  name: string;
@@ -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 };
@@ -12,6 +12,7 @@ import { type EdgePlatformTunnels, type TunnelPort } from './edge-tunnels.js';
12
12
  import { type EdgePlatformZones, type ZonePort } from './edge-zones.js';
13
13
  import { type LoginHostBindingsPort } from './login-host.js';
14
14
  import { type MailCopyPort } from './mail-copy.js';
15
+ import { type OwnServicesPort } from './own-services.js';
15
16
  import type { DestroyTarget, StepDestroyer, StepExecutionContext, StepExecutor } from './plan-apply.js';
16
17
  import type { EdgePortName } from './platform-org.js';
17
18
  export declare function coolifyProjectMark(org: Pick<DeclaredOrg, 'id'>): string;
@@ -53,6 +54,8 @@ export interface CoolifyPort {
53
54
  */
54
55
  listResources?(projectUuid: string): Promise<Array<{
55
56
  kind: string;
57
+ key?: string;
58
+ config?: Record<string, unknown>;
56
59
  }>>;
57
60
  findOrCreateEnvironment(projectUuid: string, name: string): Promise<{
58
61
  environment: {
@@ -107,10 +110,19 @@ export interface CoolifyPort {
107
110
  custom_network_aliases?: string | null;
108
111
  health_check_enabled?: boolean;
109
112
  ports_exposes?: string;
113
+ /** Coolify's own record of the resource limits (`plan/own-limits.ts`). */
114
+ limits_memory?: string;
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;
110
120
  }>;
111
121
  createGithubAppApplication(input: {
112
122
  project_uuid: string;
113
123
  server_uuid: string;
124
+ /** The Docker network the app joins (`plan/destinations.ts`); absent: the server's default. */
125
+ destination_uuid?: string;
114
126
  environment_name: string;
115
127
  environment_uuid: string;
116
128
  github_app_uuid: string;
@@ -121,6 +133,8 @@ export interface CoolifyPort {
121
133
  domains?: string;
122
134
  /** Selects which of two Dockerfiles in one repo to build (`cmsSite`'s own site/cms apps). */
123
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;
124
138
  /** The port the app listens on (`HostingEntry.port`). */
125
139
  ports_exposes?: string;
126
140
  /** `false` for an internal entry: Coolify makes no domain for it. */
@@ -138,6 +152,7 @@ export interface CoolifyPort {
138
152
  createPostgresDatabase(input: {
139
153
  project_uuid: string;
140
154
  server_uuid: string;
155
+ destination_uuid?: string;
141
156
  environment_name: string;
142
157
  environment_uuid: string;
143
158
  name: string;
@@ -162,6 +177,8 @@ export interface CoolifyPort {
162
177
  uuid: string;
163
178
  internal_db_url?: string;
164
179
  status?: string;
180
+ limits_memory?: string;
181
+ limits_cpus?: string;
165
182
  }>;
166
183
  /**
167
184
  * Every database Coolify holds. `coolifyDatabase` finds its own by name and
@@ -217,6 +234,16 @@ export interface CoolifyPort {
217
234
  deleteEnv(applicationUuid: string, envUuid: string): Promise<unknown>;
218
235
  /** Escape hatch (`CoolifyClient.raw`) — used for stop/start, which have no typed method yet. */
219
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
+ }>>;
220
247
  /**
221
248
  * Persistent (named volume) and file storages already attached to an
222
249
  * application (#75) — `ensureLogVolume` reads this before creating one, so
@@ -257,6 +284,7 @@ export interface CoolifyPort {
257
284
  createDockerImageApplication(input: {
258
285
  project_uuid: string;
259
286
  server_uuid: string;
287
+ destination_uuid?: string;
260
288
  environment_name: string;
261
289
  environment_uuid: string;
262
290
  docker_registry_image_name: string;
@@ -288,12 +316,14 @@ export interface CoolifyPort {
288
316
  createDockerfileApplication(input: {
289
317
  project_uuid: string;
290
318
  server_uuid: string;
319
+ destination_uuid?: string;
291
320
  environment_name: string;
292
321
  environment_uuid: string;
293
322
  dockerfile: string;
294
323
  name?: string;
295
324
  description?: string;
296
325
  instant_deploy?: boolean;
326
+ custom_docker_run_options?: string;
297
327
  }): Promise<{
298
328
  uuid: string;
299
329
  }>;
@@ -425,6 +455,17 @@ export interface CoolifyPort {
425
455
  getDeployment(deploymentUuid: string): Promise<{
426
456
  status: string;
427
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
+ }>>;
428
469
  }
429
470
  /**
430
471
  * The Cloudflare zone calls the platform steps make directly, shaped to match
@@ -933,6 +974,21 @@ export interface ZitadelPort {
933
974
  closeSelfRegistration(orgId: string): Promise<{
934
975
  changed: boolean;
935
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
+ }>;
936
992
  /** Grants another ZITADEL org access to a project (plan gap A9). Idempotent. */
937
993
  findOrCreateProjectGrant(orgId: string, projectId: string, grantedOrgId: string): Promise<{
938
994
  created: boolean;
@@ -1144,6 +1200,16 @@ export interface ZitadelListAppsRawApp {
1144
1200
  * an operator's `MANAGEMENT_PUBLIC_KEYS`/`MANAGEMENT_ISSUER` pins.
1145
1201
  */
1146
1202
  export interface FilesOrganisationPort {
1203
+ /**
1204
+ * An own files host's R2 storage key, sent to that host once (the files route
1205
+ * `PUT /v1/manage/r2-credentials`). Absent on a port that cannot: the own R2 key step is then
1206
+ * blocked by name. The same pair may be sent again after a failed send.
1207
+ */
1208
+ storeR2Credentials?(input: {
1209
+ accessKeyId: string;
1210
+ secretAccessKey: string;
1211
+ tokenId: string;
1212
+ }): Promise<void>;
1147
1213
  provisionOrganisation(input: {
1148
1214
  organisationId: string;
1149
1215
  ceilingBytes: string | null;
@@ -1447,6 +1513,12 @@ export interface PlatformOrgPortSource {
1447
1513
  }
1448
1514
  export interface FoundryStepExecutorPorts {
1449
1515
  coolify: CoolifyPort;
1516
+ /**
1517
+ * Ports for the instances an own org (`services.own`) runs itself. Without
1518
+ * it, a step for an own org is refused rather than run against the shared
1519
+ * services, and the planner reports the org's own services blocked.
1520
+ */
1521
+ ownServices?: OwnServicesPort;
1450
1522
  zitadel: ZitadelPort;
1451
1523
  mail: MailPort;
1452
1524
  lettermint: LettermintPort;
@@ -1574,7 +1646,7 @@ export interface FoundryStepExecutorPorts {
1574
1646
  deliverSecret(ref: string, secret: string): Promise<void>;
1575
1647
  /**
1576
1648
  * Archon's held keys, for an entry env from `sharedSecret` (read, else
1577
- * generate and `put`) and `platformMailKey` (read only). Optional: without
1649
+ * generate and `put`). Optional: without
1578
1650
  * it, or without `put`, such an env is omitted and the plan reports it
1579
1651
  * blocked. A value goes to the entry's env and nowhere else.
1580
1652
  */
@@ -1590,6 +1662,27 @@ export interface FoundryStepExecutorPorts {
1590
1662
  * passes the same store instance it constructs `FoundryPlanApply` with.
1591
1663
  */
1592
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;
1593
1686
  /**
1594
1687
  * #543: clock, sleep and `fetch` for the health-check wait every company
1595
1688
  * app's own redeploy now goes through (`deploy-health.ts`'s
@@ -1616,9 +1709,8 @@ export declare class NotBuiltError extends Error {
1616
1709
  * `cms.<domain>` -- the cms studio's own hostname (`cmsSite` below), apart
1617
1710
  * from the public site itself at `org.domain` (used directly there). Called
1618
1711
  * from `cmsSite` and from `zitadelApp` for the fixed `cms` client (#139),
1619
- * both of which only run once `org.site.enabled` is true, so `org.domain`
1620
- * is non-null by the same `validateSite` guarantee
1621
- * {@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`).
1622
1714
  */
1623
1715
  export declare function cmsHostnameFor(org: DeclaredOrg): string;
1624
1716
  /**
@@ -1641,8 +1733,24 @@ export declare function callsHostnameFor(org: DeclaredOrg): string;
1641
1733
  * this file's hostname rules.
1642
1734
  */
1643
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";
1644
1747
  /** The one Access policy foundry puts on a platform Access app (#84). */
1645
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";
1646
1754
  /**
1647
1755
  * Whether an application's own Coolify `status` (`` `running:healthy`,
1648
1756
  * `exited:unhealthy` `` -- `@wtfalch/providers`' own doc comment) means it is
@@ -1683,7 +1791,6 @@ export declare const databaseStartWait: {
1683
1791
  graceMs: number;
1684
1792
  };
1685
1793
  export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer {
1686
- private readonly ports;
1687
1794
  private edgeDns;
1688
1795
  private edgeTunnels;
1689
1796
  private edgeAccess;
@@ -1692,13 +1799,50 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1692
1799
  private edgeWriteSlot;
1693
1800
  /** Secrets this executor minted or was handed; see `StepExecutor.secretsHandled`. */
1694
1801
  private readonly handledSecrets;
1802
+ private readonly ports;
1803
+ private get rawCoolify();
1695
1804
  constructor(ports: FoundryStepExecutorPorts);
1696
1805
  secretsHandled(): readonly string[];
1806
+ /**
1807
+ * An own org's step runs on a copy of this executor whose ZITADEL, mail,
1808
+ * broker, files, jobs and calls ports are the org's own. A port the org has
1809
+ * not got yet is absent or throws, never the shared one.
1810
+ */
1811
+ private executeScoped;
1812
+ /** Whether `ensureOwnAuthMailToken` can mint: the same two inputs it blocks on. */
1813
+ ownMailKeyMintable(): boolean;
1814
+ /** What the executor's own files dependencies supply, for the planner's blocked lines. */
1815
+ ownFilesDeps(): {
1816
+ keys: boolean;
1817
+ bucket: boolean;
1818
+ };
1819
+ /** Which ports the own files storage key step needs this executor has, for the planner's blocked line. */
1820
+ ownFilesR2Ports(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<{
1821
+ r2Keys: boolean;
1822
+ storeR2Credentials: boolean;
1823
+ }>;
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. */
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>;
1697
1828
  /** #555: the edge-routed ports this executor was built with itself (they win over edge). */
1698
1829
  ownEdgePorts(): Partial<Record<EdgePortName, boolean>>;
1699
1830
  execute(action: StepAction, context: StepExecutionContext): Promise<{
1700
1831
  resourceId?: string;
1701
1832
  } | undefined>;
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;
1702
1846
  /**
1703
1847
  * `plan/diff.ts`'s `planAccess` (#246): puts a `hosting.access` rule's
1704
1848
  * hostname behind a Cloudflare Access app named
@@ -1922,6 +2066,17 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1922
2066
  private findRecreatedApp;
1923
2067
  private ensureZitadelProject;
1924
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;
1925
2080
  /**
1926
2081
  * #445: one company lands on one Coolify server. The first call for an
1927
2082
  * org picks a usable, reachable server and records it
@@ -1960,6 +2115,15 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1960
2115
  * one).
1961
2116
  */
1962
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;
1963
2127
  private coolifyDatabase;
1964
2128
  /**
1965
2129
  * Pushes the database's connection string into the app's environment,
@@ -2012,6 +2176,19 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2012
2176
  * provisioned, where this one is unconditional (#239).
2013
2177
  */
2014
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;
2015
2192
  /**
2016
2193
  * The org's shared secret `name`: read from the held keys, else generated
2017
2194
  * and held first, so a crash after the `put` and before the env write
@@ -2189,6 +2366,22 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2189
2366
  * (which must never delete the platform's own project) change.
2190
2367
  */
2191
2368
  private ensureOrgLettermintRelay;
2369
+ /**
2370
+ * The company's own sign-in mail token: a Lettermint project `<slug>-auth`
2371
+ * whose token goes to `orgTarget(org).refs.brokerMailKey`, where the broker
2372
+ * and ZITADEL's SMTP settings read it. The platform-wide Lettermint key
2373
+ * stays with the executor and never reaches a company. A held ref is left
2374
+ * alone. Otherwise the mint rule of {@link ensureOrgAppMailToken}: create
2375
+ * the project, restrict the domain, deliver its token, and only then
2376
+ * delete every other `<slug>-auth` project (a token cannot be read back,
2377
+ * so a project without a held token is a leftover). The restriction comes
2378
+ * before the delivery so a failure there leaves no held ref: the next run
2379
+ * starts over instead of skipping it. A failure before the delivery leaves
2380
+ * that project for the next run to delete. Every Lettermint call is
2381
+ * bounded; the token is registered for redaction before anything can throw
2382
+ * with it.
2383
+ */
2384
+ private ensureOwnAuthMailToken;
2192
2385
  /**
2193
2386
  * Deletes a just-minted project from inside a `catch`. A failed delete is
2194
2387
  * dropped so it never replaces the error being rethrown; the orphan is
@@ -2684,10 +2877,70 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2684
2877
  */
2685
2878
  private zonePort;
2686
2879
  private jobsPort;
2880
+ /**
2881
+ * `plan/own-diff.ts`'s `planOwnJobs`: the org's own scheduler, in the org's
2882
+ * own project, with the names and the held-key ref of `orgTarget(org)`. The
2883
+ * admin token goes to `jobs:org:<slug>:adminToken` and nowhere else.
2884
+ */
2885
+ private ownJobs;
2886
+ /**
2887
+ * `plan/own-diff.ts`'s `planOwnFiles`: the org's own files host in the
2888
+ * org's own project, trusting the company's issuer `https://auth.<domain>`.
2889
+ * Its ZITADEL project `<slug>-files` and introspection app are made in the
2890
+ * company's instance (`ownServices.zitadelFor`), never the shared one. The
2891
+ * bucket comes from the host's bucket port under the target's name. The
2892
+ * management key goes to `files:org:<slug>:managementKey` and the
2893
+ * introspection secret to `zitadel:org:<slug>:files:introspection`, both
2894
+ * once, and neither is recorded or returned.
2895
+ */
2896
+ private ownFiles;
2897
+ /**
2898
+ * `plan/own-diff.ts`'s `planOwnFilesR2`: the storage key of the org's own files bucket. A token
2899
+ * recorded as undelivered is deleted first (its secret cannot be read back, so it can never be
2900
+ * finished), then one is minted, registered as a secret, recorded by id as undelivered and
2901
+ * persisted, and only then sent to the files host, the same pair on every retry. The secret is
2902
+ * never recorded and never written to a Coolify env. The id is recorded once the host has it.
2903
+ */
2904
+ private ownFilesR2;
2905
+ /**
2906
+ * `plan/own-diff.ts`'s `planOwnAuth`: the org's own sign-in at
2907
+ * `auth.<domain>`, in the org's own project, through the org's own edge
2908
+ * routed DNS and tunnel ports. The instance owner token goes to
2909
+ * `zitadel:org:<slug>:instanceBearer` and is never recorded. A changed
2910
+ * declared auth ref patches both git apps' branch and redeploys them; the
2911
+ * applied marker comes last, so a run that died halfway is planned again.
2912
+ */
2913
+ private ownAuth;
2914
+ /**
2915
+ * `plan/own-diff.ts`'s `planOwnMail`: the org's own Stalwart in the org's own
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
2919
+ * `orgTarget(org).refs.mailAdminKey` and nowhere else. The address the relay
2920
+ * routes to is recorded last, and the step's marker after it.
2921
+ */
2922
+ private ownMail;
2923
+ /**
2924
+ * `plan/own-diff.ts`'s `planOwnSfu`: the org's own LiveKit server in the
2925
+ * org's project, on the ports of its slot. The company's key pair goes to
2926
+ * `livekit:org:<slug>:keys` and the app's env, nowhere else.
2927
+ */
2928
+ private ownSfu;
2687
2929
  private jobsRegistration;
2688
2930
  /** `deliverSecret`, tried up to {@link JOBS_DELIVERY_ATTEMPTS} times; true once one attempt succeeds. */
2689
2931
  private deliverWithRetry;
2690
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;
2691
2944
  /**
2692
2945
  * #236: when this tunnel's DNS was last published under a domain that
2693
2946
  * differs from the one `org` now declares, deletes every CNAME in the
@@ -2900,6 +3153,19 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2900
3153
  * publishes it.
2901
3154
  */
2902
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;
2903
3169
  /**
2904
3170
  * Creates or updates one mail record (Stalwart's or Lettermint's).
2905
3171
  *
@@ -2937,6 +3203,11 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2937
3203
  * #491: when `mailDomain` is the platform's own apex, this must never
2938
3204
  * delete the platform's own project (the delete loop skips it by id) or
2939
3205
  * the platform's own domain (returns before the domain lookup/delete).
3206
+ *
3207
+ * An org whose own sign-in is applied keeps its `<slug>-auth` project, and
3208
+ * keeps the Lettermint domain when that is the domain the sign-in sends
3209
+ * from. Every other org deletes `<slug>-auth` too, which only a failed
3210
+ * first sign-in run can have left behind.
2940
3211
  */
2941
3212
  private parkLettermintMail;
2942
3213
  /**