@wtfalch/foundry 0.32.0 → 0.34.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 (74) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +18 -1
  2. package/dist/bootstrap/bootstrap.js +167 -28
  3. package/dist/bootstrap/declare.d.ts +5 -1
  4. package/dist/bootstrap/declare.js +3 -1
  5. package/dist/bootstrap/index.d.ts +1 -1
  6. package/dist/bootstrap/versions.d.ts +4 -0
  7. package/dist/calls-sfu.d.ts +10 -0
  8. package/dist/calls-sfu.js +41 -0
  9. package/dist/index.d.ts +18 -8
  10. package/dist/index.js +8 -3
  11. package/dist/migrations/0051_org_services.sql +13 -0
  12. package/dist/migrations/0052_platform_mail_relay.sql +8 -0
  13. package/dist/org.d.ts +24 -2
  14. package/dist/org.js +42 -0
  15. package/dist/plan/boule.d.ts +24 -1
  16. package/dist/plan/boule.js +29 -1
  17. package/dist/plan/calls-matrix.d.ts +9 -0
  18. package/dist/plan/calls-matrix.js +17 -0
  19. package/dist/plan/cloudflared-connector.d.ts +7 -1
  20. package/dist/plan/cloudflared-connector.js +14 -1
  21. package/dist/plan/diff.d.ts +32 -1
  22. package/dist/plan/diff.js +383 -26
  23. package/dist/plan/executor.d.ts +177 -7
  24. package/dist/plan/executor.js +738 -51
  25. package/dist/plan/health-check-settings.d.ts +28 -0
  26. package/dist/plan/health-check-settings.js +36 -0
  27. package/dist/plan/health-gaps.d.ts +33 -0
  28. package/dist/plan/health-gaps.js +89 -0
  29. package/dist/plan/health-targets.js +1 -1
  30. package/dist/plan/held-key-refs.d.ts +10 -0
  31. package/dist/plan/held-key-refs.js +10 -0
  32. package/dist/plan/index.d.ts +16 -6
  33. package/dist/plan/index.js +8 -3
  34. package/dist/plan/live.d.ts +5 -2
  35. package/dist/plan/live.js +15 -2
  36. package/dist/plan/locked-hostnames.d.ts +5 -2
  37. package/dist/plan/locked-hostnames.js +9 -1
  38. package/dist/plan/login-host.d.ts +62 -2
  39. package/dist/plan/login-host.js +104 -1
  40. package/dist/plan/mail-relay.d.ts +120 -0
  41. package/dist/plan/mail-relay.js +323 -0
  42. package/dist/plan/org-names.d.ts +24 -0
  43. package/dist/plan/org-names.js +52 -0
  44. package/dist/plan/own-diff.d.ts +169 -0
  45. package/dist/plan/own-diff.js +432 -0
  46. package/dist/plan/own-dns.d.ts +6 -0
  47. package/dist/plan/own-dns.js +15 -0
  48. package/dist/plan/own-limits.d.ts +22 -0
  49. package/dist/plan/own-limits.js +101 -0
  50. package/dist/plan/own-services.d.ts +193 -0
  51. package/dist/plan/own-services.js +167 -0
  52. package/dist/plan/own-sfu.d.ts +69 -0
  53. package/dist/plan/own-sfu.js +255 -0
  54. package/dist/plan/plan-apply.d.ts +101 -7
  55. package/dist/plan/plan-apply.js +216 -12
  56. package/dist/plan/platform-diff.d.ts +61 -12
  57. package/dist/plan/platform-diff.js +140 -34
  58. package/dist/plan/platform-org.d.ts +36 -0
  59. package/dist/plan/platform-org.js +96 -2
  60. package/dist/plan/platform-providers.d.ts +407 -12
  61. package/dist/plan/platform-providers.js +947 -422
  62. package/dist/plan/sign-in-org.d.ts +99 -0
  63. package/dist/plan/sign-in-org.js +151 -0
  64. package/dist/plan/zitadel-login-policy.js +1 -1
  65. package/dist/plan/zitadel-system-api.d.ts +22 -1
  66. package/dist/plan/zitadel-system-api.js +49 -8
  67. package/dist/plan.d.ts +18 -0
  68. package/dist/platform.d.ts +8 -0
  69. package/dist/slug.js +1 -0
  70. package/dist/store/postgres.js +51 -2
  71. package/dist/store/schema.d.ts +51 -0
  72. package/dist/store/schema.js +5 -0
  73. package/package.json +1 -1
  74. package/versions.json +3 -1
@@ -12,8 +12,10 @@ 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';
18
+ export declare function coolifyProjectMark(org: Pick<DeclaredOrg, 'id'>): string;
17
19
  /**
18
20
  * The Coolify calls this executor makes, shaped to match
19
21
  * `@wtfalch/providers/coolify`'s `CoolifyClient` method-for-method — a real
@@ -36,6 +38,12 @@ export interface CoolifyPort {
36
38
  name: string;
37
39
  description?: string | null;
38
40
  } | null>;
41
+ /** A project's environments (`CoolifyClient.listEnvironments`), read-only. */
42
+ listEnvironments(projectUuid: string): Promise<Array<{
43
+ id: number;
44
+ uuid: string;
45
+ name: string;
46
+ }>>;
39
47
  createProject(name: string, description?: string): Promise<{
40
48
  uuid: string;
41
49
  }>;
@@ -100,6 +108,9 @@ export interface CoolifyPort {
100
108
  custom_network_aliases?: string | null;
101
109
  health_check_enabled?: boolean;
102
110
  ports_exposes?: string;
111
+ /** Coolify's own record of the resource limits (`plan/own-limits.ts`). */
112
+ limits_memory?: string;
113
+ limits_cpus?: string;
103
114
  }>;
104
115
  createGithubAppApplication(input: {
105
116
  project_uuid: string;
@@ -155,6 +166,8 @@ export interface CoolifyPort {
155
166
  uuid: string;
156
167
  internal_db_url?: string;
157
168
  status?: string;
169
+ limits_memory?: string;
170
+ limits_cpus?: string;
158
171
  }>;
159
172
  /**
160
173
  * Every database Coolify holds. `coolifyDatabase` finds its own by name and
@@ -878,10 +891,22 @@ export interface ZitadelPort {
878
891
  givenName: string;
879
892
  familyName: string;
880
893
  email: string;
881
- }, urlTemplate?: string): Promise<{
894
+ username?: string;
895
+ }, urlTemplate?: string, returnCode?: boolean): Promise<{
882
896
  userId: string;
883
897
  created: boolean;
898
+ inviteCode?: string;
884
899
  }>;
900
+ /** Adds a domain to the org. Idempotent: a domain already on the org is a no-op. */
901
+ addOrgDomain(orgId: string, domain: string): Promise<void>;
902
+ /** Makes a domain already on the org its primary one. Idempotent: no-op when it already is. */
903
+ setPrimaryOrgDomain(orgId: string, domain: string): Promise<void>;
904
+ setUserMetadata(orgId: string, userId: string, key: string, value: string): Promise<void>;
905
+ getUserMetadata(orgId: string, userId: string, key: string): Promise<string | null>;
906
+ findUserByEmail(orgId: string, email: string): Promise<{
907
+ userId: string;
908
+ state: string;
909
+ } | null>;
885
910
  /**
886
911
  * Adds a role to a project. Idempotent: a role that already exists is not
887
912
  * an error. `@wtfalch/providers`' `ZitadelClient` has no project-role API,
@@ -1125,6 +1150,16 @@ export interface ZitadelListAppsRawApp {
1125
1150
  * an operator's `MANAGEMENT_PUBLIC_KEYS`/`MANAGEMENT_ISSUER` pins.
1126
1151
  */
1127
1152
  export interface FilesOrganisationPort {
1153
+ /**
1154
+ * An own files host's R2 storage key, sent to that host once (the files route
1155
+ * `PUT /v1/manage/r2-credentials`). Absent on a port that cannot: the own R2 key step is then
1156
+ * blocked by name. The same pair may be sent again after a failed send.
1157
+ */
1158
+ storeR2Credentials?(input: {
1159
+ accessKeyId: string;
1160
+ secretAccessKey: string;
1161
+ tokenId: string;
1162
+ }): Promise<void>;
1128
1163
  provisionOrganisation(input: {
1129
1164
  organisationId: string;
1130
1165
  ceilingBytes: string | null;
@@ -1428,6 +1463,12 @@ export interface PlatformOrgPortSource {
1428
1463
  }
1429
1464
  export interface FoundryStepExecutorPorts {
1430
1465
  coolify: CoolifyPort;
1466
+ /**
1467
+ * Ports for the instances an own org (`services.own`) runs itself. Without
1468
+ * it, a step for an own org is refused rather than run against the shared
1469
+ * services, and the planner reports the org's own services blocked.
1470
+ */
1471
+ ownServices?: OwnServicesPort;
1431
1472
  zitadel: ZitadelPort;
1432
1473
  mail: MailPort;
1433
1474
  lettermint: LettermintPort;
@@ -1456,6 +1497,16 @@ export interface FoundryStepExecutorPorts {
1456
1497
  * compilation over one dependency nothing else needs.
1457
1498
  */
1458
1499
  mailCopy?: MailCopyPort;
1500
+ /**
1501
+ * Mails an owner's first-sign-in link to their personal address. The link
1502
+ * carries the one-time invite code `inviteOrgOwner` returned, so an error
1503
+ * from `send` must not repeat `link`. Optional: without it the planner
1504
+ * reports each owner's invite as blocked, because the owner's work address
1505
+ * is not a mailbox they can open yet.
1506
+ */
1507
+ ownerInviteMail?: {
1508
+ send(to: string, link: string): Promise<void>;
1509
+ };
1459
1510
  /**
1460
1511
  * #463: the cross-homeserver copy chat's forward re-key needs -- see
1461
1512
  * {@link ChatRekeyPort}'s own doc comment. Optional the same way
@@ -1661,12 +1712,36 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1661
1712
  private edgeRateLimits;
1662
1713
  private edgeZones;
1663
1714
  private edgeWriteSlot;
1715
+ /** Secrets this executor minted or was handed; see `StepExecutor.secretsHandled`. */
1716
+ private readonly handledSecrets;
1664
1717
  constructor(ports: FoundryStepExecutorPorts);
1718
+ secretsHandled(): readonly string[];
1719
+ /**
1720
+ * An own org's step runs on a copy of this executor whose ZITADEL, mail,
1721
+ * broker, files, jobs and calls ports are the org's own. A port the org has
1722
+ * not got yet is absent or throws, never the shared one.
1723
+ */
1724
+ private executeScoped;
1725
+ /** Whether `ensureOwnAuthMailToken` can mint: the same two inputs it blocks on. */
1726
+ ownMailKeyMintable(): boolean;
1727
+ /** What the executor's own files dependencies supply, for the planner's blocked lines. */
1728
+ ownFilesDeps(): {
1729
+ keys: boolean;
1730
+ bucket: boolean;
1731
+ };
1732
+ /** Which ports the own files storage key step needs this executor has, for the planner's blocked line. */
1733
+ ownFilesR2Ports(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<{
1734
+ r2Keys: boolean;
1735
+ storeR2Credentials: boolean;
1736
+ }>;
1737
+ /** 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
+ ownFilesOrganisationPort(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
1665
1739
  /** #555: the edge-routed ports this executor was built with itself (they win over edge). */
1666
1740
  ownEdgePorts(): Partial<Record<EdgePortName, boolean>>;
1667
1741
  execute(action: StepAction, context: StepExecutionContext): Promise<{
1668
1742
  resourceId?: string;
1669
1743
  } | undefined>;
1744
+ private run;
1670
1745
  /**
1671
1746
  * `plan/diff.ts`'s `planAccess` (#246): puts a `hosting.access` rule's
1672
1747
  * hostname behind a Cloudflare Access app named
@@ -1765,12 +1840,30 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1765
1840
  private teardownWorker;
1766
1841
  private zitadelIssuer;
1767
1842
  /**
1768
- * `plan/diff.ts`'s `planOwner`: invites the org's owner into its ZITADEL
1769
- * org by email. Never logs, returns or throws the email itself -- only the
1770
- * invited user id, handed back as `resourceId` so `runSteps` records it
1771
- * under `zitadel:owner:<orgId>:<email>`, which is exactly what makes the next
1772
- * `plan()` call idempotent (`planOwner` plans nothing once that key is
1773
- * recorded).
1843
+ * `plan/diff.ts`'s `planOrgRegistrationClosed`: closes self-registration on
1844
+ * the recorded ZITADEL org, for an org made before foundry did this or whose
1845
+ * close failed in the issuer step. Recorded only after the close returned.
1846
+ */
1847
+ private zitadelRegistrationClosed;
1848
+ /**
1849
+ * `plan/diff.ts`'s `planOrgDomain`: registers the org's sign-in domain on
1850
+ * its recorded ZITADEL org and makes it primary, for an org whose ZITADEL
1851
+ * org was made before foundry did this. Records the domain only after both
1852
+ * calls succeeded.
1853
+ */
1854
+ private zitadelDomain;
1855
+ /**
1856
+ * `plan/diff.ts`'s `planOwners`: makes the owner's account in the org's
1857
+ * ZITADEL org and mails the first-sign-in link to their personal address.
1858
+ * The account's username is the local part of `mailboxAddress` and its email
1859
+ * is `mailboxAddress`, so the owner signs in as `name@<org domain>`; the
1860
+ * personal address is kept as the `personal_email` metadata. The invite code
1861
+ * ZITADEL returns goes only into that mail. Nothing here logs, returns or
1862
+ * throws the personal address or the code -- only the invited user id, handed
1863
+ * back as `resourceId` so `runSteps` records it under
1864
+ * `zitadel:owner:<orgId>:<email>`, which is exactly what makes the next
1865
+ * `plan()` call idempotent. A failure after the account exists records
1866
+ * nothing: the next apply finds the account by username and mints a new code.
1774
1867
  */
1775
1868
  private zitadelOwner;
1776
1869
  /**
@@ -1992,6 +2085,14 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
1992
2085
  private bouleBrokerEntry;
1993
2086
  /** The shared tail of both broker-entry steps: read the app's key, write the entry, record what was written. */
1994
2087
  private writeAppBrokerEntry;
2088
+ /**
2089
+ * `null` once the app already has `AUTHZ_PEER_PASSWORD_OPERATOR`: minted only
2090
+ * the first time and never rotated, like {@link ensureAuthCookieSecret}.
2091
+ */
2092
+ private ensurePeerPassword;
2093
+ /** Same rule for the secret Archon's invite form sends to Boule. */
2094
+ private ensureBridgeSecret;
2095
+ private mintIfAbsent;
1995
2096
  /**
1996
2097
  * `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
1997
2098
  * only the first time (task brief), never regenerated or overwritten, so a
@@ -2131,6 +2232,22 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2131
2232
  * (which must never delete the platform's own project) change.
2132
2233
  */
2133
2234
  private ensureOrgLettermintRelay;
2235
+ /**
2236
+ * The company's own sign-in mail token: a Lettermint project `<slug>-auth`
2237
+ * whose token goes to `orgTarget(org).refs.brokerMailKey`, where the broker
2238
+ * and ZITADEL's SMTP settings read it. The platform-wide Lettermint key
2239
+ * stays with the executor and never reaches a company. A held ref is left
2240
+ * alone. Otherwise the mint rule of {@link ensureOrgAppMailToken}: create
2241
+ * the project, restrict the domain, deliver its token, and only then
2242
+ * delete every other `<slug>-auth` project (a token cannot be read back,
2243
+ * so a project without a held token is a leftover). The restriction comes
2244
+ * before the delivery so a failure there leaves no held ref: the next run
2245
+ * starts over instead of skipping it. A failure before the delivery leaves
2246
+ * that project for the next run to delete. Every Lettermint call is
2247
+ * bounded; the token is registered for redaction before anything can throw
2248
+ * with it.
2249
+ */
2250
+ private ensureOwnAuthMailToken;
2134
2251
  /**
2135
2252
  * Deletes a just-minted project from inside a `catch`. A failed delete is
2136
2253
  * dropped so it never replaces the error being rethrown; the orphan is
@@ -2626,6 +2743,54 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2626
2743
  */
2627
2744
  private zonePort;
2628
2745
  private jobsPort;
2746
+ /**
2747
+ * `plan/own-diff.ts`'s `planOwnJobs`: the org's own scheduler, in the org's
2748
+ * own project, with the names and the held-key ref of `orgTarget(org)`. The
2749
+ * admin token goes to `jobs:org:<slug>:adminToken` and nowhere else.
2750
+ */
2751
+ private ownJobs;
2752
+ /**
2753
+ * `plan/own-diff.ts`'s `planOwnFiles`: the org's own files host in the
2754
+ * org's own project, trusting the company's issuer `https://auth.<domain>`.
2755
+ * Its ZITADEL project `<slug>-files` and introspection app are made in the
2756
+ * company's instance (`ownServices.zitadelFor`), never the shared one. The
2757
+ * bucket comes from the host's bucket port under the target's name. The
2758
+ * management key goes to `files:org:<slug>:managementKey` and the
2759
+ * introspection secret to `zitadel:org:<slug>:files:introspection`, both
2760
+ * once, and neither is recorded or returned.
2761
+ */
2762
+ private ownFiles;
2763
+ /**
2764
+ * `plan/own-diff.ts`'s `planOwnFilesR2`: the storage key of the org's own files bucket. A token
2765
+ * recorded as undelivered is deleted first (its secret cannot be read back, so it can never be
2766
+ * finished), then one is minted, registered as a secret, recorded by id as undelivered and
2767
+ * persisted, and only then sent to the files host, the same pair on every retry. The secret is
2768
+ * never recorded and never written to a Coolify env. The id is recorded once the host has it.
2769
+ */
2770
+ private ownFilesR2;
2771
+ /**
2772
+ * `plan/own-diff.ts`'s `planOwnAuth`: the org's own sign-in at
2773
+ * `auth.<domain>`, in the org's own project, through the org's own edge
2774
+ * routed DNS and tunnel ports. The instance owner token goes to
2775
+ * `zitadel:org:<slug>:instanceBearer` and is never recorded. A changed
2776
+ * declared auth ref patches both git apps' branch and redeploys them; the
2777
+ * applied marker comes last, so a run that died halfway is planned again.
2778
+ */
2779
+ private ownAuth;
2780
+ /**
2781
+ * `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
2784
+ * `orgTarget(org).refs.mailAdminKey` and nowhere else. The address the relay
2785
+ * routes to is recorded last, and the step's marker after it.
2786
+ */
2787
+ private ownMail;
2788
+ /**
2789
+ * `plan/own-diff.ts`'s `planOwnSfu`: the org's own LiveKit server in the
2790
+ * org's project, on the ports of its slot. The company's key pair goes to
2791
+ * `livekit:org:<slug>:keys` and the app's env, nowhere else.
2792
+ */
2793
+ private ownSfu;
2629
2794
  private jobsRegistration;
2630
2795
  /** `deliverSecret`, tried up to {@link JOBS_DELIVERY_ATTEMPTS} times; true once one attempt succeeds. */
2631
2796
  private deliverWithRetry;
@@ -2879,6 +3044,11 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2879
3044
  * #491: when `mailDomain` is the platform's own apex, this must never
2880
3045
  * delete the platform's own project (the delete loop skips it by id) or
2881
3046
  * the platform's own domain (returns before the domain lookup/delete).
3047
+ *
3048
+ * An org whose own sign-in is applied keeps its `<slug>-auth` project, and
3049
+ * keeps the Lettermint domain when that is the domain the sign-in sends
3050
+ * from. Every other org deletes `<slug>-auth` too, which only a failed
3051
+ * first sign-in run can have left behind.
2882
3052
  */
2883
3053
  private parkLettermintMail;
2884
3054
  /**