@wtfalch/foundry 0.9.0 → 0.11.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 (44) hide show
  1. package/dist/apply.d.ts +16 -0
  2. package/dist/index.d.ts +4 -3
  3. package/dist/index.js +1 -1
  4. package/dist/migrations/0030_hosting_entry_hostnames.sql +24 -0
  5. package/dist/migrations/0031_adoptions.sql +37 -0
  6. package/dist/org.d.ts +49 -4
  7. package/dist/org.js +134 -1
  8. package/dist/plan/adopt.d.ts +112 -0
  9. package/dist/plan/adopt.js +252 -0
  10. package/dist/plan/adoption-vault.d.ts +9 -0
  11. package/dist/plan/adoption-vault.js +1 -0
  12. package/dist/plan/agora.d.ts +7 -0
  13. package/dist/plan/agora.js +9 -0
  14. package/dist/plan/broker-entries.d.ts +108 -0
  15. package/dist/plan/broker-entries.js +214 -0
  16. package/dist/plan/cloudflared-connector.d.ts +10 -2
  17. package/dist/plan/cloudflared-connector.js +24 -16
  18. package/dist/plan/diff.d.ts +14 -0
  19. package/dist/plan/diff.js +488 -47
  20. package/dist/plan/executor.d.ts +141 -3
  21. package/dist/plan/executor.js +1100 -149
  22. package/dist/plan/hostnames.d.ts +24 -0
  23. package/dist/plan/hostnames.js +56 -0
  24. package/dist/plan/index.d.ts +2 -0
  25. package/dist/plan/index.js +1 -0
  26. package/dist/plan/locked-hostnames.d.ts +8 -0
  27. package/dist/plan/locked-hostnames.js +26 -0
  28. package/dist/plan/login-host.d.ts +1 -1
  29. package/dist/plan/login-host.js +5 -2
  30. package/dist/plan/plan-apply.d.ts +63 -3
  31. package/dist/plan/plan-apply.js +163 -5
  32. package/dist/plan/platform-diff.d.ts +66 -13
  33. package/dist/plan/platform-diff.js +117 -15
  34. package/dist/plan/platform-providers.d.ts +95 -2
  35. package/dist/plan/platform-providers.js +393 -27
  36. package/dist/plan/providers-readers.d.ts +8 -1
  37. package/dist/plan/providers-readers.js +58 -4
  38. package/dist/plan.d.ts +6 -0
  39. package/dist/store/postgres.d.ts +6 -1
  40. package/dist/store/postgres.js +80 -5
  41. package/dist/store/schema.d.ts +162 -0
  42. package/dist/store/schema.js +19 -1
  43. package/dist/store.d.ts +37 -0
  44. package/package.json +1 -1
@@ -1,6 +1,10 @@
1
1
  import type { OrgMailOptions, OrgMailOrg, OrgMailResult } from '@wtfalch/email/postmaster';
2
2
  import type { DeclaredOrg, HostingBuildPack } from '../org.js';
3
- import type { DeclaredOrgStore } from '../store.js';
3
+ import type { UnmanagedItem } from '../plan.js';
4
+ import type { Adoption, AdoptionStore, DeclaredOrgStore } from '../store.js';
5
+ import { type IngressRoute } from './adopt.js';
6
+ import type { AdoptionVault } from './adoption-vault.js';
7
+ import { type BrokerPort } from './broker-entries.js';
4
8
  import type { StepAction } from './diff.js';
5
9
  import { type LoginHostBindingsPort } from './login-host.js';
6
10
  import { type MailCopyPort } from './mail-copy.js';
@@ -97,6 +101,19 @@ export interface CoolifyPort {
97
101
  domains?: string;
98
102
  /** Selects which of two Dockerfiles in one repo to build (`cmsSite`'s own site/cms apps). */
99
103
  dockerfile_location?: string;
104
+ /** The port the app listens on (`HostingEntry.port`). */
105
+ ports_exposes?: string;
106
+ /** `false` for an internal entry: Coolify makes no domain for it. */
107
+ autogenerate_domain?: boolean;
108
+ /** The folder a `static` entry serves (`HostingEntry.folder`). */
109
+ publish_directory?: string;
110
+ /** Whether a push to `git_branch` deploys (`HostingEntry.autoDeploy`). */
111
+ is_auto_deploy_enabled?: boolean;
112
+ /**
113
+ * The app's folder in the repo (`HostingEntry.folder`). Coolify's create
114
+ * endpoint takes it; `@wtfalch/providers` forwards the body as it is.
115
+ */
116
+ base_directory?: string;
100
117
  }): Promise<{
101
118
  uuid: string;
102
119
  }>;
@@ -167,9 +184,13 @@ export interface CoolifyPort {
167
184
  * `is_preview === false` the same way a deployed-config reader must.
168
185
  */
169
186
  listEnvs(applicationUuid: string): Promise<Array<{
187
+ uuid?: string;
170
188
  key: string;
171
189
  value: string;
172
190
  is_preview: boolean;
191
+ is_buildtime?: boolean;
192
+ is_literal?: boolean;
193
+ is_multiline?: boolean;
173
194
  }>>;
174
195
  /** Escape hatch (`CoolifyClient.raw`) — used for stop/start, which have no typed method yet. */
175
196
  raw(method: string, path: string, body?: unknown): Promise<unknown>;
@@ -316,12 +337,20 @@ export interface CoolifyPort {
316
337
  * `CoolifyClient.listDatabaseBackups` method-for-method.
317
338
  * `provisionPlatformBackup` reads this to decide whether a database
318
339
  * already has an enabled, `save_s3` schedule before creating a second one.
340
+ * `s3_storage_uuid` (#257 fix round 3) is the field a replaced storage's
341
+ * repoint compares against -- Coolify's "Update database backup" operation
342
+ * (https://coolify.io/docs/api-reference/api/operations/update-database-backup)
343
+ * documents it as a field of the same scheduled-backup resource this list
344
+ * returns, but the list endpoint's own response has not been checked
345
+ * against a live instance, so it is optional here rather than assumed
346
+ * present.
319
347
  */
320
348
  listDatabaseBackups(databaseUuid: string): Promise<Array<{
321
349
  uuid: string;
322
350
  frequency: string;
323
351
  enabled: boolean;
324
352
  save_s3: boolean;
353
+ s3_storage_uuid?: string;
325
354
  }>>;
326
355
  /**
327
356
  * #257: schedules dumps of a database to the registered S3 storage --
@@ -358,6 +387,18 @@ export interface CloudflarePort {
358
387
  name: string;
359
388
  }>;
360
389
  putTunnelConfig(tunnelId: string, config: unknown): Promise<unknown>;
390
+ /**
391
+ * #312: the tunnel's live configuration, `config` null for a tunnel never
392
+ * configured. Matches `@wtfalch/providers`' `CloudflareClient.getTunnelConfig`.
393
+ * `cloudflareTunnel` reads it on every apply of an adopted tunnel, to merge
394
+ * its routes into the live ingress list rather than overwrite it.
395
+ */
396
+ getTunnelConfig(tunnelId: string): Promise<{
397
+ config: {
398
+ ingress: IngressRoute[];
399
+ [field: string]: unknown;
400
+ } | null;
401
+ }>;
361
402
  /**
362
403
  * The tunnel's connector token (#197) -- a `cloudflared` container reads
363
404
  * this at boot to dial into the tunnel. A live credential: matches
@@ -577,14 +618,17 @@ export interface MailPort {
577
618
  * (#162). The host implements this with `@wtfalch/email/postmaster`'s own
578
619
  * `createMailbox`, resolving `org`'s domain to a `domainId` the same way
579
620
  * `applyOrgMail` does -- that function already refuses a duplicate `local`
580
- * on the resolved domain, so this port method never has to. `ensureOwnerMailbox`
621
+ * on the resolved domain, so this port method never has to. `options` is
622
+ * the same `{ apex, mailHost }` `applyOrgMail`/`parkOrgMail` take, needed
623
+ * for the same reason: `@wtfalch/email/postmaster`'s `createMailbox` takes
624
+ * it to resolve the shared mail apex (#335). `ensureOwnerMailbox`
581
625
  * and `ensureRoleMailboxes` (#230) below are this executor's callers, and each checks its own recorded scope
582
626
  * id first, so a second apply never calls this at all.
583
627
  */
584
628
  createMailbox(org: OrgMailOrg, mailbox: {
585
629
  address: string;
586
630
  name: string;
587
- }): Promise<void>;
631
+ }, options: OrgMailOptions): Promise<void>;
588
632
  /**
589
633
  * Upserts this org's own outbound relay route on Stalwart (#149): an
590
634
  * `MtaRoute` through Lettermint, restricted to `org`'s domain, keyed by
@@ -1134,6 +1178,13 @@ export interface FoundryStepExecutorPorts {
1134
1178
  * or repointed without the binding.
1135
1179
  */
1136
1180
  loginHostBindings?: LoginHostBindingsPort;
1181
+ /**
1182
+ * F5: the broker's `AUTH_APPS`, where an entry with `uses.auth` gets its own
1183
+ * entry. Optional the same way `loginHostBindings` is: without it the plan
1184
+ * reports each broker entry blocked (the host also sets
1185
+ * `FoundryPlanApplyOptions.broker`).
1186
+ */
1187
+ broker?: BrokerPort;
1137
1188
  /**
1138
1189
  * Hands a just-minted, one-time secret (today: an API app's
1139
1190
  * `clientSecret`) to the host, immediately after the provider returns it
@@ -1156,6 +1207,17 @@ export interface FoundryStepExecutorPorts {
1156
1207
  * passes the same store instance it constructs `FoundryPlanApply` with.
1157
1208
  */
1158
1209
  declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone'>;
1210
+ /**
1211
+ * #311: where an adoption keeps the env values of the live app it takes
1212
+ * over. Optional with `adoptionStore`: without both, the planner reports
1213
+ * adoption as blocked and plans no `coolify:adopt` step
1214
+ * (`FoundryPlanApplyOptions`).
1215
+ */
1216
+ adoptionVault?: AdoptionVault;
1217
+ /** #311: the rows that hold the non-secret part of each snapshot. */
1218
+ adoptionStore?: AdoptionStore;
1219
+ /** #311: the clock of a snapshot's `takenAt`; the host passes `FoundryPlanApply`'s own. Defaults to the real clock. */
1220
+ now?: () => Date;
1159
1221
  }
1160
1222
  /**
1161
1223
  * Thrown by name for a step this executor deliberately does not implement:
@@ -1326,6 +1388,49 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1326
1388
  private ensureZitadelProject;
1327
1389
  private ensureCoolifyProject;
1328
1390
  private resolveServer;
1391
+ /** The live app Coolify holds under `uuid`. The error names the id only (rule 6). */
1392
+ private readLiveApp;
1393
+ /** The live ingress routes of the adopted tunnel `id`. The error names the id only (rule 6). */
1394
+ private readAdoptedIngress;
1395
+ /**
1396
+ * #311: for each adopted resource, the lines its `coolify:adopt` step shows
1397
+ * (a setting with old value beside new; an env var by name only) and what
1398
+ * is blocked. Read from the live app on every plan. No value of an env var
1399
+ * leaves this method (rule 6).
1400
+ */
1401
+ adoptionReport(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): Promise<{
1402
+ details: Record<string, string[]>;
1403
+ blocked: UnmanagedItem[];
1404
+ withheld: string[];
1405
+ }>;
1406
+ /**
1407
+ * #311: puts each adopted app back as its snapshot recorded it, touching
1408
+ * only what foundry writes: the settings it patches and the env names it
1409
+ * owns (an edit a person made to anything else stays). Owned names the
1410
+ * snapshot has are written back with its value and flags; owned names it
1411
+ * lacks are deleted; preview rows are left alone. Every vault entry and
1412
+ * live read happens first, so a missing entry or a row that cannot be
1413
+ * deleted refuses before any Coolify write. Errors name ids only, and the
1414
+ * vault's env values go to `bulkUpsertEnvs` and nowhere else (rule 6).
1415
+ */
1416
+ restoreAdoption(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, rows: readonly Adoption[]): Promise<{
1417
+ envDeleted: string[];
1418
+ tokenLeft: string[];
1419
+ }>;
1420
+ /**
1421
+ * #311: records what a live app or database looked like before foundry
1422
+ * first writes to it, in this order: the env values to the vault, the
1423
+ * non-secret settings to the row, then both read back and compared. The
1424
+ * step writes nothing to Coolify, so a snapshot that fails stops the apply
1425
+ * before any later step does. Every error names ids only (rule 6).
1426
+ */
1427
+ private coolifyAdopt;
1428
+ /** #311: whether the resource was adopted: its row is `adopted` or `confirmed`, or `id` is a declared adopt uuid. */
1429
+ private isAdopted;
1430
+ /** #311: refuses to manage as foundry's own a resource whose row protects its key and whose declaration has no `adopt`. */
1431
+ private refuseTakeover;
1432
+ /** #311: refuses to write to a declared adopt uuid that has no active snapshot. */
1433
+ private requireSnapshot;
1329
1434
  private coolifyApp;
1330
1435
  /**
1331
1436
  * #75: mounts a persistent volume at {@link LOG_DIR} on the app, unless one
@@ -1401,6 +1506,21 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1401
1506
  * provisioned, where this one is unconditional (#239).
1402
1507
  */
1403
1508
  private pushAppEnv;
1509
+ /**
1510
+ * F5: the app's `AUTH_APP_KEY`, minted once and only when the app has none
1511
+ * of at least 32 characters (the broker refuses a shorter one). `null` once
1512
+ * it has one: the app's env is where the key lives, and
1513
+ * `coolifyBrokerEntry` reads it back from there. It goes to this env and
1514
+ * the broker's, nowhere else.
1515
+ */
1516
+ private ensureAuthAppKey;
1517
+ /**
1518
+ * `plan/diff.ts`'s `planBrokerEntries` (F5): the entry's `AUTH_APPS` entry,
1519
+ * written by `writeBrokerEntry` with the key `pushAppEnv` put in the app's
1520
+ * env. Records what it wrote (never the key) and any foreign entry the
1521
+ * broker would drop.
1522
+ */
1523
+ private coolifyBrokerEntry;
1404
1524
  /**
1405
1525
  * `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
1406
1526
  * only the first time (task brief), never regenerated or overwritten, so a
@@ -1774,6 +1894,24 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1774
1894
  /** `deliverSecret`, tried up to {@link JOBS_DELIVERY_ATTEMPTS} times; true once one attempt succeeds. */
1775
1895
  private deliverWithRetry;
1776
1896
  private cloudflareTunnel;
1897
+ /**
1898
+ * Runs `call`; for an adopted resource a failure that is not one of this
1899
+ * file's own messages (which name hostnames and record types only) is
1900
+ * replaced by `label`, because a client's error text carries response
1901
+ * bodies (rule 6, #312).
1902
+ */
1903
+ private guarded;
1904
+ /**
1905
+ * #312: writes the org's routes into the adopted tunnel's LIVE ingress list
1906
+ * (`mergeIngress`), not the snapshot, so a route a person added since is
1907
+ * kept too. The declared names go into the tunnel row's `written` only after
1908
+ * the write that carried them succeeded, so a crash in between leaves a
1909
+ * route taken as foreign and kept. Writes nothing when the merged list is the
1910
+ * live one. Errors name the tunnel id and the operation only.
1911
+ */
1912
+ private mergeAdoptedIngress;
1913
+ /** #312: replaces the `settings` of an adoption row, read fresh so no other key is lost. The state and `takenAt` stay. */
1914
+ private updateAdoptionSettings;
1777
1915
  /**
1778
1916
  * #236: when this tunnel's DNS was last published under a domain that
1779
1917
  * differs from the one `org` now declares, deletes every CNAME in the