@wtfalch/foundry 0.3.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.
- package/dist/bootstrap/bootstrap.d.ts +96 -0
- package/dist/bootstrap/bootstrap.js +285 -0
- package/dist/bootstrap/declare.d.ts +36 -0
- package/dist/bootstrap/declare.js +94 -0
- package/dist/bootstrap/drive.d.ts +40 -0
- package/dist/bootstrap/drive.js +51 -0
- package/dist/bootstrap/index.d.ts +3 -0
- package/dist/bootstrap/index.js +1 -0
- package/dist/calls-sfu-deploy-support.d.ts +9 -6
- package/dist/calls-sfu-deploy-support.js +9 -6
- package/dist/calls-sfu-keys.d.ts +21 -0
- package/dist/calls-sfu-keys.js +27 -0
- package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
- package/dist/calls-sfu-ssh-adapter.js +78 -0
- package/dist/calls-sfu-ssh-client.d.ts +49 -0
- package/dist/calls-sfu-ssh-client.js +82 -0
- package/dist/calls-sfu.d.ts +22 -4
- package/dist/calls-sfu.js +22 -4
- package/dist/index.d.ts +31 -6
- package/dist/index.js +16 -3
- package/dist/migrations/0014_calls_enabled.sql +7 -0
- package/dist/migrations/0015_domain_unique.sql +39 -0
- package/dist/migrations/0016_platform.sql +61 -0
- package/dist/migrations/0017_requested_changes.sql +42 -0
- package/dist/migrations/0018_platform_apply.sql +19 -0
- package/dist/org.d.ts +18 -2
- package/dist/org.js +56 -0
- package/dist/plan/agora.d.ts +77 -0
- package/dist/plan/agora.js +127 -0
- package/dist/plan/calls-matrix.d.ts +135 -0
- package/dist/plan/calls-matrix.js +275 -0
- package/dist/plan/cloudflared-connector.d.ts +70 -0
- package/dist/plan/cloudflared-connector.js +106 -0
- package/dist/plan/diff.d.ts +26 -5
- package/dist/plan/diff.js +720 -106
- package/dist/plan/executor.d.ts +731 -36
- package/dist/plan/executor.js +1180 -41
- package/dist/plan/index.d.ts +22 -5
- package/dist/plan/index.js +12 -2
- package/dist/plan/jobs-scheduler-http.d.ts +29 -0
- package/dist/plan/jobs-scheduler-http.js +50 -0
- package/dist/plan/live.d.ts +51 -2
- package/dist/plan/live.js +8 -0
- package/dist/plan/plan-apply.d.ts +192 -3
- package/dist/plan/plan-apply.js +290 -14
- package/dist/plan/platform-diff.d.ts +312 -0
- package/dist/plan/platform-diff.js +867 -0
- package/dist/plan/platform-org-ports.d.ts +50 -0
- package/dist/plan/platform-org-ports.js +70 -0
- package/dist/plan/platform-org.d.ts +120 -0
- package/dist/plan/platform-org.js +115 -0
- package/dist/plan/platform-providers.d.ts +501 -0
- package/dist/plan/platform-providers.js +3029 -0
- package/dist/plan/platform-sfu.d.ts +93 -0
- package/dist/plan/platform-sfu.js +100 -0
- package/dist/plan/providers-readers.js +22 -1
- package/dist/plan/zitadel-machine-user.d.ts +34 -0
- package/dist/plan/zitadel-machine-user.js +95 -0
- package/dist/plan.d.ts +6 -0
- package/dist/platform.d.ts +55 -0
- package/dist/platform.js +46 -0
- package/dist/requested-changes.d.ts +108 -0
- package/dist/requested-changes.js +95 -0
- package/dist/slug.js +21 -2
- package/dist/store/audit.js +4 -2
- package/dist/store/postgres.d.ts +40 -7
- package/dist/store/postgres.js +257 -29
- package/dist/store/schema.d.ts +700 -0
- package/dist/store/schema.js +58 -0
- package/dist/store.d.ts +56 -0
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/calls-sfu.d.ts
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
|
|
21
|
-
*
|
|
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
|
-
/**
|
|
106
|
-
export declare const CLOUDFLARED_IMAGE_REF = "cloudflare/cloudflared:
|
|
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. */
|
|
@@ -118,6 +119,23 @@ export declare const LIVEKIT_MEDIA_UDP_RANGE = "50000:60000";
|
|
|
118
119
|
* reachable from the public internet at all -- #127's "Decided" section.
|
|
119
120
|
*/
|
|
120
121
|
export declare const CALLS_SFU_PRIVATE_CIDR = "10.10.10.0/24";
|
|
122
|
+
/**
|
|
123
|
+
* The SFU's own address on {@link CALLS_SFU_PRIVATE_CIDR}'s private network
|
|
124
|
+
* -- decided as a fixed value (foundry#145's "Decided", 2026-09-27) rather
|
|
125
|
+
* than threaded through as config: the estate has exactly one calls SFU (VM
|
|
126
|
+
* 105, this file's own top doc comment), and `.105` is that VM's own number.
|
|
127
|
+
* The calls service (foundry#145, part 2 of #128) reaches LiveKit's API here,
|
|
128
|
+
* over `vmbr1`, never through the tunnel.
|
|
129
|
+
*/
|
|
130
|
+
export declare const CALLS_SFU_PRIVATE_URL = "http://10.10.10.105:7880";
|
|
131
|
+
/**
|
|
132
|
+
* The SFU's public `wss://`-capable address, reached through its own named
|
|
133
|
+
* tunnel (#127, `scripts/calls-sfu-ingress.mjs`) -- also decided as a fixed
|
|
134
|
+
* value (foundry#145's "Decided", 2026-09-27): `../slug.ts` reserves the
|
|
135
|
+
* `calls-sfu` slug for exactly this reason, so no org can ever take this
|
|
136
|
+
* hostname out from under it.
|
|
137
|
+
*/
|
|
138
|
+
export declare const CALLS_SFU_PUBLIC_URL = "https://calls-sfu.oikos.host";
|
|
121
139
|
/**
|
|
122
140
|
* Exported for the test file. Reads `ufw status`'s own report rather than
|
|
123
141
|
* assuming the exit code of a blind `ufw delete allow 7880/tcp` -- real
|
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
|
|
21
|
-
*
|
|
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
|
-
/**
|
|
103
|
-
export const CLOUDFLARED_IMAGE_REF = 'cloudflare/cloudflared:
|
|
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. */
|
|
@@ -115,6 +116,23 @@ export const LIVEKIT_MEDIA_UDP_RANGE = '50000:60000';
|
|
|
115
116
|
* reachable from the public internet at all -- #127's "Decided" section.
|
|
116
117
|
*/
|
|
117
118
|
export const CALLS_SFU_PRIVATE_CIDR = '10.10.10.0/24';
|
|
119
|
+
/**
|
|
120
|
+
* The SFU's own address on {@link CALLS_SFU_PRIVATE_CIDR}'s private network
|
|
121
|
+
* -- decided as a fixed value (foundry#145's "Decided", 2026-09-27) rather
|
|
122
|
+
* than threaded through as config: the estate has exactly one calls SFU (VM
|
|
123
|
+
* 105, this file's own top doc comment), and `.105` is that VM's own number.
|
|
124
|
+
* The calls service (foundry#145, part 2 of #128) reaches LiveKit's API here,
|
|
125
|
+
* over `vmbr1`, never through the tunnel.
|
|
126
|
+
*/
|
|
127
|
+
export const CALLS_SFU_PRIVATE_URL = `http://10.10.10.105:${LIVEKIT_SIGNALLING_PORT}`;
|
|
128
|
+
/**
|
|
129
|
+
* The SFU's public `wss://`-capable address, reached through its own named
|
|
130
|
+
* tunnel (#127, `scripts/calls-sfu-ingress.mjs`) -- also decided as a fixed
|
|
131
|
+
* value (foundry#145's "Decided", 2026-09-27): `../slug.ts` reserves the
|
|
132
|
+
* `calls-sfu` slug for exactly this reason, so no org can ever take this
|
|
133
|
+
* hostname out from under it.
|
|
134
|
+
*/
|
|
135
|
+
export const CALLS_SFU_PUBLIC_URL = 'https://calls-sfu.oikos.host';
|
|
118
136
|
/**
|
|
119
137
|
* Matches `ufw status`'s report of the exact wide-open rule PR 125 added
|
|
120
138
|
* (`ufw allow 7880/tcp`) -- "Anywhere" in the From column -- and not the
|
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 {
|
|
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,7 @@
|
|
|
1
|
+
-- @wtfalch/foundry: turns on an org's own calls Matrix stack (#128). Mirrors
|
|
2
|
+
-- `src/store/schema.ts`'s `callsEnabled`. Defaults false, same as every other
|
|
3
|
+
-- per-org on/off switch (`site_enabled`, `hosting_enabled`) -- an existing
|
|
4
|
+
-- org has no calls stack until it declares one.
|
|
5
|
+
|
|
6
|
+
alter table foundry_declared_orgs
|
|
7
|
+
add column if not exists calls_enabled boolean not null default false;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the declared org's domain is globally unique, one non-
|
|
2
|
+
-- null value per org, on top of #0001's `foundry_declared_orgs` table (#131).
|
|
3
|
+
-- Mirrors src/store/schema.ts's `domain` column, which has no index of its
|
|
4
|
+
-- own to express -- this file, not the schema, is the index's source of
|
|
5
|
+
-- truth, the same convention `0004_slug_unique.sql`'s header explains.
|
|
6
|
+
--
|
|
7
|
+
-- Partial (`where domain is not null`): a brand new org has no domain yet
|
|
8
|
+
-- (org.ts's own doc comment on `DeclaredOrg.domain`), so a plain unique
|
|
9
|
+
-- index would let only one such org exist at a time -- every other null
|
|
10
|
+
-- would collide with it.
|
|
11
|
+
--
|
|
12
|
+
-- Forced by the same routing collision `0004_slug_unique.sql` exists for: an
|
|
13
|
+
-- org's domain is the Cloudflare zone and Coolify hostname it is served on
|
|
14
|
+
-- once any service opts in (org.ts's `MailConfig`/`ChatConfig`/etc.
|
|
15
|
+
-- `usesDomain`, plan/executor.ts's `hostnameFor`), so two orgs sharing a
|
|
16
|
+
-- domain would fight over both from the moment the second is declared, with
|
|
17
|
+
-- no refusal today. src/store/postgres.ts's `create` and `update` already
|
|
18
|
+
-- call org.ts's `validateDomain` before this index is ever reached; the
|
|
19
|
+
-- index exists to catch the race the pre-check cannot -- two concurrent
|
|
20
|
+
-- writes for the same domain -- which postgres.ts maps back to the same
|
|
21
|
+
-- `DeclaredOrgError` a pre-check would have thrown, per #18's rule that a
|
|
22
|
+
-- caller gets a reason by name either way.
|
|
23
|
+
--
|
|
24
|
+
-- Format validity (DNS-label-per-segment) is org.ts's `validateDomain`'s
|
|
25
|
+
-- job, not a CHECK constraint's -- #0001's header already gives the
|
|
26
|
+
-- reasoning for keeping "combinations foundry knows are impossible" in the
|
|
27
|
+
-- code path that can explain itself, not in SQL that can't. Whether this
|
|
28
|
+
-- index (or `validateDomain`) should normalize case before comparing is
|
|
29
|
+
-- still open (#131 "Decide") -- `validateDomain` requires lowercase input
|
|
30
|
+
-- outright today, so this index never has to compare two differently-cased
|
|
31
|
+
-- values in the first place.
|
|
32
|
+
--
|
|
33
|
+
-- Human step before this ships (#131): audit existing declared orgs for a
|
|
34
|
+
-- pre-existing domain collision -- this index refuses to apply if one
|
|
35
|
+
-- already exists.
|
|
36
|
+
|
|
37
|
+
create unique index if not exists foundry_declared_orgs_domain_idx
|
|
38
|
+
on foundry_declared_orgs (domain)
|
|
39
|
+
where domain is not null;
|
|
@@ -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);
|