@wtfalch/foundry 0.4.0 → 0.5.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 (66) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +96 -0
  2. package/dist/bootstrap/bootstrap.js +285 -0
  3. package/dist/bootstrap/declare.d.ts +36 -0
  4. package/dist/bootstrap/declare.js +94 -0
  5. package/dist/bootstrap/drive.d.ts +40 -0
  6. package/dist/bootstrap/drive.js +51 -0
  7. package/dist/bootstrap/index.d.ts +3 -0
  8. package/dist/bootstrap/index.js +1 -0
  9. package/dist/calls-sfu-deploy-support.d.ts +9 -6
  10. package/dist/calls-sfu-deploy-support.js +9 -6
  11. package/dist/calls-sfu-keys.d.ts +21 -0
  12. package/dist/calls-sfu-keys.js +27 -0
  13. package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
  14. package/dist/calls-sfu-ssh-adapter.js +78 -0
  15. package/dist/calls-sfu-ssh-client.d.ts +49 -0
  16. package/dist/calls-sfu-ssh-client.js +82 -0
  17. package/dist/calls-sfu.d.ts +5 -4
  18. package/dist/calls-sfu.js +5 -4
  19. package/dist/index.d.ts +31 -6
  20. package/dist/index.js +16 -3
  21. package/dist/migrations/0016_platform.sql +61 -0
  22. package/dist/migrations/0017_requested_changes.sql +42 -0
  23. package/dist/migrations/0018_platform_apply.sql +19 -0
  24. package/dist/org.js +8 -0
  25. package/dist/plan/agora.d.ts +77 -0
  26. package/dist/plan/agora.js +127 -0
  27. package/dist/plan/cloudflared-connector.d.ts +70 -0
  28. package/dist/plan/cloudflared-connector.js +106 -0
  29. package/dist/plan/diff.d.ts +20 -3
  30. package/dist/plan/diff.js +631 -116
  31. package/dist/plan/executor.d.ts +475 -168
  32. package/dist/plan/executor.js +543 -175
  33. package/dist/plan/index.d.ts +22 -5
  34. package/dist/plan/index.js +12 -2
  35. package/dist/plan/jobs-scheduler-http.d.ts +29 -0
  36. package/dist/plan/jobs-scheduler-http.js +50 -0
  37. package/dist/plan/live.d.ts +51 -2
  38. package/dist/plan/live.js +8 -0
  39. package/dist/plan/plan-apply.d.ts +192 -3
  40. package/dist/plan/plan-apply.js +278 -9
  41. package/dist/plan/platform-diff.d.ts +312 -0
  42. package/dist/plan/platform-diff.js +867 -0
  43. package/dist/plan/platform-org-ports.d.ts +50 -0
  44. package/dist/plan/platform-org-ports.js +70 -0
  45. package/dist/plan/platform-org.d.ts +120 -0
  46. package/dist/plan/platform-org.js +115 -0
  47. package/dist/plan/platform-providers.d.ts +501 -0
  48. package/dist/plan/platform-providers.js +3029 -0
  49. package/dist/plan/platform-sfu.d.ts +93 -0
  50. package/dist/plan/platform-sfu.js +100 -0
  51. package/dist/plan/providers-readers.js +22 -1
  52. package/dist/plan/zitadel-machine-user.d.ts +34 -0
  53. package/dist/plan/zitadel-machine-user.js +95 -0
  54. package/dist/plan.d.ts +6 -0
  55. package/dist/platform.d.ts +55 -0
  56. package/dist/platform.js +46 -0
  57. package/dist/requested-changes.d.ts +108 -0
  58. package/dist/requested-changes.js +95 -0
  59. package/dist/slug.js +15 -2
  60. package/dist/store/audit.js +4 -2
  61. package/dist/store/postgres.d.ts +29 -1
  62. package/dist/store/postgres.js +172 -2
  63. package/dist/store/schema.d.ts +683 -0
  64. package/dist/store/schema.js +57 -0
  65. package/dist/store.d.ts +46 -0
  66. package/package.json +1 -1
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Mints the LiveKit API key/secret pair the shared calls SFU signs tokens
3
+ * with (#170). Before this, `scripts/calls-sfu-deploy.mjs` read
4
+ * `LIVEKIT_API_KEY`/`LIVEKIT_API_SECRET` from the environment -- a human
5
+ * minted the pair by hand and put it in the secrets store. The platform
6
+ * `sfu` step (`plan/platform-sfu.ts`) mints it instead, once, and delivers it
7
+ * to held keys through `CallsSfuKeysPort.deliverLivekitKeys`; every apply
8
+ * after that reads the same pair back through `resolveLivekitKeys` rather
9
+ * than minting a second one.
10
+ *
11
+ * The shape matches LiveKit's own self-hosting docs: an `API`-prefixed key
12
+ * id and a long random secret, both plain alphanumeric so they drop straight
13
+ * into `livekit.yaml`'s `keys:` map (`calls-sfu.ts`'s `livekitConfigYaml`)
14
+ * with no escaping to worry about.
15
+ */
16
+ import { randomBytes } from 'node:crypto';
17
+ const ALPHANUMERIC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
18
+ function randomAlphanumeric(length) {
19
+ return Array.from(randomBytes(length), (byte) => ALPHANUMERIC[byte % ALPHANUMERIC.length]).join('');
20
+ }
21
+ /** A fresh LiveKit API key/secret pair, minted with `node:crypto`'s CSPRNG. */
22
+ export function mintLivekitCredentials() {
23
+ return {
24
+ apiKey: `API${randomAlphanumeric(12)}`,
25
+ apiSecret: randomAlphanumeric(40),
26
+ };
27
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Turns one open SSH session into the `CallsSfuHostPort` {@link applyCallsSfu}
3
+ * needs, applying `calls-sfu-deploy-support.ts`'s decision logic
4
+ * (`verifyHostKey`, `fileWriteMode`, the preflight check) exactly once so
5
+ * `scripts/calls-sfu-deploy.mjs` and the platform `sfu` step
6
+ * (`plan/platform-sfu.ts`, #170) share it rather than each re-deriving it.
7
+ *
8
+ * {@link CallsSfuSshSession} is deliberately narrow -- the host key an
9
+ * already-open session presented, a way to run a command, a way to write a
10
+ * file at a chosen mode -- so a test hands in a fake session and proves the
11
+ * host-key refusal and the 0600 file mode without ever opening a socket.
12
+ * `calls-sfu-ssh-client.ts`'s `connectCallsSfuSsh` is the one real
13
+ * implementation, over `ssh2`, and stays untested for the same reason
14
+ * `foundry-db-provision.mjs`/`foundry-db-migrate.mjs` do: real I/O, no
15
+ * branch logic worth a fake session over.
16
+ */
17
+ import { type CallsSfuConfig, type CallsSfuHostPort } from './calls-sfu.js';
18
+ export declare class UnknownCallsSfuHostKeyError extends Error {
19
+ constructor();
20
+ }
21
+ /**
22
+ * One already-open SSH session, narrow enough to fake in a test: the raw
23
+ * host key it presented during the handshake, a way to run one command, and
24
+ * a way to write a file at a chosen mode (`undefined` for the SFTP server's
25
+ * own default, same as {@link CallsSfuHostPort.writeFile} today).
26
+ */
27
+ export interface CallsSfuSshSession {
28
+ hostKey: Buffer;
29
+ exec(command: string): Promise<{
30
+ exitCode: number;
31
+ stdout: string;
32
+ stderr: string;
33
+ }>;
34
+ writeFile(path: string, contents: string, mode: number | undefined): Promise<void>;
35
+ }
36
+ /**
37
+ * Refuses with {@link UnknownCallsSfuHostKeyError} before adapting anything
38
+ * when `session.hostKey` does not match `expectedFingerprint` -- checked
39
+ * here, in code a fake session exercises directly, rather than only inside
40
+ * `ssh2`'s own `hostVerifier` (PR #125's review, finding 1). Otherwise
41
+ * returns a {@link CallsSfuHostPort} that writes each file at
42
+ * {@link fileWriteMode}'s mode (finding 2) and runs commands as-is.
43
+ */
44
+ export declare function toCallsSfuHostPort(session: CallsSfuSshSession, expectedFingerprint: string | undefined): CallsSfuHostPort;
45
+ /**
46
+ * The whole deploy, over one already-open session: refuse an unknown host
47
+ * key first (before any command runs), check `docker`/`ufw` are installed
48
+ * (PR #125's review, finding 3), make sure `/opt/livekit` exists, then hand
49
+ * off to {@link applyCallsSfu} -- the same order
50
+ * `scripts/calls-sfu-deploy.mjs` ran by hand, now shared with the platform
51
+ * `sfu` step. Every step here is safe to re-run, the same "safe to re-run"
52
+ * {@link applyCallsSfu} itself promises, so calling this twice with the same
53
+ * `session`/`config` changes nothing on the second call.
54
+ */
55
+ export declare function applyCallsSfuOverSession(session: CallsSfuSshSession, config: CallsSfuConfig, expectedFingerprint: string | undefined): Promise<void>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Turns one open SSH session into the `CallsSfuHostPort` {@link applyCallsSfu}
3
+ * needs, applying `calls-sfu-deploy-support.ts`'s decision logic
4
+ * (`verifyHostKey`, `fileWriteMode`, the preflight check) exactly once so
5
+ * `scripts/calls-sfu-deploy.mjs` and the platform `sfu` step
6
+ * (`plan/platform-sfu.ts`, #170) share it rather than each re-deriving it.
7
+ *
8
+ * {@link CallsSfuSshSession} is deliberately narrow -- the host key an
9
+ * already-open session presented, a way to run a command, a way to write a
10
+ * file at a chosen mode -- so a test hands in a fake session and proves the
11
+ * host-key refusal and the 0600 file mode without ever opening a socket.
12
+ * `calls-sfu-ssh-client.ts`'s `connectCallsSfuSsh` is the one real
13
+ * implementation, over `ssh2`, and stays untested for the same reason
14
+ * `foundry-db-provision.mjs`/`foundry-db-migrate.mjs` do: real I/O, no
15
+ * branch logic worth a fake session over.
16
+ */
17
+ import { dirname } from 'node:path';
18
+ import { PREFLIGHT_COMMAND, fileWriteMode, preflightFailureMessage, verifyHostKey, } from './calls-sfu-deploy-support.js';
19
+ import { LIVEKIT_COMPOSE_PATH, LIVEKIT_CONFIG_PATH, applyCallsSfu, livekitConfigYaml, } from './calls-sfu.js';
20
+ export class UnknownCallsSfuHostKeyError extends Error {
21
+ constructor() {
22
+ super('The calls SFU host key did not match the fingerprint held for it; refusing to ' +
23
+ 'deploy. See calls-sfu-deploy-support.ts’s verifyHostKey (PR #125) -- the ' +
24
+ 'expected fingerprint comes from held keys (#170’s "Human steps").');
25
+ this.name = 'UnknownCallsSfuHostKeyError';
26
+ }
27
+ }
28
+ /**
29
+ * Refuses with {@link UnknownCallsSfuHostKeyError} before adapting anything
30
+ * when `session.hostKey` does not match `expectedFingerprint` -- checked
31
+ * here, in code a fake session exercises directly, rather than only inside
32
+ * `ssh2`'s own `hostVerifier` (PR #125's review, finding 1). Otherwise
33
+ * returns a {@link CallsSfuHostPort} that writes each file at
34
+ * {@link fileWriteMode}'s mode (finding 2) and runs commands as-is.
35
+ */
36
+ export function toCallsSfuHostPort(session, expectedFingerprint) {
37
+ if (!verifyHostKey(session.hostKey, expectedFingerprint)) {
38
+ throw new UnknownCallsSfuHostKeyError();
39
+ }
40
+ return {
41
+ writeFile: (path, contents) => session.writeFile(path, contents, fileWriteMode(path)),
42
+ exec: (command) => session.exec(command),
43
+ };
44
+ }
45
+ /**
46
+ * The whole deploy, over one already-open session: refuse an unknown host
47
+ * key first (before any command runs), check `docker`/`ufw` are installed
48
+ * (PR #125's review, finding 3), make sure `/opt/livekit` exists, then hand
49
+ * off to {@link applyCallsSfu} -- the same order
50
+ * `scripts/calls-sfu-deploy.mjs` ran by hand, now shared with the platform
51
+ * `sfu` step. Every step here is safe to re-run, the same "safe to re-run"
52
+ * {@link applyCallsSfu} itself promises, so calling this twice with the same
53
+ * `session`/`config` changes nothing on the second call.
54
+ */
55
+ export async function applyCallsSfuOverSession(session, config, expectedFingerprint) {
56
+ const host = toCallsSfuHostPort(session, expectedFingerprint);
57
+ const preflight = await host.exec(PREFLIGHT_COMMAND);
58
+ if (preflight.exitCode !== 0) {
59
+ throw new Error(preflightFailureMessage(preflight));
60
+ }
61
+ const mkdir = await host.exec(`mkdir -p ${dirname(LIVEKIT_COMPOSE_PATH)}`);
62
+ if (mkdir.exitCode !== 0) {
63
+ throw new Error(`mkdir -p failed: ${mkdir.stderr || mkdir.stdout}`);
64
+ }
65
+ // `docker compose up -d` leaves a running container alone when the compose
66
+ // file is unchanged, so LiveKit would keep the old keys in memory after a
67
+ // new `livekit.yaml` lands. Read the config before it is overwritten and
68
+ // restart LiveKit afterwards if the keys (or anything else in it) changed.
69
+ const previous = await host.exec(`cat ${LIVEKIT_CONFIG_PATH}`);
70
+ const configChanged = previous.exitCode === 0 && previous.stdout !== livekitConfigYaml(config);
71
+ await applyCallsSfu(host, config);
72
+ if (configChanged) {
73
+ const restart = await host.exec(`docker compose -f ${LIVEKIT_COMPOSE_PATH} restart livekit`);
74
+ if (restart.exitCode !== 0) {
75
+ throw new Error(`restarting livekit failed: ${restart.stderr || restart.stdout}`);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The real {@link CallsSfuSshConnector} (`plan/platform-sfu.ts`), over `ssh2`:
3
+ * the same `connect()`/`exec`/SFTP wiring `scripts/calls-sfu-deploy.mjs` uses
4
+ * by hand. A host (archon) passes {@link connectCallsSfuSsh} as the `ssh` dep
5
+ * of `CallsSfuPlatformDeps`.
6
+ *
7
+ * The host key is verified inside `ssh2`'s own `hostVerifier`, which returns
8
+ * `false` on a mismatch or a missing fingerprint so the handshake is refused
9
+ * before any authentication happens and the private key is never offered to
10
+ * an unknown host. `applyCallsSfuOverSession` checks the captured key again.
11
+ *
12
+ * `ssh2` is loaded lazily and the `Client` class can be injected, so a test
13
+ * hands in a fake and never opens a socket.
14
+ */
15
+ import type { CallsSfuSshSession } from './calls-sfu-ssh-adapter.js';
16
+ interface SshStream {
17
+ on(event: string, listener: (...args: never[]) => void): SshStream;
18
+ stderr: {
19
+ on(event: string, listener: (...args: never[]) => void): unknown;
20
+ };
21
+ }
22
+ interface SshSftp {
23
+ writeFile(path: string, contents: string, options: {
24
+ mode: number;
25
+ } | undefined, cb: (err?: Error | null) => void): void;
26
+ chmod(path: string, mode: number, cb: (err?: Error | null) => void): void;
27
+ }
28
+ interface SshClient {
29
+ on(event: string, listener: (...args: never[]) => void): SshClient;
30
+ connect(options: Record<string, unknown>): void;
31
+ exec(command: string, cb: (err: Error | undefined, stream: SshStream) => void): void;
32
+ sftp(cb: (err: Error | undefined, sftp: SshSftp) => void): void;
33
+ end(): void;
34
+ }
35
+ export type SshClientConstructor = new () => SshClient;
36
+ export interface CallsSfuSshConnectOptions {
37
+ address: string;
38
+ sshUser: string;
39
+ sshPort: number;
40
+ sshPrivateKey: string;
41
+ /** `SHA256:...`. Without it every host key is refused. */
42
+ sshHostFingerprint: string | undefined;
43
+ }
44
+ export declare function connectCallsSfuSsh(options: CallsSfuSshConnectOptions, deps?: {
45
+ Client?: SshClientConstructor;
46
+ }): Promise<CallsSfuSshSession & {
47
+ close(): void;
48
+ }>;
49
+ export {};
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The real {@link CallsSfuSshConnector} (`plan/platform-sfu.ts`), over `ssh2`:
3
+ * the same `connect()`/`exec`/SFTP wiring `scripts/calls-sfu-deploy.mjs` uses
4
+ * by hand. A host (archon) passes {@link connectCallsSfuSsh} as the `ssh` dep
5
+ * of `CallsSfuPlatformDeps`.
6
+ *
7
+ * The host key is verified inside `ssh2`'s own `hostVerifier`, which returns
8
+ * `false` on a mismatch or a missing fingerprint so the handshake is refused
9
+ * before any authentication happens and the private key is never offered to
10
+ * an unknown host. `applyCallsSfuOverSession` checks the captured key again.
11
+ *
12
+ * `ssh2` is loaded lazily and the `Client` class can be injected, so a test
13
+ * hands in a fake and never opens a socket.
14
+ */
15
+ import { verifyHostKey } from './calls-sfu-deploy-support.js';
16
+ function sshExec(conn, command) {
17
+ return new Promise((resolve, reject) => {
18
+ conn.exec(command, (err, stream) => {
19
+ if (err)
20
+ return reject(err);
21
+ let stdout = '';
22
+ let stderr = '';
23
+ stream.on('close', (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
24
+ stream.on('data', (chunk) => {
25
+ stdout += chunk;
26
+ });
27
+ stream.stderr.on('data', (chunk) => {
28
+ stderr += chunk;
29
+ });
30
+ });
31
+ });
32
+ }
33
+ function sshWriteFile(conn, path, contents, mode) {
34
+ return new Promise((resolve, reject) => {
35
+ conn.sftp((err, sftp) => {
36
+ if (err)
37
+ return reject(err);
38
+ sftp.writeFile(path, contents, mode ? { mode } : undefined, (writeErr) => {
39
+ if (writeErr)
40
+ return reject(writeErr);
41
+ // `mode` on writeFile only applies when the file is created; chmod
42
+ // repairs a file that already existed with looser permissions.
43
+ if (!mode)
44
+ return resolve();
45
+ sftp.chmod(path, mode, (chmodErr) => (chmodErr ? reject(chmodErr) : resolve()));
46
+ });
47
+ });
48
+ });
49
+ }
50
+ export async function connectCallsSfuSsh(options, deps = {}) {
51
+ const Client = deps.Client ?? (await loadSsh2()).Client;
52
+ return new Promise((resolve, reject) => {
53
+ const conn = new Client();
54
+ let hostKey = Buffer.alloc(0);
55
+ conn.on('ready', () => resolve({
56
+ hostKey,
57
+ exec: (command) => sshExec(conn, command),
58
+ writeFile: (path, contents, mode) => sshWriteFile(conn, path, contents, mode),
59
+ close: () => conn.end(),
60
+ }));
61
+ conn.on('error', reject);
62
+ conn.connect({
63
+ host: options.address,
64
+ port: options.sshPort,
65
+ username: options.sshUser,
66
+ privateKey: options.sshPrivateKey,
67
+ // One argument: ssh2 treats the return value as the verdict.
68
+ hostVerifier: (key) => {
69
+ hostKey = key;
70
+ return verifyHostKey(key, options.sshHostFingerprint);
71
+ },
72
+ });
73
+ });
74
+ }
75
+ async function loadSsh2() {
76
+ const specifier = 'ssh2';
77
+ const mod = (await import(specifier));
78
+ const Client = mod.Client ?? mod.default?.Client;
79
+ if (!Client)
80
+ throw new Error('ssh2 did not export a Client');
81
+ return { Client };
82
+ }
@@ -17,8 +17,9 @@
17
17
  * specifically to keep Coolify's own firewall script (`ADR 0011`) untouched
18
18
  * (calls#15). {@link CallsSfuHostPort} is deliberately narrow -- write a
19
19
  * file, run a command -- so a real implementation is a thin SSH/SFTP client
20
- * (`packages/foundry/scripts/calls-sfu-deploy.mjs`), never imported here,
21
- * and every test in `calls-sfu.test.ts` runs against a fake.
20
+ * (`packages/foundry/scripts/calls-sfu-deploy.mjs`, and, as of #170, the
21
+ * platform's own `sfu:livekit` step, `plan/platform-sfu.ts`), never imported
22
+ * here, and every test in `calls-sfu.test.ts` runs against a fake.
22
23
  *
23
24
  * The address, the LiveKit API key and its secret are never hardcoded here
24
25
  * ({@link CallsSfuConfig}): PR 344's review on app-template's
@@ -102,8 +103,8 @@ export declare const CLOUDFLARED_ENV_PATH = "/opt/livekit/cloudflared.env";
102
103
  * anywhere a supply-chain review has asked for a digest pin yet.
103
104
  */
104
105
  export declare const LIVEKIT_IMAGE_REF = "livekit/livekit-server:latest";
105
- /** Same tag policy as {@link LIVEKIT_IMAGE_REF}; matches `scripts/shared-ingress.mjs`'s own `CLOUDFLARED_IMAGE_REF` constant (duplicated, not imported -- this package and that top-level script sit in separate places in the tree). */
106
- export declare const CLOUDFLARED_IMAGE_REF = "cloudflare/cloudflared:latest";
106
+ /** Pinned to a release tag; equals `scripts/shared-ingress.mjs`'s own `CLOUDFLARED_IMAGE_REF` constant (duplicated, not imported -- this package and that top-level script sit in separate places in the tree). */
107
+ export declare const CLOUDFLARED_IMAGE_REF = "cloudflare/cloudflared:2026.9.3";
107
108
  /** LiveKit signalling -- reachable only through the tunnel as of #127 (see {@link CALLS_SFU_PRIVATE_CIDR}). */
108
109
  export declare const LIVEKIT_SIGNALLING_PORT = 7880;
109
110
  /** Media fallback for networks that block UDP -- reaches the SFU's own address directly. */
package/dist/calls-sfu.js CHANGED
@@ -17,8 +17,9 @@
17
17
  * specifically to keep Coolify's own firewall script (`ADR 0011`) untouched
18
18
  * (calls#15). {@link CallsSfuHostPort} is deliberately narrow -- write a
19
19
  * file, run a command -- so a real implementation is a thin SSH/SFTP client
20
- * (`packages/foundry/scripts/calls-sfu-deploy.mjs`), never imported here,
21
- * and every test in `calls-sfu.test.ts` runs against a fake.
20
+ * (`packages/foundry/scripts/calls-sfu-deploy.mjs`, and, as of #170, the
21
+ * platform's own `sfu:livekit` step, `plan/platform-sfu.ts`), never imported
22
+ * here, and every test in `calls-sfu.test.ts` runs against a fake.
22
23
  *
23
24
  * The address, the LiveKit API key and its secret are never hardcoded here
24
25
  * ({@link CallsSfuConfig}): PR 344's review on app-template's
@@ -99,8 +100,8 @@ export const CLOUDFLARED_ENV_PATH = '/opt/livekit/cloudflared.env';
99
100
  * anywhere a supply-chain review has asked for a digest pin yet.
100
101
  */
101
102
  export const LIVEKIT_IMAGE_REF = 'livekit/livekit-server:latest';
102
- /** Same tag policy as {@link LIVEKIT_IMAGE_REF}; matches `scripts/shared-ingress.mjs`'s own `CLOUDFLARED_IMAGE_REF` constant (duplicated, not imported -- this package and that top-level script sit in separate places in the tree). */
103
- export const CLOUDFLARED_IMAGE_REF = 'cloudflare/cloudflared:latest';
103
+ /** Pinned to a release tag; equals `scripts/shared-ingress.mjs`'s own `CLOUDFLARED_IMAGE_REF` constant (duplicated, not imported -- this package and that top-level script sit in separate places in the tree). */
104
+ export const CLOUDFLARED_IMAGE_REF = 'cloudflare/cloudflared:2026.9.3';
104
105
  /** LiveKit signalling -- reachable only through the tunnel as of #127 (see {@link CALLS_SFU_PRIVATE_CIDR}). */
105
106
  export const LIVEKIT_SIGNALLING_PORT = 7880;
106
107
  /** Media fallback for networks that block UDP -- reaches the SFU's own address directly. */
package/dist/index.d.ts CHANGED
@@ -1,21 +1,46 @@
1
1
  export type { OrgId, MailMode, MailConfig, ChatConfig, CallsConfig, IssuerConfig, SiteConfig, IssuerAppKind, IssuerApp, HostingConfig, SsoConfig, GuestsConfig, SoloConfig, StorageConfig, AiConfig, BuyerAddress, BuyerIdentity, AdoptedAppForm, AdoptedApp, OwnerIdentity, DeclaredOrg, } from './org.js';
2
2
  export { validateBuyerIdentity, validateHosting, validateOwner, validateSite } from './org.js';
3
+ export type { DeclaredPlatform, PlatformService, PlatformServiceKey } from './platform.js';
4
+ export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
5
+ export type { RequestableField, CallsChange, StorageLimitChange, AiLimitChange, DomainChange, MailTierChange, RequestedChangeValue, ChangeClassification, RequestedChangeState, RequestedChange, } from './requested-changes.js';
6
+ export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
3
7
  export type { SlugRejectionReason, SlugRejection } from './slug.js';
4
8
  export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
5
9
  export type { Checkpoint, LastApplied, AppliedState } from './state.js';
6
10
  export type { PlanStep, UnmanagedItem, Plan } from './plan.js';
7
- export type { DeclaredOrgStore } from './store.js';
11
+ export type { DeclaredOrgStore, PlatformStore, RequestedChangeStore } from './store.js';
8
12
  export type { PlanApply } from './apply.js';
9
- export { createPostgresDeclaredOrgStore, DeclaredOrgError } from './store/postgres.js';
13
+ export { createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
10
14
  export { createPostgresAuditLog } from './store/audit.js';
11
15
  export type { FoundryStoreDb } from './store/postgres.js';
12
- export { declaredOrgs, foundryAppliedState, foundryAuditEvents, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, } from './store/schema.js';
13
- export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, StepAction, PlannedStep, PlanDetail, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
14
- export { readLiveResources, computePlanDetail, toPlan, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, } from './plan/index.js';
15
- export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, FilesOrgLimits, AiOrgLimits, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, } from './plan/index.js';
16
+ export { declaredOrgs, foundryAppliedState, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
17
+ export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, PlanDetail, PlatformPlanDetail, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
18
+ export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
19
+ export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
20
+ export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
21
+ export type { PlatformOrgClientFactories } from './plan/index.js';
22
+ export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
23
+ export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, } from './plan/index.js';
16
24
  export { FoundryStepExecutor, NotBuiltError, healthUrls } from './plan/index.js';
25
+ export { HttpJobsSchedulerClient } from './plan/index.js';
26
+ export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
27
+ export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './plan/index.js';
28
+ export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
29
+ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './plan/index.js';
30
+ export { PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, PLATFORM_AI_MINTER_MISSING, } from './plan/index.js';
31
+ export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './plan/index.js';
32
+ export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './plan/index.js';
33
+ export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './plan/index.js';
34
+ export type { CallsSfuSshSession } from './calls-sfu-ssh-adapter.js';
35
+ export { connectCallsSfuSsh } from './calls-sfu-ssh-client.js';
36
+ export type { CallsSfuSshConnectOptions } from './calls-sfu-ssh-client.js';
37
+ export { UnknownCallsSfuHostKeyError, toCallsSfuHostPort, applyCallsSfuOverSession, } from './calls-sfu-ssh-adapter.js';
38
+ export type { LivekitCredentials } from './calls-sfu-keys.js';
39
+ export { mintLivekitCredentials } from './calls-sfu-keys.js';
17
40
  export type { MigrateSql, MigrationResult } from './migrate.js';
18
41
  export { MIGRATIONS_DIR, migrate } from './migrate.js';
42
+ export type { BootstrapCompany, BootstrapExecutorDeps, BootstrapOwner, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap/index.js';
43
+ export { bootstrapPlatform } from './bootstrap/index.js';
19
44
  export type { RunnerDb } from './runner/db.js';
20
45
  export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
21
46
  export { createJobRunner } from './runner/runner.js';
package/dist/index.js CHANGED
@@ -1,10 +1,23 @@
1
1
  export { validateBuyerIdentity, validateHosting, validateOwner, validateSite } from './org.js';
2
+ export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
3
+ export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
2
4
  export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
3
- export { createPostgresDeclaredOrgStore, DeclaredOrgError } from './store/postgres.js';
5
+ export { createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
4
6
  export { createPostgresAuditLog } from './store/audit.js';
5
- export { declaredOrgs, foundryAppliedState, foundryAuditEvents, } from './store/schema.js';
6
- export { readLiveResources, computePlanDetail, toPlan, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, } from './plan/index.js';
7
+ export { declaredOrgs, foundryAppliedState, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
8
+ export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
9
+ export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
10
+ export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
7
11
  export { FoundryStepExecutor, NotBuiltError, healthUrls } from './plan/index.js';
12
+ export { HttpJobsSchedulerClient } from './plan/index.js';
13
+ export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
14
+ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './plan/index.js';
15
+ export { PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, PLATFORM_AI_MINTER_MISSING, } from './plan/index.js';
16
+ export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './plan/index.js';
17
+ export { connectCallsSfuSsh } from './calls-sfu-ssh-client.js';
18
+ export { UnknownCallsSfuHostKeyError, toCallsSfuHostPort, applyCallsSfuOverSession, } from './calls-sfu-ssh-adapter.js';
19
+ export { mintLivekitCredentials } from './calls-sfu-keys.js';
8
20
  export { MIGRATIONS_DIR, migrate } from './migrate.js';
21
+ export { bootstrapPlatform } from './bootstrap/index.js';
9
22
  export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
10
23
  export { createJobRunner } from './runner/runner.js';
@@ -0,0 +1,61 @@
1
+ -- @wtfalch/foundry: the declared platform -- the one shared set of services
2
+ -- (auth, mail, sfu, files, ai, jobs, integrations, archon) decided in #165,
3
+ -- owned by Company Included, with its own plan and apply. Mirrors
4
+ -- src/store/schema.ts's `foundryDeclaredPlatform` table.
5
+ --
6
+ -- Singleton by construction: `id` is a boolean that can only ever be `true`
7
+ -- (the check constraint below refuses `false`), and it is the primary key,
8
+ -- so a second row can never exist. This is the same "one declared platform"
9
+ -- decision `foundry_declared_orgs` enforces per-org uniqueness for (#28,
10
+ -- #131), applied here to the whole table instead of a column.
11
+
12
+ create table if not exists foundry_declared_platform (
13
+ id boolean not null default true,
14
+
15
+ -- Company Included's own OrgId (org.ts). Not a level-1 org itself, but
16
+ -- the platform's resources are billed and owned by one.
17
+ owner_org_id text not null,
18
+
19
+ -- The shared apex every service's hostname defaults under, e.g.
20
+ -- companyincluded.com. Validated by src/platform.ts's `validatePlatform`,
21
+ -- not here, the same two-file split declared_orgs uses (#10).
22
+ apex text not null,
23
+
24
+ auth_enabled boolean not null default false,
25
+ auth_hostname text not null,
26
+ auth_git_ref text not null default '',
27
+
28
+ mail_enabled boolean not null default false,
29
+ mail_hostname text not null,
30
+ mail_git_ref text not null default '',
31
+
32
+ sfu_enabled boolean not null default false,
33
+ sfu_hostname text not null,
34
+ sfu_git_ref text not null default '',
35
+
36
+ files_enabled boolean not null default false,
37
+ files_hostname text not null,
38
+ files_git_ref text not null default '',
39
+
40
+ ai_enabled boolean not null default false,
41
+ ai_hostname text not null,
42
+ ai_git_ref text not null default '',
43
+
44
+ jobs_enabled boolean not null default false,
45
+ jobs_hostname text not null,
46
+ jobs_git_ref text not null default '',
47
+
48
+ integrations_enabled boolean not null default false,
49
+ integrations_hostname text not null,
50
+ integrations_git_ref text not null default '',
51
+
52
+ archon_enabled boolean not null default false,
53
+ archon_hostname text not null,
54
+ archon_git_ref text not null default '',
55
+
56
+ created_at timestamptz not null default now(),
57
+ updated_at timestamptz not null default now(),
58
+
59
+ constraint foundry_declared_platform_pkey primary key (id),
60
+ constraint foundry_declared_platform_singleton check (id)
61
+ );
@@ -0,0 +1,42 @@
1
+ -- @wtfalch/foundry: a company's own requested setting changes on its
2
+ -- declared org (#178, map #165: "a company's Boule can request setting
3
+ -- changes on its own declared org, and Archon applies them"). Mirrors
4
+ -- src/store/schema.ts's `foundryRequestedChanges` table.
5
+ --
6
+ -- `field` is the whitelist itself, checked here the same way
7
+ -- `foundry_declared_orgs_mail_mode_check` checks `mail_mode`'s closed set --
8
+ -- src/requested-changes.ts's `REQUESTABLE_FIELDS` is the source of truth for
9
+ -- what the list means, this constraint is the database-level copy of it.
10
+ -- `value` carries whatever that field's own shape needs (`enabled` for
11
+ -- calls, `limitBytes`/`limitMicros` for storageLimit, and so on) -- one
12
+ -- jsonb column rather than a column per possible field, since only one
13
+ -- field is ever set per request.
14
+ --
15
+ -- `state` starts at whatever `src/requested-changes.ts`'s `classifyChange`
16
+ -- decided for this request at create time (`auto` or `needs_approval`) --
17
+ -- foundry only stores and classifies (#178 "Why"); archon's own workflow is
18
+ -- what carries a request on to `approved`/`rejected`/`applied`.
19
+
20
+ create table if not exists foundry_requested_changes (
21
+ id bigserial not null,
22
+ org_id text not null,
23
+
24
+ field text not null,
25
+ value jsonb not null,
26
+
27
+ requested_by text not null,
28
+ created_at timestamptz not null default now(),
29
+
30
+ state text not null default 'needs_approval',
31
+
32
+ constraint foundry_requested_changes_pkey primary key (id),
33
+ constraint foundry_requested_changes_org_fkey
34
+ foreign key (org_id) references foundry_declared_orgs (id),
35
+ constraint foundry_requested_changes_field_check
36
+ check (field in ('calls', 'storageLimit', 'aiLimit', 'domain', 'mailTier')),
37
+ constraint foundry_requested_changes_state_check
38
+ check (state in ('auto', 'needs_approval', 'approved', 'rejected', 'applied'))
39
+ );
40
+
41
+ create index if not exists foundry_requested_changes_org_id_idx
42
+ on foundry_requested_changes (org_id);
@@ -0,0 +1,19 @@
1
+ -- @wtfalch/foundry: what applying the declared platform (#167) needs to
2
+ -- remember, and how its audit rows are told apart from an org's.
3
+ --
4
+ -- `service_ids` is the platform's own applied state: the provider ids its
5
+ -- apply made or adopted, keyed `system:kind:key` like
6
+ -- `foundry_applied_state.service_ids`. Without it a Cloudflare zone created
7
+ -- by one apply is forgotten if the next plan's read lags behind the create.
8
+ -- It lives on the singleton row because there is one platform, and
9
+ -- `put`'s upsert never writes it, so re-declaring the platform keeps it.
10
+ --
11
+ -- `target` marks an audit row as the platform's. It is null for an org row.
12
+ -- A platform plan is stamped with the owner org's id, so `org_id` alone
13
+ -- cannot say which of the two a row is.
14
+
15
+ alter table foundry_declared_platform
16
+ add column if not exists service_ids jsonb not null default '{}'::jsonb;
17
+
18
+ alter table foundry_audit_events
19
+ add column if not exists target text check (target in ('platform'));
package/dist/org.js CHANGED
@@ -49,6 +49,14 @@ export function validateHosting(hosting) {
49
49
  if (!ISSUER_APP_KINDS.has(app.kind)) {
50
50
  return `issuerApps '${app.name}' has an unknown kind '${app.kind}'`;
51
51
  }
52
+ // The fixed 'agora' ZITADEL client (#164, `plan/diff.ts`'s
53
+ // `planAgoraClient`) already owns the `zitadel:app:agora` key, for
54
+ // every org -- Agora is always on, unlike the cms client's own 'cms'
55
+ // reservation (`validateSite` below), which only applies once
56
+ // `site.enabled` is true.
57
+ if (app.name === 'agora') {
58
+ return `issuerApps '${app.name}' is reserved for the always-on Agora client every org gets`;
59
+ }
52
60
  }
53
61
  return null;
54
62
  }