@wtfalch/foundry 0.6.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.
Files changed (45) hide show
  1. package/dist/bootstrap/declare.d.ts +3 -1
  2. package/dist/bootstrap/declare.js +6 -2
  3. package/dist/bootstrap/versions.d.ts +17 -0
  4. package/dist/bootstrap/versions.js +12 -0
  5. package/dist/calls-sfu.d.ts +5 -4
  6. package/dist/calls-sfu.js +6 -4
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +1 -1
  9. package/dist/migrations/0022_chat_enabled.sql +6 -0
  10. package/dist/migrations/0023_hosting_app_env.sql +8 -0
  11. package/dist/migrations/0024_audit_events.sql +211 -0
  12. package/dist/migrations/0025_audit_display.sql +147 -0
  13. package/dist/migrations/0026_audit_erase_email_guard.sql +78 -0
  14. package/dist/migrations/0027_audit_chain.sql +170 -0
  15. package/dist/migrations/0028_audit_rls.sql +111 -0
  16. package/dist/org.d.ts +24 -0
  17. package/dist/org.js +13 -0
  18. package/dist/plan/agora.d.ts +21 -0
  19. package/dist/plan/agora.js +23 -0
  20. package/dist/plan/calls-matrix.d.ts +2 -2
  21. package/dist/plan/calls-matrix.js +3 -2
  22. package/dist/plan/cloudflared-connector.d.ts +3 -2
  23. package/dist/plan/cloudflared-connector.js +5 -3
  24. package/dist/plan/diff.d.ts +15 -2
  25. package/dist/plan/diff.js +306 -41
  26. package/dist/plan/executor.d.ts +73 -28
  27. package/dist/plan/executor.js +217 -55
  28. package/dist/plan/index.d.ts +2 -0
  29. package/dist/plan/index.js +1 -0
  30. package/dist/plan/login-host.d.ts +64 -0
  31. package/dist/plan/login-host.js +52 -0
  32. package/dist/plan/plan-apply.d.ts +21 -8
  33. package/dist/plan/plan-apply.js +10 -9
  34. package/dist/plan/platform-diff.d.ts +23 -1
  35. package/dist/plan/platform-diff.js +35 -0
  36. package/dist/plan/platform-providers.d.ts +1 -1
  37. package/dist/plan/platform-providers.js +71 -16
  38. package/dist/plan/providers-readers.js +44 -1
  39. package/dist/store/audit.d.ts +40 -4
  40. package/dist/store/audit.js +114 -29
  41. package/dist/store/postgres.js +4 -1
  42. package/dist/store/schema.d.ts +37 -1
  43. package/dist/store/schema.js +7 -0
  44. package/package.json +5 -3
  45. package/versions.json +8 -0
@@ -0,0 +1,170 @@
1
+ -- Copied verbatim from @wtfalch/audit 0.6.0's own migrations/ (#232), the
2
+ -- same way that package's own audit-migrations CLI copies them into a host's
3
+ -- drizzle/ directory -- foundry has no drizzle/ journal, so this file is
4
+ -- that copy, hand-placed at the next free number instead. Never edit the
5
+ -- copied SQL below; a fix ships as a new numbered file, same as upstream.
6
+ --
7
+ -- @wtfalch/audit: hash chaining, opt in.
8
+ --
9
+ -- Mirrors src/chain.ts and src/tables.ts. Every statement is idempotent.
10
+ -- Applied by the host's own migrate script after the host copies this file
11
+ -- into its drizzle/ directory as the next number (audit-migrations); never
12
+ -- edited there.
13
+ --
14
+ -- Nothing here forces chaining. Five nullable columns, computed and
15
+ -- verified entirely in TypeScript (src/chain.ts), the same design
16
+ -- `@wtfalch/authz`'s audit-chain.ts already shipped: no pgcrypto, no
17
+ -- trigger, because PGlite -- this package's own default test database --
18
+ -- has no pgcrypto, and a trigger cannot see the previous row's hash without
19
+ -- a race between concurrent inserts that only a lock (below) closes.
20
+ --
21
+ -- prev_hash -- the previous row's row_hash, in whatever order rows
22
+ -- were sealed at write time. null for the chain's first
23
+ -- row, and forever on a row nothing ever sealed.
24
+ -- row_hash -- sha256 over this row's own fields (chain.ts's
25
+ -- ROW_HASH_KEYS) plus prev_hash and content_hash. Never
26
+ -- changes once set; the append-only guard below still
27
+ -- refuses to.
28
+ -- content_hash -- sha256 over the row's erasable content (actor_display,
29
+ -- target_display, before, after), salted.
30
+ -- content_salt -- the salt content_hash was taken over. Set at seal
31
+ -- time, nulled by erasure: once it is gone nobody --
32
+ -- including this package -- can reproduce content_hash
33
+ -- from a guessed payload, which is what erasure means
34
+ -- here. Unlike row_hash, an erasure IS allowed to null
35
+ -- this, so the guard's allowed list grows to include it.
36
+ -- erasure_hash -- set only once content_salt is nulled: sha256 over
37
+ -- { row_hash, erased_at }, so the fact and the instant
38
+ -- of erasure stay checkable forever even though the
39
+ -- erased content no longer is.
40
+ --
41
+ -- A row written before this migration applies has row_hash null forever --
42
+ -- nothing recorded its hash at write time, so computing one now would only
43
+ -- hash what the row happens to contain today, proving nothing about
44
+ -- whether it changed since. verifyChain reports it 'unsealed', not
45
+ -- verified, the same stance 0002's target_display took on the rows before
46
+ -- it and worker_log_chain took on the rows before T255.
47
+ --
48
+ -- audit_seal_erasure(row_id, erasure_hash_value): the runtime role has no
49
+ -- UPDATE on audit_events at all (0001's revoke), so ledger.erase()'s
50
+ -- pending-erasure sweep -- setting content_salt to null and erasure_hash
51
+ -- once a chain-sealed row has been erased -- needs its own security
52
+ -- definer door back in, the same shape as audit_erase_person's.
53
+
54
+ alter table audit_events add column if not exists prev_hash text;
55
+ alter table audit_events add column if not exists row_hash text;
56
+ alter table audit_events add column if not exists content_hash text;
57
+ alter table audit_events add column if not exists content_salt text;
58
+ alter table audit_events add column if not exists erasure_hash text;
59
+
60
+ do $$ begin
61
+ alter table audit_events
62
+ add constraint audit_events_prev_hash_check
63
+ check (prev_hash is null or prev_hash ~ '^[0-9a-f]{64}$');
64
+ exception when duplicate_object then null; end $$;
65
+
66
+ do $$ begin
67
+ alter table audit_events
68
+ add constraint audit_events_row_hash_check
69
+ check (row_hash is null or row_hash ~ '^[0-9a-f]{64}$');
70
+ exception when duplicate_object then null; end $$;
71
+
72
+ do $$ begin
73
+ alter table audit_events
74
+ add constraint audit_events_content_hash_check
75
+ check (content_hash is null or content_hash ~ '^[0-9a-f]{64}$');
76
+ exception when duplicate_object then null; end $$;
77
+
78
+ do $$ begin
79
+ alter table audit_events
80
+ add constraint audit_events_erasure_hash_check
81
+ check (erasure_hash is null or erasure_hash ~ '^[0-9a-f]{64}$');
82
+ exception when duplicate_object then null; end $$;
83
+
84
+ -- A chain-sealed row not yet erased, so ledger.erase()'s pending-erasure
85
+ -- sweep finds it in an index scan rather than a table scan. Empty on a
86
+ -- host that never turns chaining on.
87
+ create index if not exists audit_events_chain_pending_idx
88
+ on audit_events (id)
89
+ where row_hash is not null and erased_at is not null and erasure_hash is null;
90
+
91
+ -- 0002's guard, with content_salt and erasure_hash added to the columns an
92
+ -- update may change: erasure nulls the first and sets the second, on a row
93
+ -- that was already erased by the same audit_erase_person call this
94
+ -- migration does not touch. prev_hash, row_hash and content_hash are
95
+ -- absent from the list on purpose -- once set, never.
96
+ create or replace function audit_events_guard() returns trigger
97
+ language plpgsql as $$
98
+ begin
99
+ if tg_op = 'DELETE' then
100
+ raise exception 'audit_events is append-only: delete refused';
101
+ elsif tg_op = 'TRUNCATE' then
102
+ raise exception 'audit_events is append-only: truncate refused';
103
+ elsif tg_op = 'UPDATE' then
104
+ if new.id is distinct from old.id
105
+ or new.occurred_at is distinct from old.occurred_at
106
+ or new.tenant_id is distinct from old.tenant_id
107
+ or new.tenant_display is distinct from old.tenant_display
108
+ or new.actor_class is distinct from old.actor_class
109
+ or new.actor_id is distinct from old.actor_id
110
+ or new.action is distinct from old.action
111
+ or new.target_type is distinct from old.target_type
112
+ or new.target_id is distinct from old.target_id
113
+ or new.outcome is distinct from old.outcome
114
+ or new.context is distinct from old.context
115
+ or new.session_id is distinct from old.session_id
116
+ or new.reason is distinct from old.reason
117
+ or new.reference is distinct from old.reference
118
+ or new.request_id is distinct from old.request_id
119
+ or new.ip is distinct from old.ip
120
+ or new.user_agent is distinct from old.user_agent
121
+ or new.tenant_visible is distinct from old.tenant_visible
122
+ or new.schema_version is distinct from old.schema_version
123
+ or new.subject_class is distinct from old.subject_class
124
+ or new.subject_id is distinct from old.subject_id
125
+ or new.prev_hash is distinct from old.prev_hash
126
+ or new.row_hash is distinct from old.row_hash
127
+ or new.content_hash is distinct from old.content_hash
128
+ then
129
+ raise exception 'audit_events is append-only: only actor_display, target_display, before, after, erased_at, content_salt and erasure_hash may change';
130
+ end if;
131
+ end if;
132
+ return new;
133
+ end
134
+ $$;
135
+
136
+ -- 0001's revoke took update off the runtime role entirely, on purpose:
137
+ -- audit_erase_person (security definer) is the one door back in. This is
138
+ -- the second one, for exactly one thing: chain-sealing a row
139
+ -- audit_erase_person already erased. It never computes a hash itself --
140
+ -- the caller does, in TypeScript (chain.ts's computeErasureHash), the same
141
+ -- design choice audit_erase_person's own absence of a hash column made
142
+ -- before this file existed -- it only writes the value it is given, and
143
+ -- only onto a row that is erased, chain-sealed, and not already sealed.
144
+ create or replace function audit_seal_erasure(row_id bigint, erasure_hash_value text)
145
+ returns void
146
+ language plpgsql
147
+ security definer
148
+ set search_path = pg_catalog, public
149
+ as $$
150
+ begin
151
+ update audit_events
152
+ set content_salt = null,
153
+ erasure_hash = erasure_hash_value
154
+ where id = row_id
155
+ and row_hash is not null
156
+ and erased_at is not null
157
+ and erasure_hash is null;
158
+ end
159
+ $$;
160
+ revoke all on function audit_seal_erasure(bigint, text) from public;
161
+
162
+ do $$
163
+ declare
164
+ rt text := current_database() || '_rt';
165
+ begin
166
+ if exists (select 1 from pg_roles where rolname = rt) then
167
+ execute format('grant execute on function audit_seal_erasure(bigint, text) to %I', rt);
168
+ end if;
169
+ end
170
+ $$;
@@ -0,0 +1,111 @@
1
+ -- Copied verbatim from @wtfalch/audit 0.6.0's own migrations/ (#232), the
2
+ -- same way that package's own audit-migrations CLI copies them into a host's
3
+ -- drizzle/ directory -- foundry has no drizzle/ journal, so this file is
4
+ -- that copy, hand-placed at the next free number instead. Never edit the
5
+ -- copied SQL below; a fix ships as a new numbered file, same as upstream.
6
+ --
7
+ -- @wtfalch/audit: row-level security on audit_events.
8
+ --
9
+ -- Mirrors src/ledger.ts (scopeAuditTenant). Every statement is idempotent.
10
+ -- Applied by the host's own migrate script after the host copies this file
11
+ -- into its drizzle/ directory as the next number (audit-migrations); never
12
+ -- edited there.
13
+ --
14
+ -- Until this file, tenant isolation was only the `tenantId` a caller
15
+ -- remembered to pass to ledger.page(). One missed predicate showed a
16
+ -- customer another company's trail. With RLS on, the database holds the
17
+ -- line for every role that is not the table's owner -- the runtime role
18
+ -- <database>_rt, and any read-only role a host adds:
19
+ --
20
+ -- audit.tenant_id -- set per transaction by the host
21
+ -- (scopeAuditTenant, or
22
+ -- set_config('audit.tenant_id', $1, true)).
23
+ -- While set, a SELECT sees that tenant's rows and
24
+ -- nothing else -- not another tenant's, and not
25
+ -- the estate's own tenant_id-null rows.
26
+ -- audit.require_tenant -- 'on' makes an unscoped SELECT see nothing
27
+ -- instead of everything. Set it on a role, once:
28
+ -- alter role <database>_rt set audit.require_tenant = 'on';
29
+ -- That is the setting that survives a forgotten
30
+ -- scope. It is not set here because an operator
31
+ -- surface reads across tenants on purpose, and
32
+ -- which role does that is the host's decision.
33
+ --
34
+ -- Unset and not required, a SELECT sees every row, exactly as before this
35
+ -- file: applying it changes nothing for a host until the host scopes.
36
+ --
37
+ -- INSERT is not restricted: a request scoped to one tenant may still record
38
+ -- an estate-level (tenant_id null) event, and the ledger's own vocabulary
39
+ -- checks already decide what may be written. UPDATE and DELETE have no
40
+ -- policy, so they stay refused for the runtime role -- which 0001's revoke
41
+ -- already did.
42
+ --
43
+ -- Not FORCE: the owner, and the security definer functions it owns
44
+ -- (audit_erase_person, audit_seal_erasure, and the two below), bypass RLS.
45
+ -- Erasure is by subject, across tenants, by design.
46
+
47
+ alter table audit_events enable row level security;
48
+
49
+ drop policy if exists audit_events_read on audit_events;
50
+ create policy audit_events_read on audit_events
51
+ for select
52
+ using (
53
+ case
54
+ when coalesce(current_setting('audit.tenant_id', true), '') <> ''
55
+ then tenant_id is not null
56
+ and tenant_id = current_setting('audit.tenant_id', true)::uuid
57
+ when coalesce(current_setting('audit.require_tenant', true), '') = 'on'
58
+ then false
59
+ else true
60
+ end
61
+ );
62
+
63
+ drop policy if exists audit_events_append on audit_events;
64
+ create policy audit_events_append on audit_events
65
+ for insert
66
+ with check (true);
67
+
68
+ -- The hash chain is one chain across every tenant. Under a tenant scope the
69
+ -- runtime role can no longer see the true tail, so ledger.sign() with
70
+ -- hashChain on would chain onto that tenant's last row and fork the chain.
71
+ -- This reads the tail as the owner. It returns a hash, never content.
72
+ create or replace function audit_chain_tail()
73
+ returns text
74
+ language sql
75
+ stable
76
+ security definer
77
+ set search_path = pg_catalog, public
78
+ as $$
79
+ select row_hash from audit_events order by id desc limit 1
80
+ $$;
81
+ revoke all on function audit_chain_tail() from public;
82
+
83
+ -- The same for ledger.erase()'s pending-erasure sweep, which must find
84
+ -- every tenant's pending rows, scoped or not. Ids, hashes and erasure
85
+ -- instants only.
86
+ create or replace function audit_pending_erasures()
87
+ returns table (id bigint, row_hash text, erased_at timestamptz)
88
+ language sql
89
+ stable
90
+ security definer
91
+ set search_path = pg_catalog, public
92
+ as $$
93
+ select e.id, e.row_hash, e.erased_at
94
+ from audit_events e
95
+ where e.row_hash is not null
96
+ and e.erased_at is not null
97
+ and e.erasure_hash is null
98
+ order by e.id
99
+ $$;
100
+ revoke all on function audit_pending_erasures() from public;
101
+
102
+ do $$
103
+ declare
104
+ rt text := current_database() || '_rt';
105
+ begin
106
+ if exists (select 1 from pg_roles where rolname = rt) then
107
+ execute format('grant execute on function audit_chain_tail() to %I', rt);
108
+ execute format('grant execute on function audit_pending_erasures() to %I', rt);
109
+ end if;
110
+ end
111
+ $$;
package/dist/org.d.ts CHANGED
@@ -99,6 +99,12 @@ export declare const DEFAULT_NO_REPLY_LOCAL_PART = "no-reply";
99
99
  * Never fired by `domain` alone (#7 amendment 1).
100
100
  */
101
101
  export interface ChatConfig {
102
+ /**
103
+ * Chat is not in v1 (decided 2026-10-01): off by default for every org.
104
+ * While off, `apply` plans no chat step and leaves a live chat resource
105
+ * alone, listed in `unmanaged`; Agora hides all chat and call UI.
106
+ */
107
+ enabled: boolean;
102
108
  usesDomain: boolean;
103
109
  }
104
110
  /**
@@ -214,7 +220,25 @@ export interface HostingConfig {
214
220
  * means no Access gate. Requires `enabled`.
215
221
  */
216
222
  accessEmails: string[];
223
+ /**
224
+ * Maps the app's own env var name to one of `AppEnvSource`'s values --
225
+ * each a value the plan already mints for this org (Files, AI, mail) but
226
+ * never used to push an app's own env (#243). Each bespoke app names these
227
+ * differently (lokessmie uses `STORAGE_*`/`AI_*`); this is how an org
228
+ * declares its own app's names for them. `{}` means the app names none of
229
+ * them, so `pushAppEnv` pushes nothing from this map.
230
+ */
231
+ appEnv: Record<string, AppEnvSource>;
217
232
  }
233
+ /**
234
+ * The closed set of values the plan already mints that `HostingConfig.appEnv`
235
+ * may map an app's env var name to (#243): the org's own Files organisation
236
+ * id, its AI organisation id, and the mail domain its Lettermint project
237
+ * sends from. Each is read from `StepExecutionContext.recordedIds()` in
238
+ * `executor.ts`'s `pushAppEnv`, never minted again there.
239
+ */
240
+ export type AppEnvSource = 'filesOrganisationId' | 'aiOrganisationId' | 'mailDomain';
241
+ export declare const APP_ENV_SOURCES: ReadonlySet<AppEnvSource>;
218
242
  /**
219
243
  * `null` when `hosting` is valid; otherwise the reason it is not (#71), same
220
244
  * shape as `validateBuyerIdentity` below.
package/dist/org.js CHANGED
@@ -2,6 +2,11 @@
2
2
  export const DEFAULT_MAIL_APPS = ['agora', 'webmail'];
3
3
  /** {@link MailConfig.noReplyLocalPart}'s own default, restated wherever it is read rather than imported (this file's `LOG_DIR`-style convention, named in `plan/executor.ts`'s own doc comments). */
4
4
  export const DEFAULT_NO_REPLY_LOCAL_PART = 'no-reply';
5
+ export const APP_ENV_SOURCES = new Set([
6
+ 'filesOrganisationId',
7
+ 'aiOrganisationId',
8
+ 'mailDomain',
9
+ ]);
5
10
  const ISSUER_APP_KINDS = new Set(['web', 'native', 'api']);
6
11
  const HOSTING_BUILD_PACKS = new Set([
7
12
  'dockerfile',
@@ -62,6 +67,14 @@ export function validateHosting(hosting) {
62
67
  return `issuerApps '${app.name}' is reserved for the always-on Agora client every org gets`;
63
68
  }
64
69
  }
70
+ for (const [envName, source] of Object.entries(hosting.appEnv)) {
71
+ if (!envName) {
72
+ return 'every appEnv key needs a non-empty env var name';
73
+ }
74
+ if (!APP_ENV_SOURCES.has(source)) {
75
+ return `appEnv '${envName}' has an unknown source '${source}'`;
76
+ }
77
+ }
65
78
  return null;
66
79
  }
67
80
  /**
@@ -13,6 +13,27 @@ import type { DeclaredOrg, OrgId } from '../org.js';
13
13
  */
14
14
  export declare const ZITADEL_ORG_SCOPE_KEY = "zitadel_org";
15
15
  export declare const AGORA_CLIENT_ID_SCOPE_KEY = "agora_client_id";
16
+ /**
17
+ * The recorded OIDC app's `clientId` (level-1 app-env campaign, gap P2) —
18
+ * not a `system:kind:key` resource id (that slot already holds the app's
19
+ * `appId`, `zitadelApp`'s own `resourceId`) and not a secret (`deliverSecret`
20
+ * is for that), so a flat scope id like `zitadel_org`/`zitadel_project`
21
+ * above. A level-1 org declares exactly one web `issuerApps` entry for its
22
+ * one standard app, so one flat key is enough; an org with more than one web
23
+ * app would need a richer key, not built here (Finding, this campaign's
24
+ * report).
25
+ */
26
+ export declare const APP_CLIENT_ID_SCOPE_KEY = "zitadel_app_client_id";
27
+ /**
28
+ * #139: the `cms` app's own ZITADEL web client's `clientId`, recorded
29
+ * separately from {@link APP_CLIENT_ID_SCOPE_KEY} -- that key belongs to the
30
+ * org's own hosted app (`diff.ts`'s `planIssuerApps`), a different
31
+ * `zitadel:app` step from the fixed `cms` client `diff.ts`'s `planCmsClient`
32
+ * plans. `cmsSite` reads this key to push `AUTH_CLIENT_ID` onto the cms app,
33
+ * the same way `pushAppEnv` reads `APP_CLIENT_ID_SCOPE_KEY` for the hosted
34
+ * app.
35
+ */
36
+ export declare const CMS_CLIENT_ID_SCOPE_KEY = "cms_client_id";
16
37
  /**
17
38
  * The accent every org's Agora wears until orgs choose their own: the house
18
39
  * theme's `--accent` (`@wtfalch/house`, `HOUSE_THEMES['good-company']` in
@@ -12,6 +12,27 @@
12
12
  */
13
13
  export const ZITADEL_ORG_SCOPE_KEY = 'zitadel_org';
14
14
  export const AGORA_CLIENT_ID_SCOPE_KEY = 'agora_client_id';
15
+ /**
16
+ * The recorded OIDC app's `clientId` (level-1 app-env campaign, gap P2) —
17
+ * not a `system:kind:key` resource id (that slot already holds the app's
18
+ * `appId`, `zitadelApp`'s own `resourceId`) and not a secret (`deliverSecret`
19
+ * is for that), so a flat scope id like `zitadel_org`/`zitadel_project`
20
+ * above. A level-1 org declares exactly one web `issuerApps` entry for its
21
+ * one standard app, so one flat key is enough; an org with more than one web
22
+ * app would need a richer key, not built here (Finding, this campaign's
23
+ * report).
24
+ */
25
+ export const APP_CLIENT_ID_SCOPE_KEY = 'zitadel_app_client_id';
26
+ /**
27
+ * #139: the `cms` app's own ZITADEL web client's `clientId`, recorded
28
+ * separately from {@link APP_CLIENT_ID_SCOPE_KEY} -- that key belongs to the
29
+ * org's own hosted app (`diff.ts`'s `planIssuerApps`), a different
30
+ * `zitadel:app` step from the fixed `cms` client `diff.ts`'s `planCmsClient`
31
+ * plans. `cmsSite` reads this key to push `AUTH_CLIENT_ID` onto the cms app,
32
+ * the same way `pushAppEnv` reads `APP_CLIENT_ID_SCOPE_KEY` for the hosted
33
+ * app.
34
+ */
35
+ export const CMS_CLIENT_ID_SCOPE_KEY = 'cms_client_id';
15
36
  /**
16
37
  * The accent every org's Agora wears until orgs choose their own: the house
17
38
  * theme's `--accent` (`@wtfalch/house`, `HOUSE_THEMES['good-company']` in
@@ -85,6 +106,8 @@ export function desiredAgoraEnv(org, recorded, host) {
85
106
  organisationId: zitadelOrgId,
86
107
  mailProjectId: host.mailProjectId,
87
108
  messagingProjectId: host.messagingProjectId,
109
+ // Chat is not in v1: Agora hides all chat UI unless this is true.
110
+ chat: { enabled: org.chat.enabled },
88
111
  });
89
112
  }
90
113
  if (org.mail.mode !== 'off' && host.mailUpstream) {
@@ -45,8 +45,8 @@
45
45
  * (`executor.ts`'s `callsHostnameFor`).
46
46
  */
47
47
  export declare const SYNAPSE_IMAGE = "ghcr.io/element-hq/synapse";
48
- export declare const SYNAPSE_TAG = "v1.161.0";
49
- export declare const MAS_IMAGE = "ghcr.io/element-hq/matrix-authentication-service:1.24.0";
48
+ export declare const SYNAPSE_TAG: string;
49
+ export declare const MAS_IMAGE: string;
50
50
  /**
51
51
  * Never resolved by DNS and never federates (`callsSynapseHomeserverYaml`'s
52
52
  * `federation_domain_whitelist: []`) -- purely a logical identifier MAS and
@@ -1,4 +1,5 @@
1
1
  import { generateKeyPairSync, randomBytes } from 'node:crypto';
2
+ import { VERSIONS } from '../bootstrap/versions.js';
2
3
  /**
3
4
  * The calls Matrix stack (#128, part 1 of 2 -- see that issue's "If this
4
5
  * passes about 800 changed lines, split it" note): pure config generation
@@ -46,8 +47,8 @@ import { generateKeyPairSync, randomBytes } from 'node:crypto';
46
47
  * (`executor.ts`'s `callsHostnameFor`).
47
48
  */
48
49
  export const SYNAPSE_IMAGE = 'ghcr.io/element-hq/synapse';
49
- export const SYNAPSE_TAG = 'v1.161.0';
50
- export const MAS_IMAGE = 'ghcr.io/element-hq/matrix-authentication-service:1.24.0';
50
+ export const SYNAPSE_TAG = VERSIONS.synapse;
51
+ export const MAS_IMAGE = `ghcr.io/element-hq/matrix-authentication-service:${VERSIONS.mas}`;
51
52
  /**
52
53
  * Never resolved by DNS and never federates (`callsSynapseHomeserverYaml`'s
53
54
  * `federation_domain_whitelist: []`) -- purely a logical identifier MAS and
@@ -2,9 +2,10 @@ import type { CloudflarePort, CoolifyPort } from './executor.js';
2
2
  /**
3
3
  * #197: the pinned `cloudflared` image, the same release tag as
4
4
  * `scripts/shared-ingress.mjs`'s and `calls-sfu.ts`'s own
5
- * `CLOUDFLARED_IMAGE_REF`. A test in each keeps the three equal.
5
+ * `CLOUDFLARED_IMAGE_REF`. A test in each keeps the three equal. Pinned in
6
+ * `versions.json` (#247).
6
7
  */
7
- export declare const CLOUDFLARED_IMAGE_REF = "cloudflare/cloudflared:2026.9.3";
8
+ export declare const CLOUDFLARED_IMAGE_REF: string;
8
9
  /**
9
10
  * The inline Dockerfile a docker-image `cloudflared` app needs, copied from
10
11
  * `scripts/shared-ingress.mjs`'s `cloudflaredDockerfile()`: the published
@@ -1,10 +1,12 @@
1
+ import { VERSIONS } from '../bootstrap/versions.js';
1
2
  import { isNotFoundError } from './executor.js';
2
3
  /**
3
4
  * #197: the pinned `cloudflared` image, the same release tag as
4
5
  * `scripts/shared-ingress.mjs`'s and `calls-sfu.ts`'s own
5
- * `CLOUDFLARED_IMAGE_REF`. A test in each keeps the three equal.
6
+ * `CLOUDFLARED_IMAGE_REF`. A test in each keeps the three equal. Pinned in
7
+ * `versions.json` (#247).
6
8
  */
7
- export const CLOUDFLARED_IMAGE_REF = 'cloudflare/cloudflared:2026.9.3';
9
+ export const CLOUDFLARED_IMAGE_REF = `cloudflare/cloudflared:${VERSIONS.cloudflared}`;
8
10
  /**
9
11
  * The inline Dockerfile a docker-image `cloudflared` app needs, copied from
10
12
  * `scripts/shared-ingress.mjs`'s `cloudflaredDockerfile()`: the published
@@ -72,7 +74,7 @@ export async function ensureCloudflaredConnector(input) {
72
74
  environment_uuid: input.environment.uuid,
73
75
  dockerfile: cloudflaredDockerfile(),
74
76
  name: input.appName,
75
- description: 'cloudflared connector for this platform tunnel (#197)',
77
+ description: 'cloudflared connector for a Cloudflare tunnel',
76
78
  instant_deploy: false,
77
79
  });
78
80
  appId = created.uuid;
@@ -26,6 +26,15 @@ export interface PlannedStep {
26
26
  step: PlanStep;
27
27
  action: StepAction;
28
28
  }
29
+ /**
30
+ * Facts about the host's executor ports that an org's plan needs, so a step
31
+ * whose port is not wired is reported blocked instead of planned (rule 2).
32
+ * New ones go here, not in new positional arguments (rule 7).
33
+ */
34
+ export interface PlanOrgOptions {
35
+ /** #267: the executor was built with a `loginHostBindings` port. */
36
+ loginHostBindings?: boolean;
37
+ }
29
38
  export interface PlanDetail {
30
39
  orgId: OrgId;
31
40
  plannedSteps: PlannedStep[];
@@ -73,7 +82,11 @@ export declare function toPlan(detail: PlanDetail): Plan;
73
82
  * app with the platform's jobs scheduler, idempotent on the *recorded*
74
83
  * resource id like {@link planOwner} rather than a live resource, since
75
84
  * nothing in `live` can report a scheduler registration (no reader exists for
76
- * it). Everything
85
+ * it). #267 adds one more, right after {@link planOwner} and before every
86
+ * app-creating step below: {@link planLoginHost}, the org's own company
87
+ * login host -- also idempotent on the *recorded* resource id, since
88
+ * `live` has no notion of a ZITADEL trusted domain or a login binding
89
+ * either. Everything
77
90
  * else in `live` that none of these eighteen singletons and however many
78
91
  * declared issuer apps claim is unmanaged. The full field-by-field provisioning surface (SSO,
79
92
  * guests, calls on the org's own domain, adopted/bespoke apps, the actual
@@ -81,7 +94,7 @@ export declare function toPlan(detail: PlanDetail): Plan;
81
94
  * Company Factory end to end", which wires real readers and executors
82
95
  * against this seam.
83
96
  */
84
- export declare function computePlanDetail(org: DeclaredOrg, live: LiveResource[], recordedIds?: Readonly<Record<string, string>>, agoraHost?: AgoraHostReading, jobsAvailable?: boolean, platformReadiness?: OrgPlatformReadiness): PlanDetail;
97
+ export declare function computePlanDetail(org: DeclaredOrg, live: LiveResource[], recordedIds?: Readonly<Record<string, string>>, agoraHost?: AgoraHostReading, jobsAvailable?: boolean, platformReadiness?: OrgPlatformReadiness, options?: PlanOrgOptions): PlanDetail;
85
98
  /** Restated from `executor.ts`, the same rule: the id of an ai key minted but not delivered. */
86
99
  export declare const AI_KEY_UNDELIVERED_SCOPE_KEY = "ai_key_undelivered";
87
100
  /** Restated from `executor.ts`, the same rule: the id of a jobs project registered but not delivered. */