@wtfalch/foundry 0.5.0 → 0.7.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.
- package/dist/bootstrap/bootstrap.d.ts +16 -1
- package/dist/bootstrap/bootstrap.js +40 -2
- package/dist/bootstrap/declare.d.ts +3 -1
- package/dist/bootstrap/declare.js +12 -3
- package/dist/bootstrap/versions.d.ts +17 -0
- package/dist/bootstrap/versions.js +12 -0
- package/dist/calls-sfu-deploy-support.d.ts +8 -0
- package/dist/calls-sfu-deploy-support.js +10 -1
- package/dist/calls-sfu.d.ts +36 -12
- package/dist/calls-sfu.js +42 -9
- package/dist/index.d.ts +10 -4
- package/dist/index.js +6 -3
- package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
- package/dist/migrations/0020_mail_apps.sql +7 -0
- package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
- package/dist/migrations/0022_chat_enabled.sql +6 -0
- package/dist/migrations/0023_hosting_app_env.sql +8 -0
- package/dist/migrations/0024_audit_events.sql +211 -0
- package/dist/migrations/0025_audit_display.sql +147 -0
- package/dist/migrations/0026_audit_erase_email_guard.sql +78 -0
- package/dist/migrations/0027_audit_chain.sql +170 -0
- package/dist/migrations/0028_audit_rls.sql +111 -0
- package/dist/org.d.ts +105 -0
- package/dist/org.js +101 -0
- package/dist/plan/agora.d.ts +21 -0
- package/dist/plan/agora.js +23 -0
- package/dist/plan/calls-matrix.d.ts +2 -2
- package/dist/plan/calls-matrix.js +3 -2
- package/dist/plan/cloudflared-connector.d.ts +3 -2
- package/dist/plan/cloudflared-connector.js +5 -3
- package/dist/plan/diff.d.ts +15 -2
- package/dist/plan/diff.js +399 -51
- package/dist/plan/executor.d.ts +307 -61
- package/dist/plan/executor.js +489 -129
- package/dist/plan/index.d.ts +9 -1
- package/dist/plan/index.js +5 -1
- package/dist/plan/login-host.d.ts +64 -0
- package/dist/plan/login-host.js +52 -0
- package/dist/plan/mail-copy.d.ts +125 -0
- package/dist/plan/mail-copy.js +166 -0
- package/dist/plan/mail-identity-allowlist.d.ts +50 -0
- package/dist/plan/mail-identity-allowlist.js +11 -0
- package/dist/plan/mail-identity.d.ts +68 -0
- package/dist/plan/mail-identity.js +17 -0
- package/dist/plan/plan-apply.d.ts +26 -8
- package/dist/plan/plan-apply.js +10 -9
- package/dist/plan/platform-diff.d.ts +207 -10
- package/dist/plan/platform-diff.js +403 -15
- package/dist/plan/platform-org.d.ts +3 -2
- package/dist/plan/platform-providers.d.ts +139 -25
- package/dist/plan/platform-providers.js +1531 -95
- package/dist/plan/platform-sfu.d.ts +54 -19
- package/dist/plan/platform-sfu.js +64 -22
- package/dist/plan/providers-readers.js +44 -1
- package/dist/plan/zitadel-machine-user.d.ts +60 -11
- package/dist/plan/zitadel-machine-user.js +182 -39
- package/dist/requested-changes.d.ts +17 -7
- package/dist/requested-changes.js +37 -7
- package/dist/store/audit.d.ts +40 -4
- package/dist/store/audit.js +114 -29
- package/dist/store/postgres.js +35 -3
- package/dist/store/schema.d.ts +109 -1
- package/dist/store/schema.js +20 -0
- package/dist/store.d.ts +7 -0
- package/package.json +5 -3
- package/versions.json +8 -0
package/dist/plan/executor.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { OrgMailOptions, OrgMailOrg, OrgMailResult } from '@wtfalch/email/p
|
|
|
2
2
|
import type { DeclaredOrg, HostingBuildPack } from '../org.js';
|
|
3
3
|
import type { DeclaredOrgStore } from '../store.js';
|
|
4
4
|
import type { StepAction } from './diff.js';
|
|
5
|
+
import { type LoginHostBindingsPort } from './login-host.js';
|
|
6
|
+
import { type MailCopyPort } from './mail-copy.js';
|
|
5
7
|
import type { DestroyTarget, StepDestroyer, StepExecutionContext, StepExecutor } from './plan-apply.js';
|
|
6
8
|
/**
|
|
7
9
|
* The real {@link StepExecutor} (#20 "provision Company Factory end to
|
|
@@ -139,13 +141,14 @@ export interface CoolifyPort {
|
|
|
139
141
|
}>>;
|
|
140
142
|
/**
|
|
141
143
|
* Upsert environment variables on an application in one call — used here to
|
|
142
|
-
* push the
|
|
143
|
-
* (hosting-wiring campaign
|
|
144
|
-
*
|
|
145
|
-
* value is a live credential: it must reach
|
|
146
|
-
* never a step's return value, `serviceIds`
|
|
147
|
-
* record, a log or a thrown error. See
|
|
148
|
-
* `
|
|
144
|
+
* push the org's own app's `DATABASE_URL_OWNER` plus a freshly minted
|
|
145
|
+
* `DATABASE_URL` runtime-role credential (hosting-wiring campaign, split
|
|
146
|
+
* per #239 the same way #117 already splits the cms app's), and likewise
|
|
147
|
+
* for the cms app itself. Each value is a live credential: it must reach
|
|
148
|
+
* this call and nothing else — never a step's return value, `serviceIds`
|
|
149
|
+
* (hence the store), an audit record, a log or a thrown error. See
|
|
150
|
+
* `coolifyApp`'s `pushDatabaseUrl`/`ensureAppRuntimeUrl` and `cmsSite`'s
|
|
151
|
+
* `ensureCmsRuntimeUrl`.
|
|
149
152
|
*/
|
|
150
153
|
bulkUpsertEnvs(applicationUuid: string, envs: Array<{
|
|
151
154
|
key: string;
|
|
@@ -504,35 +507,65 @@ export interface MailPort {
|
|
|
504
507
|
* `createMailbox`, resolving `org`'s domain to a `domainId` the same way
|
|
505
508
|
* `applyOrgMail` does -- that function already refuses a duplicate `local`
|
|
506
509
|
* on the resolved domain, so this port method never has to. `ensureOwnerMailbox`
|
|
507
|
-
* below
|
|
510
|
+
* and `ensureRoleMailboxes` (#230) below are this executor's callers, and each checks its own recorded scope
|
|
508
511
|
* id first, so a second apply never calls this at all.
|
|
509
512
|
*/
|
|
510
513
|
createMailbox(org: OrgMailOrg, mailbox: {
|
|
511
514
|
address: string;
|
|
512
515
|
name: string;
|
|
513
516
|
}): Promise<void>;
|
|
517
|
+
/**
|
|
518
|
+
* Upserts this org's own outbound relay route on Stalwart (#149): an
|
|
519
|
+
* `MtaRoute` through Lettermint, restricted to `org`'s domain, keyed by
|
|
520
|
+
* `org.slug` -- the host implements this with `@wtfalch/email/postmaster`'s
|
|
521
|
+
* own `applyOrgRelay` (https://github.com/wtfalch/email/issues/118), which
|
|
522
|
+
* rebuilds the shared routing expression so two orgs' routes never
|
|
523
|
+
* collide. `secret` is the Lettermint project token this executor just
|
|
524
|
+
* minted (`ensureOrgLettermintRelay`'s own mint rule) -- it goes straight
|
|
525
|
+
* here and nowhere else: never returned, never recorded, never logged, the
|
|
526
|
+
* same discipline `ensureAppLettermintMail`'s own doc comment states for
|
|
527
|
+
* `LETTERMINT_API_TOKEN`.
|
|
528
|
+
*/
|
|
529
|
+
applyOrgRelay(org: OrgMailOrg, secret: string): Promise<void>;
|
|
530
|
+
/**
|
|
531
|
+
* Drops this org's relay route (#149), implemented with
|
|
532
|
+
* `@wtfalch/email/postmaster`'s own `removeOrgRelay` -- a missing route is
|
|
533
|
+
* a no-op, so `parkLettermintMail` can call this unconditionally before it
|
|
534
|
+
* deletes the Lettermint project the route pointed at.
|
|
535
|
+
*/
|
|
536
|
+
removeOrgRelay(org: OrgMailOrg): Promise<void>;
|
|
537
|
+
/**
|
|
538
|
+
* True once this org's relay route exists on Stalwart with a secret set
|
|
539
|
+
* (#149). `ensureOrgLettermintRelay`'s own mint rule reads this alongside
|
|
540
|
+
* Lettermint's own project listing: a project without a live route (the
|
|
541
|
+
* route recorded a token this executor never minted) is exactly as
|
|
542
|
+
* unhealthy as a route with no project behind it, so a fresh mint fixes
|
|
543
|
+
* either.
|
|
544
|
+
*/
|
|
545
|
+
hasOrgRelay(org: OrgMailOrg): Promise<boolean>;
|
|
514
546
|
}
|
|
515
547
|
/**
|
|
516
|
-
* The shared calls SFU's own LiveKit API credentials (#122, #127,
|
|
517
|
-
* Before #170, a human minted
|
|
518
|
-
* read wherever `scripts/calls-sfu-deploy.mjs`'s
|
|
519
|
-
* delivered it from -- this package never minted or
|
|
520
|
-
* key/secret pair itself.
|
|
548
|
+
* The shared calls SFU's own per-org LiveKit API credentials (#122, #127,
|
|
549
|
+
* #145, #152). Before #170, a human minted one pair by hand, shared by every
|
|
550
|
+
* org, and `resolveCredentials` read wherever `scripts/calls-sfu-deploy.mjs`'s
|
|
551
|
+
* own `CallsSfuConfig` delivered it from -- this package never minted or
|
|
552
|
+
* invented a LiveKit key/secret pair itself.
|
|
521
553
|
*
|
|
522
554
|
* As of #170 the platform's own `sfu:livekit` step
|
|
523
|
-
* (`plan/platform-sfu.ts`'s `applyCallsSfuPlatformStep`) mints the pair,
|
|
524
|
-
*
|
|
525
|
-
* `CallsSfuKeysPort.deliverLivekitKeys
|
|
555
|
+
* (`plan/platform-sfu.ts`'s `applyCallsSfuPlatformStep`) mints the pair, and
|
|
556
|
+
* as of #152 it mints one *per org*, the first time that org applies, and
|
|
557
|
+
* delivers it to held keys through `CallsSfuKeysPort.deliverLivekitKeys`,
|
|
558
|
+
* keyed by the org's slug. `resolveCredentials` reads a specific org's own
|
|
526
559
|
* held-keys entry back -- still resolved by the host (archon), never minted
|
|
527
560
|
* or read directly by this port itself, the same "the executor is
|
|
528
561
|
* constructed with narrow ports ... a host supplies real clients" rule this
|
|
529
562
|
* file's own top doc comment states for every provider credential.
|
|
530
563
|
* `resolveCredentials` returns `null` before the platform's `sfu` step has
|
|
531
|
-
* ever applied, the same "nothing recorded" signal a bare
|
|
564
|
+
* ever applied for that org, the same "nothing recorded" signal a bare
|
|
532
565
|
* `CoolifyPort.listEnvs` gives `ensureCallsMatrixSecrets`.
|
|
533
566
|
*/
|
|
534
567
|
export interface CallsSfuPort {
|
|
535
|
-
resolveCredentials(): Promise<{
|
|
568
|
+
resolveCredentials(orgSlug: string): Promise<{
|
|
536
569
|
apiKey: string;
|
|
537
570
|
apiSecret: string;
|
|
538
571
|
} | null>;
|
|
@@ -664,6 +697,95 @@ export interface ZitadelPort {
|
|
|
664
697
|
userId: string;
|
|
665
698
|
token: string;
|
|
666
699
|
}>;
|
|
700
|
+
/**
|
|
701
|
+
* #216 (building on #209/#215): mints a second, narrower machine user on
|
|
702
|
+
* the same instance, with only the `IAM_LOGIN_CLIENT` role -- never
|
|
703
|
+
* `IAM_OWNER`, which {@link mintAdminMachineUser} already covers and which
|
|
704
|
+
* this method must not reuse or grant in addition. The login app and the
|
|
705
|
+
* broker both refuse to start without this token
|
|
706
|
+
* (`platform-providers.ts`'s `provisionPlatformAuth`, "Known gaps").
|
|
707
|
+
*
|
|
708
|
+
* Idempotent on the user by `userName`, like {@link mintAdminMachineUser},
|
|
709
|
+
* but never on the token the same way: ZITADEL shows a PAT's value once,
|
|
710
|
+
* and a crash or a failed delivery between a prior mint and the caller
|
|
711
|
+
* recording it would otherwise leave that lost token live forever. Unlike
|
|
712
|
+
* {@link mintAdminMachineUser}, revoking this user's PAT destroys nothing
|
|
713
|
+
* -- nothing else was ever encrypted with it, the way ZITADEL's own
|
|
714
|
+
* masterkey protects the admin path's data -- so a real implementation
|
|
715
|
+
* lists and deletes every PAT this user already holds before minting a
|
|
716
|
+
* fresh one, leaving exactly one live token no matter how many times this
|
|
717
|
+
* is called for the same user. This is what lets the caller
|
|
718
|
+
* (`provisionPlatformAuth`) simply re-mint on every rerun until delivery
|
|
719
|
+
* succeeds, rather than blocking forever the way an undelivered admin key
|
|
720
|
+
* does.
|
|
721
|
+
*
|
|
722
|
+
* Refuses (a named error, never a silent reuse) when a `foundry-login-client`
|
|
723
|
+
* user already exists with any IAM role other than exactly
|
|
724
|
+
* `IAM_LOGIN_CLIENT` -- that user was not created by this method, and
|
|
725
|
+
* granting it an additional role, or reusing its PAT, would widen access
|
|
726
|
+
* this caller never asked for.
|
|
727
|
+
*
|
|
728
|
+
* #216 round 2: also returns `patId`, the minted PAT's own id, so the
|
|
729
|
+
* caller can hand it to {@link listLoginClientPats} after delivering the
|
|
730
|
+
* token and confirm nothing else revoked or replaced it in the meantime.
|
|
731
|
+
*
|
|
732
|
+
* #216 (third PR review): also returns `revokedPriorPat`, true exactly
|
|
733
|
+
* when a live PAT existed on this user and was revoked before this mint --
|
|
734
|
+
* never true on a first-ever mint, where nothing was live to revoke. The
|
|
735
|
+
* caller uses this to word a delivery-failure error correctly: on a
|
|
736
|
+
* re-mint the login app and broker are left with no live token at all
|
|
737
|
+
* (the old one is already gone); on a first mint they simply never had one
|
|
738
|
+
* yet.
|
|
739
|
+
*/
|
|
740
|
+
mintLoginClientMachineUser(orgId: string, input: {
|
|
741
|
+
userName: string;
|
|
742
|
+
name: string;
|
|
743
|
+
}): Promise<{
|
|
744
|
+
userId: string;
|
|
745
|
+
token: string;
|
|
746
|
+
patId: string;
|
|
747
|
+
revokedPriorPat: boolean;
|
|
748
|
+
}>;
|
|
749
|
+
/**
|
|
750
|
+
* #216 round 2: every live PAT id on the `foundry-login-client` machine
|
|
751
|
+
* user, read straight from ZITADEL. `provisionPlatformAuth`
|
|
752
|
+
* (`platform-providers.ts`) calls this right after it delivers a freshly
|
|
753
|
+
* minted token to the login app and the broker, to close a race two
|
|
754
|
+
* concurrent applies can otherwise hit: run A mints token a and is about to
|
|
755
|
+
* deliver it; run B's own mint (`mintLoginClientMachineUser`, idempotent on
|
|
756
|
+
* the same `userName`) revokes a and mints token b first; run A then
|
|
757
|
+
* delivers the now-revoked a and would, without this check, still record
|
|
758
|
+
* success. Expecting the result to be exactly `[patId]` (the id
|
|
759
|
+
* {@link mintLoginClientMachineUser} returned) catches that: anything else
|
|
760
|
+
* means some other mint ran in between, so this run must fail rather than
|
|
761
|
+
* record delivery, leaving the next apply to re-mint and re-deliver.
|
|
762
|
+
*
|
|
763
|
+
* #216 round 3: a single call made once, after every write, cannot catch a
|
|
764
|
+
* write that lands after it -- `provisionPlatformAuth` now calls this
|
|
765
|
+
* again immediately before each individual write (each retry attempt of
|
|
766
|
+
* each of the login app's env, the broker's env, and each app's deploy),
|
|
767
|
+
* not only once at the end, so a write about to deliver a token this call
|
|
768
|
+
* reports no longer live is never attempted at all.
|
|
769
|
+
*/
|
|
770
|
+
listLoginClientPats(orgId: string, userId: string): Promise<string[]>;
|
|
771
|
+
/**
|
|
772
|
+
* #267: adds the org's own company login host to the ZITADEL instance's
|
|
773
|
+
* trusted domains (`@wtfalch/providers`' `ZitadelClient.ensureTrustedDomain`)
|
|
774
|
+
* -- the functionally critical half of `zitadelLoginHost` below, per
|
|
775
|
+
* `auth/docs/company-login-hosts.md`'s enforcement section: "the issuer
|
|
776
|
+
* refuses a public host that is not one of the instance's trusted
|
|
777
|
+
* domains." Idempotent: lists first, adds only if not already trusted.
|
|
778
|
+
*/
|
|
779
|
+
ensureTrustedDomain(domain: string): Promise<{
|
|
780
|
+
added: boolean;
|
|
781
|
+
}>;
|
|
782
|
+
/**
|
|
783
|
+
* #267: moves an already-created app's Login V2 base URI, read-modify-
|
|
784
|
+
* write (`@wtfalch/providers`' `ZitadelClient.updateOidcAppLoginBaseUri`).
|
|
785
|
+
* `zitadelLoginHost` below calls this for each live app whose client id
|
|
786
|
+
* the binding holds; `zitadelApp` never sets it at creation.
|
|
787
|
+
*/
|
|
788
|
+
updateOidcAppLoginBaseUri(orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
|
|
667
789
|
}
|
|
668
790
|
/**
|
|
669
791
|
* Provisions one org's organisation on Files (#80), matching
|
|
@@ -839,6 +961,15 @@ export interface FoundryStepExecutorPorts {
|
|
|
839
961
|
jobsScheduler?: JobsSchedulerPort;
|
|
840
962
|
/** The shared calls SFU's own LiveKit credentials (#145) -- see {@link CallsSfuPort}'s own doc comment. */
|
|
841
963
|
callsSfu: CallsSfuPort;
|
|
964
|
+
/**
|
|
965
|
+
* #113: the org's old-mail copy into Stalwart, ahead of a `received` ->
|
|
966
|
+
* `hosted` MX cutover -- see {@link MailCopyPort}'s own doc comment.
|
|
967
|
+
* Optional the same way `jobsScheduler` is: a host that never runs this
|
|
968
|
+
* transition builds the executor without it, and `coolify:mail-copy`
|
|
969
|
+
* throws a clear "not built" error rather than every other step losing
|
|
970
|
+
* compilation over one dependency nothing else needs.
|
|
971
|
+
*/
|
|
972
|
+
mailCopy?: MailCopyPort;
|
|
842
973
|
/** Flat, not a factory: `organisationId` is an explicit argument on every method of both ports (see each port's own doc comment; #177 dropped the `filesLimitsFor`/`aiLimitsFor` factories this used to need). */
|
|
843
974
|
filesOrganisation?: FilesOrganisationPort;
|
|
844
975
|
aiOrganisation?: AiOrganisationPort;
|
|
@@ -848,6 +979,15 @@ export interface FoundryStepExecutorPorts {
|
|
|
848
979
|
* step throws. The planner blocks these steps first while a key is not held.
|
|
849
980
|
*/
|
|
850
981
|
platformOrg?: PlatformOrgPortSource;
|
|
982
|
+
/**
|
|
983
|
+
* #267: zitadel-login's own `LOGIN_HOST_BINDINGS` entry for the org -- see
|
|
984
|
+
* {@link LoginHostBindingsPort}'s own doc comment. Optional the same way
|
|
985
|
+
* `jobsScheduler`/`mailCopy` are: a host that has not wired this yet gets no
|
|
986
|
+
* login-host step at all -- the planner reports it blocked (the host also
|
|
987
|
+
* sets `FoundryPlanApplyOptions.loginHostBindings`), so nothing is trusted
|
|
988
|
+
* or repointed without the binding.
|
|
989
|
+
*/
|
|
990
|
+
loginHostBindings?: LoginHostBindingsPort;
|
|
851
991
|
/**
|
|
852
992
|
* Hands a just-minted, one-time secret (today: an API app's
|
|
853
993
|
* `clientSecret`) to the host, immediately after the provider returns it
|
|
@@ -949,6 +1089,14 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
949
1089
|
* recorded).
|
|
950
1090
|
*/
|
|
951
1091
|
private zitadelOwner;
|
|
1092
|
+
/**
|
|
1093
|
+
* `plan/diff.ts`'s `planLoginHost` (#267): adds the org's own company
|
|
1094
|
+
* login host (`action.desired.host`) to the ZITADEL instance's trusted
|
|
1095
|
+
* domains, writes zitadel-login's binding, then repoints the apps whose
|
|
1096
|
+
* client id that binding holds. Apps created later are not in it yet:
|
|
1097
|
+
* the next run replans (`diff.ts`'s `planLoginHost`) and moves them.
|
|
1098
|
+
*/
|
|
1099
|
+
private zitadelLoginHost;
|
|
952
1100
|
/**
|
|
953
1101
|
* `plan/diff.ts`'s `planCustomers` (plan gap A9): the org's customers sign
|
|
954
1102
|
* in through a second ZITADEL org, `<slug> customers`, with
|
|
@@ -997,28 +1145,34 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
997
1145
|
private ensureLogVolume;
|
|
998
1146
|
private coolifyDatabase;
|
|
999
1147
|
/**
|
|
1000
|
-
* Pushes the database's connection string into the app's environment
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
* `db-role-guard.sh` then refuses to boot, since `DATABASE_URL_OWNER` being
|
|
1009
|
-
* set means it expects `DATABASE_URL` to connect as a distinct,
|
|
1010
|
-
* non-superuser runtime role (`<database>_rt`), which nothing here creates.
|
|
1011
|
-
* Minting that role's own password and URL is a provisioning step of its
|
|
1012
|
-
* own, not built here (Finding, this campaign's report).
|
|
1148
|
+
* Pushes the database's connection string into the app's environment,
|
|
1149
|
+
* split the same way `cmsSite`'s own database is (#117, #239): the
|
|
1150
|
+
* superuser connection as `DATABASE_URL_OWNER`, and a distinct, restricted
|
|
1151
|
+
* `<dbName>_rt` role's connection as `DATABASE_URL`. Archon's and manage's
|
|
1152
|
+
* own boot scripts (`db-role-guard.sh`) refuse to start against a
|
|
1153
|
+
* superuser `DATABASE_URL`, and lokessmie's refuses to start without
|
|
1154
|
+
* `DATABASE_URL_OWNER` at all -- a bare superuser `DATABASE_URL` with no
|
|
1155
|
+
* `DATABASE_URL_OWNER` is "before the split" there too.
|
|
1013
1156
|
*
|
|
1014
|
-
*
|
|
1015
|
-
* (`postgres://user:password@host/db`):
|
|
1016
|
-
*
|
|
1017
|
-
* nowhere else in this class -- never returned, never
|
|
1018
|
-
* `serviceIds` (hence never in the store or an audit record),
|
|
1019
|
-
* never interpolated into a thrown error.
|
|
1157
|
+
* Both connection strings are live credentials
|
|
1158
|
+
* (`postgres://user:password@host/db`): read from `CoolifyPort.getDatabase`
|
|
1159
|
+
* (or minted here, for the runtime one) and handed straight to
|
|
1160
|
+
* `bulkUpsertEnvs` and nowhere else in this class -- never returned, never
|
|
1161
|
+
* recorded in `serviceIds` (hence never in the store or an audit record),
|
|
1162
|
+
* never logged, never interpolated into a thrown error.
|
|
1020
1163
|
*/
|
|
1021
1164
|
private pushDatabaseUrl;
|
|
1165
|
+
/**
|
|
1166
|
+
* `null` once the app already has a `DATABASE_URL` -- never rotated, the
|
|
1167
|
+
* same rule `ensureCmsRuntimeUrl` follows and for the same reason: the
|
|
1168
|
+
* app's own boot script creates or updates the live `<dbName>_rt` Postgres
|
|
1169
|
+
* role from exactly the username/password this method mints, once:
|
|
1170
|
+
* overwriting `DATABASE_URL` on a later apply would leave that
|
|
1171
|
+
* already-created role's real password out of sync with whatever foundry
|
|
1172
|
+
* hands the app next. See `ensureCmsRuntimeUrl`'s own doc comment for the
|
|
1173
|
+
* parsing and encoding choices this mirrors exactly.
|
|
1174
|
+
*/
|
|
1175
|
+
private ensureAppRuntimeUrl;
|
|
1022
1176
|
/**
|
|
1023
1177
|
* Pushes what the org's app needs to sign a user in through ZITADEL (gap
|
|
1024
1178
|
* P2, `.plans/2026-09-26-level-1-org-map.md`) -- a second `bulkUpsertEnvs`
|
|
@@ -1033,9 +1187,12 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1033
1187
|
* `zitadel:issuer` (`AUTH_ORGANIZATION_ID`), `zitadel:owner`
|
|
1034
1188
|
* (`OWNER_USER_ID`, gap P3) and every `zitadel:app` step
|
|
1035
1189
|
* (`AUTH_CLIENT_ID`), all ordered before `coolify:app` for exactly this
|
|
1036
|
-
* reason. AUTH_APP_KEY (
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1190
|
+
* reason. `AUTH_APP_KEY` (#206 -- which broker an org's app registers with
|
|
1191
|
+
* is its own design question, left out of #204's own "How" rather than
|
|
1192
|
+
* guessed) is out of scope here (task brief). `DATABASE_URL_OWNER`/
|
|
1193
|
+
* `DATABASE_URL` are pushed by `pushDatabaseUrl` instead, not here --
|
|
1194
|
+
* that call runs only once `hosting.database` is declared and
|
|
1195
|
+
* provisioned, where this one is unconditional (#239).
|
|
1039
1196
|
*/
|
|
1040
1197
|
private pushAppEnv;
|
|
1041
1198
|
/**
|
|
@@ -1079,7 +1236,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1079
1236
|
* be deleted.
|
|
1080
1237
|
*
|
|
1081
1238
|
* `MAIL_FROM` is upserted every time: it depends only on the mail domain
|
|
1082
|
-
* `coolify:mail` resolved.
|
|
1239
|
+
* `coolify:mail` resolved and `org.mail.noReplyLocalPart` (#162, decided
|
|
1240
|
+
* 2026-09-29 -- defaults to `'no-reply'`, restated here rather than
|
|
1241
|
+
* imported from `org.ts`'s `DEFAULT_NO_REPLY_LOCAL_PART`, this file's own
|
|
1242
|
+
* `LOG_DIR`-style convention).
|
|
1083
1243
|
*
|
|
1084
1244
|
* The token goes straight to `bulkUpsertEnvs` and nowhere else: never
|
|
1085
1245
|
* returned, never recorded in `serviceIds` (`recordScopeId`'s own doc
|
|
@@ -1088,6 +1248,63 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1088
1248
|
* own connection string.
|
|
1089
1249
|
*/
|
|
1090
1250
|
private ensureAppLettermintMail;
|
|
1251
|
+
/**
|
|
1252
|
+
* #149: mints this org's own Lettermint relay credential and writes it to
|
|
1253
|
+
* Stalwart via `MailPort.applyOrgRelay`, the same mint rule
|
|
1254
|
+
* {@link ensureAppLettermintMail} already uses for the org's app token --
|
|
1255
|
+
* "healthy" means Stalwart's own route exists ({@link MailPort.hasOrgRelay}),
|
|
1256
|
+
* AND Lettermint lists a project named `<slug>-relay` whose id
|
|
1257
|
+
* {@link ORG_MAIL_RELAY_SCOPE_KEY} records for this
|
|
1258
|
+
* exact mail domain (review finding: a route with a secret and a
|
|
1259
|
+
* same-named project both present is not enough -- the project could be a
|
|
1260
|
+
* lost-and-reprovisioned one this step never wrote a route for, and a mail
|
|
1261
|
+
* domain switch must re-point the route rather than read as healthy
|
|
1262
|
+
* forever because the old route still has a secret in it). A route with no
|
|
1263
|
+
* live project behind it, a project the route's own secret never reached,
|
|
1264
|
+
* or a recorded project id/domain that no longer matches, are all exactly
|
|
1265
|
+
* as unhealthy as neither existing.
|
|
1266
|
+
*
|
|
1267
|
+
* A fresh project is created and its token written to Stalwart first, and
|
|
1268
|
+
* only then is every other `<slug>-relay` project deleted -- a lost route
|
|
1269
|
+
* and a duplicate project both end the same way. The new project's domain
|
|
1270
|
+
* restriction runs before the token is written, so a failure there never
|
|
1271
|
+
* touches the working route. Review finding: if either the
|
|
1272
|
+
* domain-restriction step or `applyOrgRelay` throws after a
|
|
1273
|
+
* fresh project was minted, that project is deleted before the error
|
|
1274
|
+
* propagates -- otherwise every retry sees more than one `<slug>-relay`
|
|
1275
|
+
* project and mints yet another one, growing the orphan count without
|
|
1276
|
+
* bound.
|
|
1277
|
+
*
|
|
1278
|
+
* `coolify:mail` runs before `coolify:app` (`diff.ts`'s own step order), so
|
|
1279
|
+
* this is very often the first of the two to reach the org's Lettermint
|
|
1280
|
+
* domain: {@link ensureLettermintDomainRestriction} registers and restricts
|
|
1281
|
+
* it the same way the app step does, rather than assuming it already
|
|
1282
|
+
* exists -- the domain must never end up unrestricted, whichever step gets
|
|
1283
|
+
* there first.
|
|
1284
|
+
*/
|
|
1285
|
+
private ensureOrgLettermintRelay;
|
|
1286
|
+
/**
|
|
1287
|
+
* Deletes a just-minted project from inside a `catch`. A failed delete is
|
|
1288
|
+
* dropped so it never replaces the error being rethrown; the orphan is
|
|
1289
|
+
* swept by the next apply's duplicate delete.
|
|
1290
|
+
*/
|
|
1291
|
+
private discardLettermintProject;
|
|
1292
|
+
/**
|
|
1293
|
+
* Ensures this org's Lettermint domain exists, is restricted to exactly
|
|
1294
|
+
* the `<slug>-app`/`<slug>-relay` projects live in `allProjects`, has its
|
|
1295
|
+
* DNS published and a verification triggered. Shared by
|
|
1296
|
+
* {@link ensureAppLettermintMail} and {@link ensureOrgLettermintRelay}
|
|
1297
|
+
* (#149) -- whichever of the two steps runs first on a fresh org creates
|
|
1298
|
+
* the domain, and the other then finds it (`findExactLettermintDomain`).
|
|
1299
|
+
*
|
|
1300
|
+
* A crash between the restriction write and the DNS publish/verify below
|
|
1301
|
+
* is safe even for a freshly created domain: DNS is only published, and
|
|
1302
|
+
* verification only triggered, after the restriction -- so an
|
|
1303
|
+
* unrestricted domain cannot verify and cannot send, whichever project
|
|
1304
|
+
* owns it (post-#140 review finding, restated here rather than only at
|
|
1305
|
+
* the one call site it used to have).
|
|
1306
|
+
*/
|
|
1307
|
+
private ensureLettermintDomainRestriction;
|
|
1091
1308
|
/**
|
|
1092
1309
|
* Review finding (post-#140 rework): {@link LettermintPort.findDomain}'s
|
|
1093
1310
|
* own match is partial and case-insensitive, so a raw result can be a
|
|
@@ -1374,20 +1591,21 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1374
1591
|
* `@wtfalch/email/postmaster`'s `applyOrgMail`/`parkOrgMail` bound), then
|
|
1375
1592
|
* publishes whatever Stalwart DNS records the call returns, and records the
|
|
1376
1593
|
* resolved mail domain as a scope id ({@link MAIL_DOMAIN_SCOPE_KEY}).
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
* (`ensureAppLettermintMail`, "Core rule"), since the
|
|
1381
|
-
* only exist once the token has a home. Parking mail
|
|
1382
|
-
*
|
|
1383
|
-
* deletes the org's own projects and
|
|
1384
|
-
* ends up restricted only to dead projects
|
|
1594
|
+
*
|
|
1595
|
+
* #149: this step also mints the org's own Lettermint *relay* credential
|
|
1596
|
+
* ({@link ensureOrgLettermintRelay}) -- the app's own *sending* credential
|
|
1597
|
+
* stays in the app step (`ensureAppLettermintMail`, "Core rule"), since the
|
|
1598
|
+
* first app project can only exist once the token has a home. Parking mail
|
|
1599
|
+
* (mode `'off'`) reaches Lettermint too: {@link parkLettermintMail} drops
|
|
1600
|
+
* the relay route, then deletes the org's own projects and its domain, so
|
|
1601
|
+
* a domain never ends up restricted only to dead projects and no route
|
|
1602
|
+
* ever points at a project this apply is about to remove.
|
|
1385
1603
|
*
|
|
1386
1604
|
* Review finding: a mail domain switch (`org.domain` changes, or
|
|
1387
1605
|
* `usesDomain` flips) must also delete the *old* Lettermint domain here,
|
|
1388
1606
|
* before recording the new one -- otherwise it stays registered and
|
|
1389
|
-
* restricted to the org's still-live `<slug>-app`
|
|
1390
|
-
* then keep sending as it forever. {@link deleteStaleLettermintDomain}
|
|
1607
|
+
* restricted to the org's still-live `<slug>-app`/`<slug>-relay` projects,
|
|
1608
|
+
* which could then keep sending as it forever. {@link deleteStaleLettermintDomain}
|
|
1391
1609
|
* does this by comparing the previously recorded domain (case-insensitive)
|
|
1392
1610
|
* against the one this apply just resolved.
|
|
1393
1611
|
*
|
|
@@ -1397,15 +1615,39 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1397
1615
|
*/
|
|
1398
1616
|
private coolifyMail;
|
|
1399
1617
|
/**
|
|
1400
|
-
* #
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
* `
|
|
1406
|
-
*
|
|
1618
|
+
* `plan/diff.ts`'s `planMailCopy` (#113): this apply's share of copying
|
|
1619
|
+
* the org's old mail into Stalwart, via `mail-copy.ts`'s `runMailCopyStep`
|
|
1620
|
+
* -- the only thing this method itself does is read back what the last
|
|
1621
|
+
* apply recorded, hand it to that function, and record what comes back.
|
|
1622
|
+
* An account that failed to copy records the progress made, never
|
|
1623
|
+
* `mail_copy_done`, then throws with the failing account, phase and
|
|
1624
|
+
* redacted stderr tail, so the apply reports it as not done. A missing
|
|
1625
|
+
* `mailCopy` port throws too: a configuration gap a retry cannot fix.
|
|
1626
|
+
*/
|
|
1627
|
+
private mailCopy;
|
|
1628
|
+
/**
|
|
1629
|
+
* #162 (decided 2026-09-29): gives the org's declared owner a mailbox at
|
|
1630
|
+
* its declared `mail.ownerMailboxAddress` -- nothing is guessed any more
|
|
1631
|
+
* (the hardcoded `owner@<domain>` PR 163 shipped first is gone). A no-op
|
|
1632
|
+
* when the org has no owner yet, when no address is declared yet
|
|
1633
|
+
* (`plan/diff.ts`'s `planMail` reports that case as blocked, not planned,
|
|
1634
|
+
* so this step's own mode/domain work still runs), or once
|
|
1635
|
+
* {@link MAIL_OWNER_MAILBOX_SCOPE_KEY} is already recorded -- checked last
|
|
1636
|
+
* so a second apply never calls `MailPort.createMailbox` again, rather
|
|
1637
|
+
* than relying on that port method's own duplicate refusal.
|
|
1407
1638
|
*/
|
|
1408
1639
|
private ensureOwnerMailbox;
|
|
1640
|
+
/**
|
|
1641
|
+
* #230: creates each of the org's declared `mail.roleMailboxes` on its
|
|
1642
|
+
* just-resolved mail domain, once. A role mailbox has no person behind it,
|
|
1643
|
+
* so its display name is just its local part, and like the owner's it is
|
|
1644
|
+
* created with no password: `MailPort.createMailbox` takes none, so there
|
|
1645
|
+
* is no secret to store or print. A "already has a mailbox" refusal from the port counts as created. The created set is recorded under
|
|
1646
|
+
* {@link MAIL_ROLE_MAILBOXES_SCOPE_KEY} after every mailbox, so a second
|
|
1647
|
+
* apply (or a retry after a crash) skips what exists rather than relying
|
|
1648
|
+
* on the mail server's own duplicate refusal.
|
|
1649
|
+
*/
|
|
1650
|
+
private ensureRoleMailboxes;
|
|
1409
1651
|
/**
|
|
1410
1652
|
* Deletes the org's previous Lettermint domain once the mail domain it was
|
|
1411
1653
|
* registered under has changed -- otherwise it stays registered and
|
|
@@ -1427,9 +1669,13 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1427
1669
|
/**
|
|
1428
1670
|
* Parking mail (2026-09-28 lettermint-org-mail swap, "Core rule": "Parking
|
|
1429
1671
|
* mail deletes the org's projects and then the domain itself, so no
|
|
1430
|
-
* restriction points only at dead projects").
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1672
|
+
* restriction points only at dead projects"). #149: first drops the
|
|
1673
|
+
* Stalwart relay route with `MailPort.removeOrgRelay` -- a missing route is
|
|
1674
|
+
* a no-op, per that port method's own contract, so this is safe to call
|
|
1675
|
+
* even for an org whose relay was never minted -- then deletes every
|
|
1676
|
+
* Lettermint project named `<slug>-app` or `<slug>-relay`, then the org's
|
|
1677
|
+
* own Lettermint domain, if one was found -- in that order, never the
|
|
1678
|
+
* reverse, so the route is always gone before the project it pointed at.
|
|
1433
1679
|
* A no-op until a previous apply recorded {@link MAIL_DOMAIN_SCOPE_KEY}:
|
|
1434
1680
|
* nothing was ever provisioned for an org whose mail went straight from
|
|
1435
1681
|
* never-applied to off.
|