@wtfalch/foundry 0.16.15 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/apply.d.ts +22 -1
  2. package/dist/bootstrap/bootstrap.d.ts +6 -1
  3. package/dist/bootstrap/bootstrap.js +26 -3
  4. package/dist/bootstrap/declare.d.ts +11 -1
  5. package/dist/bootstrap/declare.js +12 -2
  6. package/dist/index.d.ts +9 -4
  7. package/dist/index.js +7 -0
  8. package/dist/migrations/0038_calls_version_pin.sql +8 -0
  9. package/dist/migrations/0039_destroy_record.sql +7 -0
  10. package/dist/migrations/0040_company_included_uuid.sql +72 -0
  11. package/dist/migrations/0041_company_included_uuid_keys.sql +132 -0
  12. package/dist/migrations/0042_mutation_leases.sql +12 -0
  13. package/dist/org.d.ts +82 -5
  14. package/dist/org.js +62 -1
  15. package/dist/plan/adopt.d.ts +76 -0
  16. package/dist/plan/adopt.js +184 -0
  17. package/dist/plan/agora.d.ts +21 -0
  18. package/dist/plan/agora.js +37 -0
  19. package/dist/plan/broker-entries.d.ts +9 -1
  20. package/dist/plan/broker-entries.js +13 -5
  21. package/dist/plan/chat-matrix.d.ts +1 -1
  22. package/dist/plan/chat-matrix.js +11 -3
  23. package/dist/plan/core-files.d.ts +25 -0
  24. package/dist/plan/core-files.js +25 -0
  25. package/dist/plan/deploy-health.d.ts +136 -0
  26. package/dist/plan/deploy-health.js +153 -0
  27. package/dist/plan/diff.d.ts +11 -3
  28. package/dist/plan/diff.js +77 -71
  29. package/dist/plan/entry-ai-key.d.ts +3 -3
  30. package/dist/plan/entry-files-key.d.ts +6 -4
  31. package/dist/plan/entry-files-key.js +7 -10
  32. package/dist/plan/executor.d.ts +109 -28
  33. package/dist/plan/executor.js +213 -56
  34. package/dist/plan/hostnames.d.ts +6 -0
  35. package/dist/plan/hostnames.js +13 -0
  36. package/dist/plan/index.d.ts +5 -1
  37. package/dist/plan/index.js +3 -1
  38. package/dist/plan/live.d.ts +5 -0
  39. package/dist/plan/login-host.d.ts +1 -1
  40. package/dist/plan/login-host.js +6 -5
  41. package/dist/plan/plan-apply.d.ts +153 -3
  42. package/dist/plan/plan-apply.js +584 -69
  43. package/dist/plan/platform-diff.d.ts +103 -9
  44. package/dist/plan/platform-diff.js +192 -14
  45. package/dist/plan/platform-org.d.ts +25 -0
  46. package/dist/plan/platform-org.js +17 -1
  47. package/dist/plan/platform-providers.d.ts +38 -5
  48. package/dist/plan/platform-providers.js +592 -109
  49. package/dist/plan/providers-readers.d.ts +2 -0
  50. package/dist/plan/providers-readers.js +4 -0
  51. package/dist/plan/rollout.d.ts +52 -0
  52. package/dist/plan/rollout.js +37 -0
  53. package/dist/plan/step-system.d.ts +22 -0
  54. package/dist/plan/step-system.js +37 -0
  55. package/dist/plan.d.ts +3 -0
  56. package/dist/state.d.ts +13 -0
  57. package/dist/store/audit.js +15 -1
  58. package/dist/store/postgres.d.ts +1 -1
  59. package/dist/store/postgres.js +127 -14
  60. package/dist/store/schema.d.ts +134 -5
  61. package/dist/store/schema.js +12 -1
  62. package/dist/store.d.ts +47 -4
  63. package/dist/store.js +9 -1
  64. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import { type DeclaredOrg, type HostingEntry } from '../org.js';
2
- import type { PlannedStep } from './diff.js';
2
+ import type { DraftStep } from './diff.js';
3
3
  import type { OrgPlatformReadiness } from './platform-org.js';
4
4
  /**
5
5
  * F6b: the org's own files key, minted below the files organisation's
@@ -56,8 +56,10 @@ export interface EntryFilesWant {
56
56
  permissions: string[];
57
57
  collections: string[];
58
58
  filesUrl: string;
59
+ /** `https://share.<domain>`, set only when the entry declares `shareHost`. */
60
+ shareOrigin?: string;
59
61
  }
60
- export declare function wantedEntryFiles(entry: HostingEntry, filesUrl: string): EntryFilesWant;
62
+ export declare function wantedEntryFiles(entry: HostingEntry, filesUrl: string, shareHostname?: string | null): EntryFilesWant;
61
63
  export type EntryFilesMode = 'mint' | 'renew' | 'settings' | 'finish' | 'retire';
62
64
  type Blocked = Array<{
63
65
  description: string;
@@ -75,8 +77,8 @@ type Blocked = Array<{
75
77
  * it by its own `renew` step, and only then does `revoke-previous` revoke the
76
78
  * old org key, which revokes everything still below it.
77
79
  */
78
- export declare function planFilesKeys(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly PlannedStep[], readiness: OrgPlatformReadiness, now: number): {
79
- steps: PlannedStep[];
80
+ export declare function planFilesKeys(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], readiness: OrgPlatformReadiness, now: number): {
81
+ steps: DraftStep[];
80
82
  blocked: Blocked;
81
83
  };
82
84
  export {};
@@ -1,4 +1,5 @@
1
1
  import { FILES_ENTRY_PERMISSIONS } from '../org.js';
2
+ import { filesShareHostname } from './hostnames.js';
2
3
  import { appStepKey } from './platform-diff.js';
3
4
  /**
4
5
  * F6b: the org's own files key, minted below the files organisation's
@@ -67,12 +68,13 @@ export function filesOwnerUserId(org, recordedIds) {
67
68
  const first = org.owners[0];
68
69
  return first ? recordedIds[`zitadel:owner:${org.id}:${first.email.toLowerCase()}`] : undefined;
69
70
  }
70
- export function wantedEntryFiles(entry, filesUrl) {
71
+ export function wantedEntryFiles(entry, filesUrl, shareHostname = null) {
71
72
  const files = entry.uses?.files;
72
73
  return {
73
74
  permissions: [...(files?.permissions ?? FILES_ENTRY_PERMISSIONS)],
74
75
  collections: [...(files?.collections ?? [])],
75
76
  filesUrl,
77
+ ...(files?.shareHost && shareHostname ? { shareOrigin: `https://${shareHostname}` } : {}),
76
78
  };
77
79
  }
78
80
  /**
@@ -157,14 +159,9 @@ export function planFilesKeys(org, recordedIds, plannedSteps, readiness, now) {
157
159
  }
158
160
  if (entries.length === 0 || !readiness.filesReady)
159
161
  return { steps, blocked };
160
- for (const entry of entries) {
161
- if (entry.uses?.files?.shareHost) {
162
- blocked.push({
163
- description: `${org.slug}'s ${entry.key} share host is blocked: the platform's files host has no route for share.<domain> yet, so FILES_SHARE_ORIGIN is not delivered`,
164
- key: `files:share:${org.id}:${entry.key}`,
165
- });
166
- }
167
- }
162
+ // The route and DNS for share.<domain> are planned with the tunnel; the org
163
+ // apply then gives the platform files runner the host (`FoundryPlanApply.apply`).
164
+ const shareHostname = filesShareHostname(org);
168
165
  // The org's own key: minted, or renewed when it is short or its expiry is unknown.
169
166
  const rootBlock = (why) => blocked.push({
170
167
  description: `${org.slug}'s own files key is blocked: ${why}`,
@@ -244,7 +241,7 @@ export function planFilesKeys(org, recordedIds, plannedSteps, readiness, now) {
244
241
  block("the entry's app is not recorded yet");
245
242
  continue;
246
243
  }
247
- const want = wantedEntryFiles(entry, readiness.filesUrl);
244
+ const want = wantedEntryFiles(entry, readiness.filesUrl, shareHostname);
248
245
  const applied = recordedIds[entryFilesAppliedScopeKey(entry.key)];
249
246
  const wantJson = JSON.stringify(want);
250
247
  let mode = null;
@@ -183,10 +183,19 @@ export interface CoolifyPort {
183
183
  * `is_preview === false` the same way a deployed-config reader must.
184
184
  */
185
185
  listEnvs(applicationUuid: string): Promise<Array<{
186
+ uuid?: string;
186
187
  key: string;
187
188
  value: string;
188
189
  is_preview: boolean;
189
190
  }>>;
191
+ /**
192
+ * #436 round 2: removes one environment variable row by the `uuid`
193
+ * `listEnvs` names it under -- `bulkUpsertEnvs` only adds or updates, so
194
+ * this is the only way to make a var foundry wrote in an earlier release
195
+ * actually go away, rather than leaving it set to a value nothing reads
196
+ * any more (`provisionPlatformArchon`'s stale `FILES_MANAGEMENT_KEY`).
197
+ */
198
+ deleteEnv(applicationUuid: string, envUuid: string): Promise<unknown>;
190
199
  /** Escape hatch (`CoolifyClient.raw`) — used for stop/start, which have no typed method yet. */
191
200
  raw(method: string, path: string, body?: unknown): Promise<unknown>;
192
201
  /**
@@ -294,10 +303,13 @@ export interface CoolifyPort {
294
303
  * fresh Synapse/MAS config file, since a file-storage write alone does not
295
304
  * reach the running container until its next deploy (issue #128's "a
296
305
  * running app is redeployed when its config changed"). Fire-and-forget,
297
- * like `restart` above: this executor does not poll a deployment to
298
- * completion anywhere else, and `coolifyCalls` only calls this when the
299
- * config it just wrote was actually freshly minted. Matches `CoolifyClient.
300
- * deploy` method-for-method.
306
+ * like `restart` above: `coolifyCalls` only calls this when the config it
307
+ * just wrote was actually freshly minted, with nothing recorded afterward
308
+ * that depends on the new version actually being live. Matches
309
+ * `CoolifyClient.deploy` method-for-method. A step that records an applied
310
+ * hostname/gitRef after a deploy uses {@link CoolifyPort.deployAndWait}
311
+ * instead (#252) — `deploy-health.ts`'s `waitForNewDeployment` is the one
312
+ * shared helper every such step calls, never this method directly.
301
313
  */
302
314
  deploy(uuid: string, force?: boolean): Promise<unknown>;
303
315
  /**
@@ -361,6 +373,27 @@ export interface CoolifyPort {
361
373
  }): Promise<{
362
374
  uuid: string;
363
375
  }>;
376
+ /**
377
+ * Deploys and blocks until Coolify's own deployment record reaches a
378
+ * terminal state (#252) — matches `CoolifyClient.deployAndWait`
379
+ * method-for-method. Unlike `deploy` above, or unlike polling
380
+ * `getApplication`'s live `status`, this is the one honest answer to
381
+ * "did the new version actually land": a failed build leaves the previous
382
+ * container running and healthy, so `status` and a served request read
383
+ * identically whether the deploy succeeded or not (`CoolifyDeploymentRecord`'s
384
+ * own doc comment in `@wtfalch/providers`). `force` defaults to `true` on
385
+ * the real client for the same reason its own doc comment gives — every
386
+ * caller here is provisioning or releasing, where a wrong "it's applied"
387
+ * costs more than a warm build cache.
388
+ */
389
+ deployAndWait(uuid: string, options?: {
390
+ force?: boolean;
391
+ attempts?: number;
392
+ intervalMs?: number;
393
+ }): Promise<{
394
+ deploymentUuid: string;
395
+ status: string;
396
+ }>;
364
397
  }
365
398
  /**
366
399
  * The Cloudflare calls this executor makes, shaped to match
@@ -812,7 +845,7 @@ export interface ZitadelPort {
812
845
  givenName: string;
813
846
  familyName: string;
814
847
  email: string;
815
- }): Promise<{
848
+ }, urlTemplate?: string): Promise<{
816
849
  userId: string;
817
850
  created: boolean;
818
851
  }>;
@@ -1025,16 +1058,6 @@ export interface ZitadelListAppsRawApp {
1025
1058
  * `signManagementRequest`/`AssertionTrust`). A host can construct this without
1026
1059
  * any human session: it only needs the Ed25519 private key (`SigningIdentity`)
1027
1060
  * an operator's `MANAGEMENT_PUBLIC_KEYS`/`MANAGEMENT_ISSUER` pins.
1028
- *
1029
- * #177: `setQuota`/`setBudget` (`files/packages/sdk/src/client.ts:154-166`)
1030
- * used to live on a separate `FilesOrgLimits` a host built per org through a
1031
- * `filesLimitsFor(orgId)` factory on `FoundryStepExecutorPorts` — ceremony
1032
- * left over from nothing but currying an id this port's own callers already
1033
- * have in hand. The signed claim's `organisation` field selects the org per
1034
- * call, so one shared keypair signs for every org already, the same signer
1035
- * `provisionOrganisation` uses; there was never a per-org client to build,
1036
- * only an org id to pass. Both methods now take it explicitly, like
1037
- * `provisionOrganisation` always did, and `filesLimitsFor` is gone.
1038
1061
  */
1039
1062
  export interface FilesOrganisationPort {
1040
1063
  provisionOrganisation(input: {
@@ -1064,9 +1087,6 @@ export interface FilesOrganisationPort {
1064
1087
  secret: string;
1065
1088
  };
1066
1089
  }>;
1067
- /** #27, #177: the org's own spend limit on Files -- see this interface's own doc comment for why `organisationId` is explicit here. */
1068
- setQuota(organisationId: string, limitBytes: string | null): Promise<unknown>;
1069
- setBudget(organisationId: string, limitMicros: string | null): Promise<unknown>;
1070
1090
  /**
1071
1091
  * F6b: mints the organisation's own key, matching files'
1072
1092
  * `POST /v1/manage/organisations/:organisationId/keys` (SDK
@@ -1227,10 +1247,10 @@ export interface AiOrganisationPort {
1227
1247
  * `ai/packages/sdk/src/client.ts:94-101`'s `updateBudget` (`UpdateBudget`
1228
1248
  * from `ai/packages/sdk/src/types.ts:134-137`). Used to live on a separate
1229
1249
  * `AiOrgLimits` a host built per org through an `aiLimitsFor(orgId)`
1230
- * factory on `FoundryStepExecutorPorts` -- the same dead currying
1231
- * {@link FilesOrganisationPort}'s own doc comment explains for
1232
- * `setQuota`/`setBudget`. `organisationId` is explicit here now, like
1233
- * every other method on this port.
1250
+ * factory on `FoundryStepExecutorPorts` -- ceremony left over from nothing
1251
+ * but currying an id this port's own callers already have in hand.
1252
+ * `organisationId` is explicit here now, like every other method on this
1253
+ * port.
1234
1254
  */
1235
1255
  updateBudget(organisationId: string, input: {
1236
1256
  keyId: string | null;
@@ -1341,6 +1361,16 @@ export interface FoundryStepExecutorPorts {
1341
1361
  * themselves.
1342
1362
  */
1343
1363
  deliverSecret(ref: string, secret: string): Promise<void>;
1364
+ /**
1365
+ * Archon's held keys, for an entry env from `sharedSecret` (read, else
1366
+ * generate and `put`) and `platformMailKey` (read only). Optional: without
1367
+ * it, or without `put`, such an env is omitted and the plan reports it
1368
+ * blocked. A value goes to the entry's env and nowhere else.
1369
+ */
1370
+ heldKeys?: {
1371
+ resolve(ref: string): Promise<string | null | undefined>;
1372
+ put?(ref: string, value: string): Promise<void>;
1373
+ };
1344
1374
  /**
1345
1375
  * Read access to the declared-org store, narrowed to the one query
1346
1376
  * `cloudflareZone` needs for its cross-org refusal check (#131) -- see that
@@ -1560,6 +1590,20 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1560
1590
  private findRecreatedApp;
1561
1591
  private ensureZitadelProject;
1562
1592
  private ensureCoolifyProject;
1593
+ /**
1594
+ * #445: one company lands on one Coolify server. The first call for an
1595
+ * org picks a usable, reachable server and records it
1596
+ * ({@link COOLIFY_SERVER_SCOPE_KEY}); every later call -- this apply, or a
1597
+ * later one -- reads that recording back and returns the same server,
1598
+ * regardless of what the pool's order or health looks like by then. A
1599
+ * recorded server that has gone unusable or unreachable is reported
1600
+ * blocked (never silently replaced with a second host, which would split
1601
+ * the org's resources across servers and break their internal container
1602
+ * URLs); a recorded server no longer registered at all is the same.
1603
+ * Deliberately no fallback to `servers[0]` when nothing is usable: an
1604
+ * unusable server picked blind is not a server anyone's resources could
1605
+ * actually be created on.
1606
+ */
1563
1607
  private resolveServer;
1564
1608
  private coolifyApp;
1565
1609
  /**
@@ -1636,6 +1680,13 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1636
1680
  * provisioned, where this one is unconditional (#239).
1637
1681
  */
1638
1682
  private pushAppEnv;
1683
+ /**
1684
+ * The org's shared secret `name`: read from the held keys, else generated
1685
+ * and held first, so a crash after the `put` and before the env write
1686
+ * hands the same value out on the next apply. Only called when `put` exists.
1687
+ * An error here names the ref, never a value.
1688
+ */
1689
+ private ensureSharedSecret;
1639
1690
  /**
1640
1691
  * F5: the app's `AUTH_APP_KEY`, minted once and only when the app has none
1641
1692
  * of at least 32 characters (the broker refuses a shorter one). `null` once
@@ -1897,6 +1948,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1897
1948
  * `coolifyApp`'s own analogous gap (it never restarted a found-but-stopped
1898
1949
  * app either) has since closed the same way, inline in that method (#130);
1899
1950
  * this method is `cmsSite`'s only caller.
1951
+ *
1952
+ * `repo` (#245) lets the site app build from an org's own skin repo
1953
+ * (`org.site.repo`) instead of the shared `CMS_REPO`; `cmsSite` always
1954
+ * passes `CMS_REPO` for the cms admin app, which is never org-specific.
1900
1955
  */
1901
1956
  private findOrCreateCmsApp;
1902
1957
  /**
@@ -2050,6 +2105,19 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2050
2105
  * pushed once calls are on and this org's calls service app carries its
2051
2106
  * internal name (`desiredAgoraEnv` reads that marker from the recorded ids,
2052
2107
  * not the host values).
2108
+ *
2109
+ * #442: a found-but-already-running app's container binds env only at
2110
+ * start, so writing it alone leaves that process on its old config --
2111
+ * restarted (never redeployed: no new image, so `restart` is the same
2112
+ * primitive `ensureLogVolume`'s own restart uses) whenever the live env
2113
+ * actually differs, or whenever the last restart this step confirmed
2114
+ * ({@link AGORA_ACTIVATED_ENV_SCOPE_KEY}) is not this exact env (an org
2115
+ * applied before this existed, whose live env already matched desired
2116
+ * with nothing ever confirmed running it). A brand new app has nothing
2117
+ * running yet to restart. The activation marker is recorded last, after
2118
+ * the restart (if any) returns without throwing, so a failed restart
2119
+ * leaves it stale and the next plan replans this step even though
2120
+ * Coolify's own env store already matches desired.
2053
2121
  */
2054
2122
  private coolifyAgora;
2055
2123
  /**
@@ -2354,17 +2422,31 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2354
2422
  */
2355
2423
  private ensureLettermintDmarcRecord;
2356
2424
  /**
2357
- * #80: provisions the org's own organisation on Files, before
2358
- * {@link filesStorage} can set its spend limit. `provisionOrganisation` is
2359
- * safely repeatable on its own (`FilesOrganisationPort`'s doc comment), so
2360
- * this is called every time regardless of whether it already ran --
2361
- * idempotent by find-or-create, not by checking `recordedIds` first.
2425
+ * #80: provisions the org's own organisation on Files.
2426
+ * `provisionOrganisation` is safely repeatable on its own
2427
+ * (`FilesOrganisationPort`'s doc comment), so this is called every time
2428
+ * regardless of whether it already ran -- idempotent by find-or-create,
2429
+ * not by checking `recordedIds` first.
2430
+ *
2431
+ * F6b: `action.desired.shareHost` (`diff.ts`'s `planFilesOrganisation`) is
2432
+ * passed through every time, even though `entry-files-key.ts` reports the
2433
+ * route itself as blocked until the platform's files host can serve
2434
+ * `share.<domain>` -- this is the one place the name reaches files at all,
2435
+ * so files can act on it the moment its own route exists.
2362
2436
  *
2363
2437
  * #273: also ensures the org's Agora origin is on the files host's CORS
2364
2438
  * allowlist (`FilesOrganisationPort.ensureAllowedOrigin`), the same
2365
2439
  * repeat-is-a-no-op shape as `provisionOrganisation` itself -- every org
2366
2440
  * gets an Agora (#164), so `diff.ts`'s `planFilesOrganisation` always sets
2367
2441
  * `desired.agoraOrigin`.
2442
+ *
2443
+ * #437: the owner member/name, the core ZITADEL files project and the core
2444
+ * collection (`core-files.ts`) are no longer conditioned on a hosting entry
2445
+ * declaring `uses.files` -- the core bundle (Agora's own uploads) needs
2446
+ * all three just as much as a bespoke entry does, and withholding them
2447
+ * until one existed is why a plain Agora/Boule org never got a usable
2448
+ * Files setup at all. Both the project and the collection are idempotent
2449
+ * by name, so a re-run that already created either finds it instead.
2368
2450
  */
2369
2451
  private filesOrganisation;
2370
2452
  /**
@@ -2375,7 +2457,6 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2375
2457
  * that fails revokes the key again instead of leaving a live key nobody holds.
2376
2458
  */
2377
2459
  private deliverFilesAgoraKey;
2378
- private filesStorage;
2379
2460
  /**
2380
2461
  * #80: provisions the org's own organisation on AI and switches its AI use
2381
2462
  * on, before {@link aiBudget} can set its own spend limit. Both