@wtfalch/foundry 0.8.0 → 0.10.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.
@@ -1,6 +1,7 @@
1
1
  import type { OrgMailOptions, OrgMailOrg, OrgMailResult } from '@wtfalch/email/postmaster';
2
2
  import type { DeclaredOrg, HostingBuildPack } from '../org.js';
3
3
  import type { DeclaredOrgStore } from '../store.js';
4
+ import { type BrokerPort } from './broker-entries.js';
4
5
  import type { StepAction } from './diff.js';
5
6
  import { type LoginHostBindingsPort } from './login-host.js';
6
7
  import { type MailCopyPort } from './mail-copy.js';
@@ -97,6 +98,19 @@ export interface CoolifyPort {
97
98
  domains?: string;
98
99
  /** Selects which of two Dockerfiles in one repo to build (`cmsSite`'s own site/cms apps). */
99
100
  dockerfile_location?: string;
101
+ /** The port the app listens on (`HostingEntry.port`). */
102
+ ports_exposes?: string;
103
+ /** `false` for an internal entry: Coolify makes no domain for it. */
104
+ autogenerate_domain?: boolean;
105
+ /** The folder a `static` entry serves (`HostingEntry.folder`). */
106
+ publish_directory?: string;
107
+ /** Whether a push to `git_branch` deploys (`HostingEntry.autoDeploy`). */
108
+ is_auto_deploy_enabled?: boolean;
109
+ /**
110
+ * The app's folder in the repo (`HostingEntry.folder`). Coolify's create
111
+ * endpoint takes it; `@wtfalch/providers` forwards the body as it is.
112
+ */
113
+ base_directory?: string;
100
114
  }): Promise<{
101
115
  uuid: string;
102
116
  }>;
@@ -316,12 +330,20 @@ export interface CoolifyPort {
316
330
  * `CoolifyClient.listDatabaseBackups` method-for-method.
317
331
  * `provisionPlatformBackup` reads this to decide whether a database
318
332
  * already has an enabled, `save_s3` schedule before creating a second one.
333
+ * `s3_storage_uuid` (#257 fix round 3) is the field a replaced storage's
334
+ * repoint compares against -- Coolify's "Update database backup" operation
335
+ * (https://coolify.io/docs/api-reference/api/operations/update-database-backup)
336
+ * documents it as a field of the same scheduled-backup resource this list
337
+ * returns, but the list endpoint's own response has not been checked
338
+ * against a live instance, so it is optional here rather than assumed
339
+ * present.
319
340
  */
320
341
  listDatabaseBackups(databaseUuid: string): Promise<Array<{
321
342
  uuid: string;
322
343
  frequency: string;
323
344
  enabled: boolean;
324
345
  save_s3: boolean;
346
+ s3_storage_uuid?: string;
325
347
  }>>;
326
348
  /**
327
349
  * #257: schedules dumps of a database to the registered S3 storage --
@@ -1134,6 +1156,13 @@ export interface FoundryStepExecutorPorts {
1134
1156
  * or repointed without the binding.
1135
1157
  */
1136
1158
  loginHostBindings?: LoginHostBindingsPort;
1159
+ /**
1160
+ * F5: the broker's `AUTH_APPS`, where an entry with `uses.auth` gets its own
1161
+ * entry. Optional the same way `loginHostBindings` is: without it the plan
1162
+ * reports each broker entry blocked (the host also sets
1163
+ * `FoundryPlanApplyOptions.broker`).
1164
+ */
1165
+ broker?: BrokerPort;
1137
1166
  /**
1138
1167
  * Hands a just-minted, one-time secret (today: an API app's
1139
1168
  * `clientSecret`) to the host, immediately after the provider returns it
@@ -1198,7 +1227,7 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1198
1227
  } | undefined>;
1199
1228
  /**
1200
1229
  * `plan/diff.ts`'s `planAccess` (#84): puts the org's app hostname behind a
1201
- * Cloudflare Access app that lets only `hosting.accessEmails` through.
1230
+ * Cloudflare Access app that lets only the `app` entry's `accessEmails` through.
1202
1231
  * foundry names its Access app `foundry:<orgId>` and reuses only an app with
1203
1232
  * that name, so a re-run after a crash does not duplicate it. It refuses,
1204
1233
  * rather than adopt, when another Access app already holds the hostname:
@@ -1384,7 +1413,7 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1384
1413
  * P2, `.plans/2026-09-26-level-1-org-map.md`) -- a second `bulkUpsertEnvs`
1385
1414
  * call rather than folding into `pushDatabaseUrl`'s, since this one runs
1386
1415
  * unconditionally (`coolifyApp`) where that one runs only once
1387
- * `hosting.database` is declared and provisioned.
1416
+ * the `app` entry's `database` is declared and provisioned.
1388
1417
  *
1389
1418
  * `APP_URL` and `LOG_DIR` (#75) are pushed unconditionally; every other
1390
1419
  * value is pushed only once its own source step has actually recorded it
@@ -1397,10 +1426,25 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1397
1426
  * is its own design question, left out of #204's own "How" rather than
1398
1427
  * guessed) is out of scope here (task brief). `DATABASE_URL_OWNER`/
1399
1428
  * `DATABASE_URL` are pushed by `pushDatabaseUrl` instead, not here --
1400
- * that call runs only once `hosting.database` is declared and
1429
+ * that call runs only once the `app` entry's `database` is declared and
1401
1430
  * provisioned, where this one is unconditional (#239).
1402
1431
  */
1403
1432
  private pushAppEnv;
1433
+ /**
1434
+ * F5: the app's `AUTH_APP_KEY`, minted once and only when the app has none
1435
+ * of at least 32 characters (the broker refuses a shorter one). `null` once
1436
+ * it has one: the app's env is where the key lives, and
1437
+ * `coolifyBrokerEntry` reads it back from there. It goes to this env and
1438
+ * the broker's, nowhere else.
1439
+ */
1440
+ private ensureAuthAppKey;
1441
+ /**
1442
+ * `plan/diff.ts`'s `planBrokerEntries` (F5): the entry's `AUTH_APPS` entry,
1443
+ * written by `writeBrokerEntry` with the key `pushAppEnv` put in the app's
1444
+ * env. Records what it wrote (never the key) and any foreign entry the
1445
+ * broker would drop.
1446
+ */
1447
+ private coolifyBrokerEntry;
1404
1448
  /**
1405
1449
  * `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
1406
1450
  * only the first time (task brief), never regenerated or overwritten, so a
@@ -1793,15 +1837,27 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1793
1837
  * existed falls back to the old zone-wide pass, the one case where no
1794
1838
  * hostname list was ever written to look up instead.
1795
1839
  *
1796
- * `previousTunnelId` (round 4 finding 1): the old domain's CNAMEs were
1797
- * published under whatever tunnel id was live the *last* time this step
1798
- * recorded that domain, which is `previousTunnelId`, not necessarily
1799
- * `tunnelId` -- a tunnel recreated under the same name
1800
- * (`findTunnelByName` returning a new id) in the same apply that also
1801
- * changes the domain leaves the old domain's records pointed at the old
1802
- * id. Matching only `tunnelId`'s target left them live forever. Both
1803
- * targets are checked so this stays a no-op once a retry has already
1804
- * moved the old domain onto the new id.
1840
+ * `previousIds` (round 4 finding 1, round 6 finding 2): the old domain's
1841
+ * CNAMEs were published under whatever tunnel id was live the *last*
1842
+ * time this step recorded that domain, which may be any id this step has
1843
+ * ever replaced, not necessarily `tunnelId` -- a tunnel recreated under
1844
+ * the same name (`findTunnelByName` returning a new id), possibly more
1845
+ * than once with no successful publish in between, in the same apply
1846
+ * that also changes the domain leaves the old domain's records pointed
1847
+ * at an old id. Matching only `tunnelId`'s target left them live
1848
+ * forever; matching only the single most recent previous id (round 4)
1849
+ * still missed an older one two unresolved recreates back. Every target
1850
+ * is checked so this stays a no-op once a retry has already moved the
1851
+ * old domain onto the current id.
1852
+ *
1853
+ * Round 6 finding 1: once `previousHostnames` is non-empty, every name in
1854
+ * it has now been looked up -- deleted (its CNAME matched one of
1855
+ * `dnsTargets`), left alone (found but foreign), or never found at all
1856
+ * (already gone) -- so none of them needs tracking under `previousDomain`
1857
+ * in {@link CLOUDFLARE_TUNNEL_DNS_TRACKED_SCOPE_KEY} any longer; dropped
1858
+ * from that set accordingly. The zone-wide fallback path (no recorded
1859
+ * hostnames) never added entries of its own to begin with, so there is
1860
+ * nothing of this domain's left in that set either way.
1805
1861
  */
1806
1862
  private deleteStaleTunnelDnsRecords;
1807
1863
  /**
@@ -1828,13 +1884,15 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1828
1884
  * (round 4 finding 3), so a crash mid-write is recovered by re-running
1829
1885
  * this same idempotent step, never silently skipped.
1830
1886
  *
1831
- * Round 3: `previousTunnelId` is the tunnel id this same step recorded
1832
- * last time it ran (`cloudflareTunnel`'s own `cloudflare:tunnel:<key>`
1833
- * resource id, read before `tunnel` above is computed for this run) -- a
1834
- * tunnel Cloudflare shows as recreated under the same name
1835
- * (`findTunnelByName` returning a new id for `<slug>-tunnel`) leaves every
1836
- * CNAME still pointed at the *old* id. That is this org's own stale
1837
- * record, not a foreign one, so it is repointed rather than refused.
1887
+ * Round 3, round 6 finding 2: `previousIds` is every tunnel id this same
1888
+ * step has ever replaced (`cloudflareTunnel`'s own accumulated set,
1889
+ * captured before `tunnel` above is computed for this run) -- a tunnel
1890
+ * Cloudflare shows as recreated under the same name (`findTunnelByName`
1891
+ * returning a new id for `<slug>-tunnel`), possibly more than once with
1892
+ * no successful publish in between, leaves every CNAME still pointed at
1893
+ * one of those old ids. That is this org's own stale record, not a
1894
+ * foreign one, so it is repointed rather than refused, whichever of
1895
+ * `previousIds` it still names.
1838
1896
  *
1839
1897
  * Round 4 finding 2: `previousHostnames` is this domain's own previously
1840
1898
  * recorded hostname list (empty when the domain just changed --
@@ -1844,17 +1902,19 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1844
1902
  * `hostnames` below but was never cleaned up before this round: its CNAME
1845
1903
  * stayed live and its name stayed in the recorded list forever, because
1846
1904
  * only a domain *change* ever ran cleanup. Each dropped name's CNAME is
1847
- * deleted here when it still points at this tunnel or its immediately
1848
- * prior id; a name already gone, or now pointed at something foreign, is
1849
- * simply left out of the returned list -- nothing here is this method's to
1850
- * reclaim or report as a conflict.
1905
+ * deleted here when it still points at this tunnel or any of
1906
+ * `previousIds`; a name already gone, or now pointed at something
1907
+ * foreign, is simply left out of the returned list -- nothing here is
1908
+ * this method's to reclaim or report as a conflict.
1851
1909
  *
1852
- * Round 5 finding 1: `context` is used only to record
1853
- * {@link CLOUDFLARE_TUNNEL_DNS_PENDING_DOMAIN_SCOPE_KEY}/
1854
- * {@link CLOUDFLARE_TUNNEL_DNS_PENDING_HOSTNAMES_SCOPE_KEY} after each
1855
- * individual CNAME write below succeeds, so a crash or a thrown conflict
1856
- * partway through never leaves an already-created CNAME out of every
1857
- * recorded list -- `destroy` checks this pending pair too.
1910
+ * Round 5 finding 1, round 6 finding 1: `context` is used to add every
1911
+ * hostname this call confirms correct (settled, created or repointed) to
1912
+ * {@link CLOUDFLARE_TUNNEL_DNS_TRACKED_SCOPE_KEY} as soon as it is
1913
+ * confirmed, and to drop a dropped hostname from it once that hostname's
1914
+ * own CNAME is resolved one way or another -- additively, alongside
1915
+ * whatever other domains that shared set already names, never
1916
+ * overwriting them. `destroy` and {@link deleteStaleTunnelDnsRecords}
1917
+ * both read this set.
1858
1918
  */
1859
1919
  private publishTunnelDnsRecords;
1860
1920
  /**