@wtfalch/foundry 0.5.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/bootstrap.d.ts +16 -1
- package/dist/bootstrap/bootstrap.js +40 -2
- package/dist/bootstrap/declare.d.ts +3 -1
- package/dist/bootstrap/declare.js +12 -3
- package/dist/bootstrap/versions.d.ts +17 -0
- package/dist/bootstrap/versions.js +12 -0
- package/dist/calls-sfu-deploy-support.d.ts +8 -0
- package/dist/calls-sfu-deploy-support.js +10 -1
- package/dist/calls-sfu.d.ts +36 -12
- package/dist/calls-sfu.js +42 -9
- package/dist/index.d.ts +10 -4
- package/dist/index.js +6 -3
- package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
- package/dist/migrations/0020_mail_apps.sql +7 -0
- package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
- package/dist/migrations/0022_chat_enabled.sql +6 -0
- package/dist/migrations/0023_hosting_app_env.sql +8 -0
- package/dist/migrations/0024_audit_events.sql +211 -0
- package/dist/migrations/0025_audit_display.sql +147 -0
- package/dist/migrations/0026_audit_erase_email_guard.sql +78 -0
- package/dist/migrations/0027_audit_chain.sql +170 -0
- package/dist/migrations/0028_audit_rls.sql +111 -0
- package/dist/org.d.ts +105 -0
- package/dist/org.js +101 -0
- package/dist/plan/agora.d.ts +21 -0
- package/dist/plan/agora.js +23 -0
- package/dist/plan/calls-matrix.d.ts +2 -2
- package/dist/plan/calls-matrix.js +3 -2
- package/dist/plan/cloudflared-connector.d.ts +3 -2
- package/dist/plan/cloudflared-connector.js +5 -3
- package/dist/plan/diff.d.ts +15 -2
- package/dist/plan/diff.js +399 -51
- package/dist/plan/executor.d.ts +307 -61
- package/dist/plan/executor.js +489 -129
- package/dist/plan/index.d.ts +9 -1
- package/dist/plan/index.js +5 -1
- package/dist/plan/login-host.d.ts +64 -0
- package/dist/plan/login-host.js +52 -0
- package/dist/plan/mail-copy.d.ts +125 -0
- package/dist/plan/mail-copy.js +166 -0
- package/dist/plan/mail-identity-allowlist.d.ts +50 -0
- package/dist/plan/mail-identity-allowlist.js +11 -0
- package/dist/plan/mail-identity.d.ts +68 -0
- package/dist/plan/mail-identity.js +17 -0
- package/dist/plan/plan-apply.d.ts +26 -8
- package/dist/plan/plan-apply.js +10 -9
- package/dist/plan/platform-diff.d.ts +207 -10
- package/dist/plan/platform-diff.js +403 -15
- package/dist/plan/platform-org.d.ts +3 -2
- package/dist/plan/platform-providers.d.ts +139 -25
- package/dist/plan/platform-providers.js +1531 -95
- package/dist/plan/platform-sfu.d.ts +54 -19
- package/dist/plan/platform-sfu.js +64 -22
- package/dist/plan/providers-readers.js +44 -1
- package/dist/plan/zitadel-machine-user.d.ts +60 -11
- package/dist/plan/zitadel-machine-user.js +182 -39
- package/dist/requested-changes.d.ts +17 -7
- package/dist/requested-changes.js +37 -7
- package/dist/store/audit.d.ts +40 -4
- package/dist/store/audit.js +114 -29
- package/dist/store/postgres.js +35 -3
- package/dist/store/schema.d.ts +109 -1
- package/dist/store/schema.js +20 -0
- package/dist/store.d.ts +7 -0
- package/package.json +5 -3
- package/versions.json +8 -0
|
@@ -7,7 +7,16 @@ export interface BootstrapPlatformInput {
|
|
|
7
7
|
company: BootstrapCompany;
|
|
8
8
|
owner: BootstrapOwner;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The master keys, read from the host's held keys by the caller.
|
|
12
|
+
*
|
|
13
|
+
* #213: `coolifyUrl`, `coolifyToken` and `cloudflareApiToken` are also what
|
|
14
|
+
* `bootstrapPlatform` puts as Archon's own held keys
|
|
15
|
+
* (`platform:master:coolify-url`, `-coolify-token`, `-cloudflare-token`)
|
|
16
|
+
* before Archon's app deploys, so its onboarding can read them back.
|
|
17
|
+
* `cloudflareApiToken` needs "API Tokens Write" for that: Archon mints its
|
|
18
|
+
* own scoped child tokens from it.
|
|
19
|
+
*/
|
|
11
20
|
export interface RootCredentials {
|
|
12
21
|
coolifyUrl: string;
|
|
13
22
|
coolifyToken: string;
|
|
@@ -32,8 +41,14 @@ export interface BootstrapPlatformDeps {
|
|
|
32
41
|
migrateSql: MigrateSql;
|
|
33
42
|
rootCredentials: () => Promise<RootCredentials>;
|
|
34
43
|
deliverSecret: (ref: string, secret: string) => Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* `put` (#213) is optional, like `PlatformArchonDeps.heldKeys`'s own: a
|
|
46
|
+
* host without one still compiles, and Archon's master keys are reported
|
|
47
|
+
* blocked, not skipped, while it is missing.
|
|
48
|
+
*/
|
|
35
49
|
heldKeys: {
|
|
36
50
|
resolve(ref: string): Promise<string | null>;
|
|
51
|
+
put?(ref: string, value: string): Promise<void>;
|
|
37
52
|
};
|
|
38
53
|
/** Per-step dependencies (auth, mail, files, ai, integrations, archon, sfu). A step without its own is reported blocked. */
|
|
39
54
|
executor?: BootstrapExecutorDeps;
|
|
@@ -106,12 +106,39 @@ export function guardStepDeps(executor, redactor, deliverSecret, heldKeys) {
|
|
|
106
106
|
const copy = { ...deps };
|
|
107
107
|
if ('deliverSecret' in copy)
|
|
108
108
|
copy.deliverSecret = deliverSecret;
|
|
109
|
-
|
|
109
|
+
// #201: ai's own default minter needs heldKeys too, to re-sign a mint
|
|
110
|
+
// request against its signing key when an earlier run pinned it but
|
|
111
|
+
// crashed before the management key itself was minted and delivered.
|
|
112
|
+
if (key === 'archon' || key === 'ai' || 'heldKeys' in copy)
|
|
110
113
|
copy.heldKeys = heldKeys;
|
|
111
114
|
out[key] = copy;
|
|
112
115
|
}
|
|
113
116
|
return out;
|
|
114
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* #213: stamps `PlatformArchonDeps.masterKeys` from the root credentials
|
|
120
|
+
* `bootstrapPlatform` already resolved, onto whatever the host gave the
|
|
121
|
+
* archon step through `deps.executor.archon` -- the values are
|
|
122
|
+
* `credentials`' own, not something the host names separately, so this is
|
|
123
|
+
* not one more thing for `guardStepDeps`'s caller to wire by hand. A run
|
|
124
|
+
* with no `archon` deps at all (the step then reported blocked, as before)
|
|
125
|
+
* is passed through unchanged.
|
|
126
|
+
*/
|
|
127
|
+
function archonMasterKeys(guarded, credentials) {
|
|
128
|
+
if (!guarded.archon)
|
|
129
|
+
return guarded;
|
|
130
|
+
return {
|
|
131
|
+
...guarded,
|
|
132
|
+
archon: {
|
|
133
|
+
...guarded.archon,
|
|
134
|
+
masterKeys: {
|
|
135
|
+
coolifyUrl: credentials.coolifyUrl,
|
|
136
|
+
coolifyToken: credentials.coolifyToken,
|
|
137
|
+
cloudflareParentToken: credentials.cloudflareApiToken,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
115
142
|
/**
|
|
116
143
|
* The platform's first apply, run from Archon's onboarding flow: migrates
|
|
117
144
|
* foundry's tables, declares Company Included and the platform (never
|
|
@@ -148,6 +175,17 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
148
175
|
redactor.add(value);
|
|
149
176
|
return value;
|
|
150
177
|
},
|
|
178
|
+
// #213: only present when the host's own heldKeys can put, so a host
|
|
179
|
+
// without one leaves Archon's master keys reported blocked rather
|
|
180
|
+
// than silently no-op'd.
|
|
181
|
+
...(deps.heldKeys.put
|
|
182
|
+
? {
|
|
183
|
+
async put(ref, value) {
|
|
184
|
+
redactor.add(value);
|
|
185
|
+
await deps.heldKeys.put?.(ref, value);
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
: {}),
|
|
151
189
|
};
|
|
152
190
|
const migrated = await migrate(deps.migrateSql);
|
|
153
191
|
emit({
|
|
@@ -193,7 +231,7 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
193
231
|
executor: new FoundryPlatformStepExecutor(cloudflare, orgStore, {
|
|
194
232
|
coolify,
|
|
195
233
|
deliverSecret,
|
|
196
|
-
...guardStepDeps(deps.executor, redactor, deliverSecret, heldKeys),
|
|
234
|
+
...archonMasterKeys(guardStepDeps(deps.executor, redactor, deliverSecret, heldKeys), credentials),
|
|
197
235
|
}),
|
|
198
236
|
};
|
|
199
237
|
}
|
|
@@ -7,8 +7,10 @@ export declare const DEFAULT_GIT_REF = "main";
|
|
|
7
7
|
* The Stalwart image tag the platform's mail step runs. `platform.mail.gitRef`
|
|
8
8
|
* is an image tag, not a git ref, and the planner blocks a floating one
|
|
9
9
|
* (`platform-providers.ts`, `mailImageTagBlocked`), so this is a pinned release.
|
|
10
|
+
* Pinned in `versions.json` as `stalwart` (#247) -- this is the pattern that
|
|
11
|
+
* file's loader follows.
|
|
10
12
|
*/
|
|
11
|
-
export declare const DEFAULT_MAIL_IMAGE_TAG
|
|
13
|
+
export declare const DEFAULT_MAIL_IMAGE_TAG: string;
|
|
12
14
|
export interface BootstrapCompany {
|
|
13
15
|
name: string;
|
|
14
16
|
slug: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateOwner } from '../org.js';
|
|
2
2
|
import { PLATFORM_SERVICE_KEYS, defaultPlatformHostname } from '../platform.js';
|
|
3
|
+
import { VERSIONS } from './versions.js';
|
|
3
4
|
/** Company Included's own org id, the same literal `test/fakes.ts`'s `basePlatform()` uses for `ownerOrgId`. */
|
|
4
5
|
export const COMPANY_INCLUDED_ORG_ID = 'org_company_included';
|
|
5
6
|
export const DEFAULT_GIT_REF = 'main';
|
|
@@ -7,8 +8,10 @@ export const DEFAULT_GIT_REF = 'main';
|
|
|
7
8
|
* The Stalwart image tag the platform's mail step runs. `platform.mail.gitRef`
|
|
8
9
|
* is an image tag, not a git ref, and the planner blocks a floating one
|
|
9
10
|
* (`platform-providers.ts`, `mailImageTagBlocked`), so this is a pinned release.
|
|
11
|
+
* Pinned in `versions.json` as `stalwart` (#247) -- this is the pattern that
|
|
12
|
+
* file's loader follows.
|
|
10
13
|
*/
|
|
11
|
-
export const DEFAULT_MAIL_IMAGE_TAG =
|
|
14
|
+
export const DEFAULT_MAIL_IMAGE_TAG = VERSIONS.stalwart;
|
|
12
15
|
/**
|
|
13
16
|
* Every platform service except `sfu` is enabled, like the other steps: a
|
|
14
17
|
* service whose dependencies the host did not supply is reported blocked, not
|
|
@@ -49,12 +52,18 @@ export function declaredCompanyOrg(company, owner) {
|
|
|
49
52
|
slug: company.slug,
|
|
50
53
|
displayName: company.name,
|
|
51
54
|
domain: null,
|
|
52
|
-
mail: {
|
|
53
|
-
|
|
55
|
+
mail: {
|
|
56
|
+
mode: 'off',
|
|
57
|
+
usesDomain: false,
|
|
58
|
+
ownerMailboxAddress: null,
|
|
59
|
+
noReplyLocalPart: 'no-reply',
|
|
60
|
+
},
|
|
61
|
+
chat: { enabled: false, usesDomain: false },
|
|
54
62
|
calls: { enabled: false, usesDomain: false },
|
|
55
63
|
issuer: { usesDomain: false },
|
|
56
64
|
site: { enabled: false, usesDomain: false },
|
|
57
65
|
hosting: {
|
|
66
|
+
appEnv: {},
|
|
58
67
|
enabled: false,
|
|
59
68
|
usesDomain: false,
|
|
60
69
|
database: false,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The estate's pinned third-party image/release refs for the platform
|
|
3
|
+
* services foundry provisions (#247). One JSON file, so Renovate can read
|
|
4
|
+
* and later propose bumps against it, instead of four separate string
|
|
5
|
+
* constants it cannot see. `postgres` has no entry here: no planner step
|
|
6
|
+
* pins a postgres image tag today, so there is no ref to record yet.
|
|
7
|
+
*/
|
|
8
|
+
export interface PinnedVersions {
|
|
9
|
+
zitadel: string;
|
|
10
|
+
synapse: string;
|
|
11
|
+
mas: string;
|
|
12
|
+
livekit: string;
|
|
13
|
+
cloudflared: string;
|
|
14
|
+
stalwart: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function loadVersions(): PinnedVersions;
|
|
17
|
+
export declare const VERSIONS: PinnedVersions;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Read at module load from `versions.json`, which sits beside this
|
|
4
|
+
* package's `package.json` (not under `src`, so plain JSON import would
|
|
5
|
+
* fight `tsconfig.build.json`'s `rootDir`) and ships in the published
|
|
6
|
+
* tarball via `package.json`'s `files`.
|
|
7
|
+
*/
|
|
8
|
+
const VERSIONS_PATH = new URL('../../versions.json', import.meta.url);
|
|
9
|
+
export function loadVersions() {
|
|
10
|
+
return JSON.parse(readFileSync(VERSIONS_PATH, 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
export const VERSIONS = loadVersions();
|
|
@@ -38,6 +38,14 @@ export declare function verifyHostKey(rawKey: Buffer, expectedFingerprint: strin
|
|
|
38
38
|
* this script writes holds no secret. Returns the SFTP file mode to write
|
|
39
39
|
* with, or `undefined` for anything else (the SFTP server's own default).
|
|
40
40
|
* PR #125's review, finding 2; extended for #127's second secret file.
|
|
41
|
+
*
|
|
42
|
+
* A trailing `.tmp` is stripped before comparing: `calls-sfu.ts`'s
|
|
43
|
+
* `applyCallsSfu` writes `livekit.yaml` to a `.tmp` path first and renames
|
|
44
|
+
* it into place (#152 round 2 review, finding 3 -- a temp file plus an
|
|
45
|
+
* atomic rename, rather than overwriting the live config two concurrent
|
|
46
|
+
* applies could otherwise tear). The temp file holds the same secret the
|
|
47
|
+
* final path does, so it gets the same 0600 mode; `mv` preserves it across
|
|
48
|
+
* the rename.
|
|
41
49
|
*/
|
|
42
50
|
export declare function fileWriteMode(path: string): number | undefined;
|
|
43
51
|
/**
|
|
@@ -53,9 +53,18 @@ export function verifyHostKey(rawKey, expectedFingerprint) {
|
|
|
53
53
|
* this script writes holds no secret. Returns the SFTP file mode to write
|
|
54
54
|
* with, or `undefined` for anything else (the SFTP server's own default).
|
|
55
55
|
* PR #125's review, finding 2; extended for #127's second secret file.
|
|
56
|
+
*
|
|
57
|
+
* A trailing `.tmp` is stripped before comparing: `calls-sfu.ts`'s
|
|
58
|
+
* `applyCallsSfu` writes `livekit.yaml` to a `.tmp` path first and renames
|
|
59
|
+
* it into place (#152 round 2 review, finding 3 -- a temp file plus an
|
|
60
|
+
* atomic rename, rather than overwriting the live config two concurrent
|
|
61
|
+
* applies could otherwise tear). The temp file holds the same secret the
|
|
62
|
+
* final path does, so it gets the same 0600 mode; `mv` preserves it across
|
|
63
|
+
* the rename.
|
|
56
64
|
*/
|
|
57
65
|
export function fileWriteMode(path) {
|
|
58
|
-
|
|
66
|
+
const target = path.endsWith('.tmp') ? path.slice(0, -'.tmp'.length) : path;
|
|
67
|
+
return target === LIVEKIT_CONFIG_PATH || target === CLOUDFLARED_ENV_PATH ? 0o600 : undefined;
|
|
59
68
|
}
|
|
60
69
|
// ---------------------------------------------------------------------------
|
|
61
70
|
// 3. Preflight
|
package/dist/calls-sfu.d.ts
CHANGED
|
@@ -21,13 +21,21 @@
|
|
|
21
21
|
* platform's own `sfu:livekit` step, `plan/platform-sfu.ts`), never imported
|
|
22
22
|
* here, and every test in `calls-sfu.test.ts` runs against a fake.
|
|
23
23
|
*
|
|
24
|
-
* The address
|
|
24
|
+
* The address and every LiveKit API key/secret pair are never hardcoded here
|
|
25
25
|
* ({@link CallsSfuConfig}): PR 344's review on app-template's
|
|
26
26
|
* `calls-instance` step found its first cut created three Coolify apps
|
|
27
27
|
* before it ever checked the address was known.
|
|
28
28
|
* {@link requireCallsSfuAddress} is that check, run first, before either
|
|
29
29
|
* file is written.
|
|
30
30
|
*
|
|
31
|
+
* #152: one shared instance, kept apart by *per-company* keys rather than
|
|
32
|
+
* one shared pair -- {@link CallsSfuConfig.livekitKeys} carries every org's
|
|
33
|
+
* own key/secret pair, and {@link livekitConfigYaml} writes all of them into
|
|
34
|
+
* `keys:`. Minting and delivering each org's pair is the platform `sfu`
|
|
35
|
+
* step's job (`plan/platform-sfu.ts`'s `applyCallsSfuPlatformStep`), not
|
|
36
|
+
* this module's -- the same "this package never invents a credential" rule
|
|
37
|
+
* the single-pair design already followed.
|
|
38
|
+
*
|
|
31
39
|
* #127 adds a second compose service, `cloudflared`, so the SFU has a public
|
|
32
40
|
* `wss://` address through its own named tunnel -- browsers on an https page
|
|
33
41
|
* cannot open a plain `ws://` connection, and 7880 was open to the whole
|
|
@@ -64,16 +72,24 @@ export declare class MissingCloudflaredTunnelTokenError extends Error {
|
|
|
64
72
|
export declare function requireCloudflaredTunnelToken(token: string | undefined | null): string;
|
|
65
73
|
/**
|
|
66
74
|
* Everything {@link applyCallsSfu} needs and this package never invents on
|
|
67
|
-
* its own: the SFU's own address (calls#15),
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
75
|
+
* its own: the SFU's own address (calls#15), one LiveKit API key/secret pair
|
|
76
|
+
* per org with calls enabled (#152 -- a shared instance, kept apart by
|
|
77
|
+
* per-company keys, not a port range or a deployment each), and the token
|
|
78
|
+
* for the named tunnel `scripts/calls-sfu-ingress.mjs` creates. All of it
|
|
79
|
+
* comes from the secrets store, at the caller's own script -- #122's and
|
|
80
|
+
* #127's "Human steps" -- except the per-org pairs, which the platform `sfu`
|
|
81
|
+
* step mints itself (`plan/platform-sfu.ts`).
|
|
72
82
|
*/
|
|
73
83
|
export interface CallsSfuConfig {
|
|
74
84
|
address: string;
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
/**
|
|
86
|
+
* API key id -> secret, one entry per org currently enabled for calls.
|
|
87
|
+
* {@link livekitConfigYaml} writes every entry under `keys:`, sorted by key
|
|
88
|
+
* id so the same set of orgs always serializes to the same bytes -- what
|
|
89
|
+
* `calls-sfu-ssh-adapter.ts`'s `applyCallsSfuOverSession` compares, byte
|
|
90
|
+
* for byte, to decide whether LiveKit needs a restart.
|
|
91
|
+
*/
|
|
92
|
+
livekitKeys: Record<string, string>;
|
|
77
93
|
cloudflaredTunnelToken: string;
|
|
78
94
|
}
|
|
79
95
|
/**
|
|
@@ -100,11 +116,12 @@ export declare const CLOUDFLARED_ENV_PATH = "/opt/livekit/cloudflared.env";
|
|
|
100
116
|
/**
|
|
101
117
|
* `:latest`, matching this codebase's one other pinned-by-tag-only image
|
|
102
118
|
* (`scripts/shared-ingress.mjs`'s `CLOUDFLARED_IMAGE_REF`) -- neither runs
|
|
103
|
-
* anywhere a supply-chain review has asked for a digest pin yet.
|
|
119
|
+
* anywhere a supply-chain review has asked for a digest pin yet. Pinned in
|
|
120
|
+
* `versions.json` (#247).
|
|
104
121
|
*/
|
|
105
|
-
export declare const LIVEKIT_IMAGE_REF
|
|
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
|
|
122
|
+
export declare const LIVEKIT_IMAGE_REF: string;
|
|
123
|
+
/** 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). Pinned in `versions.json` (#247). */
|
|
124
|
+
export declare const CLOUDFLARED_IMAGE_REF: string;
|
|
108
125
|
/** LiveKit signalling -- reachable only through the tunnel as of #127 (see {@link CALLS_SFU_PRIVATE_CIDR}). */
|
|
109
126
|
export declare const LIVEKIT_SIGNALLING_PORT = 7880;
|
|
110
127
|
/** Media fallback for networks that block UDP -- reaches the SFU's own address directly. */
|
|
@@ -163,6 +180,13 @@ export declare function cloudflaredEnvFile(token: string): string;
|
|
|
163
180
|
* has one public address of its own, bridged directly rather than sitting
|
|
164
181
|
* behind NAT (calls#15's decision comment), so there is a known-good answer
|
|
165
182
|
* and no reason to make LiveKit discover it via STUN.
|
|
183
|
+
*
|
|
184
|
+
* `keys:` carries one line per entry in {@link CallsSfuConfig.livekitKeys}
|
|
185
|
+
* (#152), sorted by key id -- never object insertion order, which
|
|
186
|
+
* JavaScript does not even guarantee is the order the platform step built
|
|
187
|
+
* the map in. An org dropped from the map (calls turned off) simply has no
|
|
188
|
+
* line here on the next apply; nothing here deletes a key, it only ever
|
|
189
|
+
* writes the set it was given.
|
|
166
190
|
*/
|
|
167
191
|
export declare function livekitConfigYaml(config: CallsSfuConfig): string;
|
|
168
192
|
/**
|
package/dist/calls-sfu.js
CHANGED
|
@@ -21,13 +21,21 @@
|
|
|
21
21
|
* platform's own `sfu:livekit` step, `plan/platform-sfu.ts`), never imported
|
|
22
22
|
* here, and every test in `calls-sfu.test.ts` runs against a fake.
|
|
23
23
|
*
|
|
24
|
-
* The address
|
|
24
|
+
* The address and every LiveKit API key/secret pair are never hardcoded here
|
|
25
25
|
* ({@link CallsSfuConfig}): PR 344's review on app-template's
|
|
26
26
|
* `calls-instance` step found its first cut created three Coolify apps
|
|
27
27
|
* before it ever checked the address was known.
|
|
28
28
|
* {@link requireCallsSfuAddress} is that check, run first, before either
|
|
29
29
|
* file is written.
|
|
30
30
|
*
|
|
31
|
+
* #152: one shared instance, kept apart by *per-company* keys rather than
|
|
32
|
+
* one shared pair -- {@link CallsSfuConfig.livekitKeys} carries every org's
|
|
33
|
+
* own key/secret pair, and {@link livekitConfigYaml} writes all of them into
|
|
34
|
+
* `keys:`. Minting and delivering each org's pair is the platform `sfu`
|
|
35
|
+
* step's job (`plan/platform-sfu.ts`'s `applyCallsSfuPlatformStep`), not
|
|
36
|
+
* this module's -- the same "this package never invents a credential" rule
|
|
37
|
+
* the single-pair design already followed.
|
|
38
|
+
*
|
|
31
39
|
* #127 adds a second compose service, `cloudflared`, so the SFU has a public
|
|
32
40
|
* `wss://` address through its own named tunnel -- browsers on an https page
|
|
33
41
|
* cannot open a plain `ws://` connection, and 7880 was open to the whole
|
|
@@ -44,6 +52,7 @@
|
|
|
44
52
|
* `calls-sfu-deploy-support.ts`'s `fileWriteMode`), never logged, scrubbed
|
|
45
53
|
* from any error `calls-sfu-deploy.mjs` prints.
|
|
46
54
|
*/
|
|
55
|
+
import { VERSIONS } from './bootstrap/versions.js';
|
|
47
56
|
// ---------------------------------------------------------------------------
|
|
48
57
|
// The guard
|
|
49
58
|
// ---------------------------------------------------------------------------
|
|
@@ -97,11 +106,12 @@ export const CLOUDFLARED_ENV_PATH = '/opt/livekit/cloudflared.env';
|
|
|
97
106
|
/**
|
|
98
107
|
* `:latest`, matching this codebase's one other pinned-by-tag-only image
|
|
99
108
|
* (`scripts/shared-ingress.mjs`'s `CLOUDFLARED_IMAGE_REF`) -- neither runs
|
|
100
|
-
* anywhere a supply-chain review has asked for a digest pin yet.
|
|
109
|
+
* anywhere a supply-chain review has asked for a digest pin yet. Pinned in
|
|
110
|
+
* `versions.json` (#247).
|
|
101
111
|
*/
|
|
102
|
-
export const LIVEKIT_IMAGE_REF =
|
|
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 =
|
|
112
|
+
export const LIVEKIT_IMAGE_REF = `livekit/livekit-server:${VERSIONS.livekit}`;
|
|
113
|
+
/** 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). Pinned in `versions.json` (#247). */
|
|
114
|
+
export const CLOUDFLARED_IMAGE_REF = `cloudflare/cloudflared:${VERSIONS.cloudflared}`;
|
|
105
115
|
/** LiveKit signalling -- reachable only through the tunnel as of #127 (see {@link CALLS_SFU_PRIVATE_CIDR}). */
|
|
106
116
|
export const LIVEKIT_SIGNALLING_PORT = 7880;
|
|
107
117
|
/** Media fallback for networks that block UDP -- reaches the SFU's own address directly. */
|
|
@@ -191,9 +201,20 @@ export function cloudflaredEnvFile(token) {
|
|
|
191
201
|
* has one public address of its own, bridged directly rather than sitting
|
|
192
202
|
* behind NAT (calls#15's decision comment), so there is a known-good answer
|
|
193
203
|
* and no reason to make LiveKit discover it via STUN.
|
|
204
|
+
*
|
|
205
|
+
* `keys:` carries one line per entry in {@link CallsSfuConfig.livekitKeys}
|
|
206
|
+
* (#152), sorted by key id -- never object insertion order, which
|
|
207
|
+
* JavaScript does not even guarantee is the order the platform step built
|
|
208
|
+
* the map in. An org dropped from the map (calls turned off) simply has no
|
|
209
|
+
* line here on the next apply; nothing here deletes a key, it only ever
|
|
210
|
+
* writes the set it was given.
|
|
194
211
|
*/
|
|
195
212
|
export function livekitConfigYaml(config) {
|
|
196
213
|
const address = requireCallsSfuAddress(config.address);
|
|
214
|
+
const keys = Object.entries(config.livekitKeys).sort(([a], [b]) => a.localeCompare(b));
|
|
215
|
+
const keysYaml = keys.length === 0
|
|
216
|
+
? 'keys: {}\n'
|
|
217
|
+
: `keys:\n${keys.map(([apiKey, apiSecret]) => ` ${apiKey}: ${apiSecret}`).join('\n')}\n`;
|
|
197
218
|
return `port: ${LIVEKIT_SIGNALLING_PORT}
|
|
198
219
|
rtc:
|
|
199
220
|
tcp_port: ${LIVEKIT_MEDIA_TCP_PORT}
|
|
@@ -201,9 +222,7 @@ rtc:
|
|
|
201
222
|
port_range_end: 60000
|
|
202
223
|
use_external_ip: false
|
|
203
224
|
node_ip: ${address}
|
|
204
|
-
|
|
205
|
-
${config.livekitApiKey}: ${config.livekitApiSecret}
|
|
206
|
-
`;
|
|
225
|
+
${keysYaml}`;
|
|
207
226
|
}
|
|
208
227
|
async function run(host, command) {
|
|
209
228
|
const result = await host.exec(command);
|
|
@@ -211,6 +230,20 @@ async function run(host, command) {
|
|
|
211
230
|
throw new Error(`"${command}" exited ${result.exitCode}: ${result.stderr || result.stdout}`);
|
|
212
231
|
}
|
|
213
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Writes `contents` to `${path}.tmp` and renames it onto `path` with `mv`,
|
|
235
|
+
* rather than writing `path` directly -- `mv` within the same directory is a
|
|
236
|
+
* single rename syscall, so a reader of `path` (this function's own
|
|
237
|
+
* `cat`-and-compare in `calls-sfu-ssh-adapter.ts`, or LiveKit's own read at
|
|
238
|
+
* container start) only ever sees the old complete file or the new complete
|
|
239
|
+
* one, never a partial write from one apply interleaved with another's
|
|
240
|
+
* (#152 round 2 review, finding 3).
|
|
241
|
+
*/
|
|
242
|
+
async function writeFileAtomically(host, path, contents) {
|
|
243
|
+
const tmpPath = `${path}.tmp`;
|
|
244
|
+
await host.writeFile(tmpPath, contents);
|
|
245
|
+
await run(host, `mv -f ${tmpPath} ${path}`);
|
|
246
|
+
}
|
|
214
247
|
/**
|
|
215
248
|
* Writes all three files, brings LiveKit and `cloudflared` up, then narrows
|
|
216
249
|
* the VM firewall to exactly what #122's and #127's "Decided" sections name
|
|
@@ -240,7 +273,7 @@ export async function applyCallsSfu(host, config) {
|
|
|
240
273
|
requireCallsSfuAddress(config.address);
|
|
241
274
|
requireCloudflaredTunnelToken(config.cloudflaredTunnelToken);
|
|
242
275
|
await host.writeFile(LIVEKIT_COMPOSE_PATH, dockerComposeYaml());
|
|
243
|
-
await host
|
|
276
|
+
await writeFileAtomically(host, LIVEKIT_CONFIG_PATH, livekitConfigYaml(config));
|
|
244
277
|
await host.writeFile(CLOUDFLARED_ENV_PATH, cloudflaredEnvFile(config.cloudflaredTunnelToken));
|
|
245
278
|
await run(host, `docker compose -f ${LIVEKIT_COMPOSE_PATH} up -d`);
|
|
246
279
|
await run(host, 'ufw allow 22/tcp');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
|
-
export { validateBuyerIdentity, validateHosting, validateOwner, validateSite } from './org.js';
|
|
2
|
+
export { DEFAULT_MAIL_APPS, validateBuyerIdentity, validateHosting, validateMail, validateOwner, validateSite, } from './org.js';
|
|
3
3
|
export type { DeclaredPlatform, PlatformService, PlatformServiceKey } from './platform.js';
|
|
4
4
|
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
5
5
|
export type { RequestableField, CallsChange, StorageLimitChange, AiLimitChange, DomainChange, MailTierChange, RequestedChangeValue, ChangeClassification, RequestedChangeState, RequestedChange, } from './requested-changes.js';
|
|
@@ -20,14 +20,18 @@ export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
|
20
20
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
21
21
|
export type { PlatformOrgClientFactories } from './plan/index.js';
|
|
22
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';
|
|
24
|
-
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './plan/index.js';
|
|
23
|
+
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, } from './plan/index.js';
|
|
24
|
+
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
25
25
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
26
|
+
export type { MailIdentityApp, MailIdentityAudienceCheck } from './plan/index.js';
|
|
27
|
+
export { checkMailIdentityAudience } from './plan/index.js';
|
|
28
|
+
export type { MailAllowlistApp } from './plan/index.js';
|
|
29
|
+
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
26
30
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
27
31
|
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './plan/index.js';
|
|
28
32
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
|
|
29
33
|
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,
|
|
34
|
+
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
|
31
35
|
export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './plan/index.js';
|
|
32
36
|
export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './plan/index.js';
|
|
33
37
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './plan/index.js';
|
|
@@ -37,6 +41,8 @@ export type { CallsSfuSshConnectOptions } from './calls-sfu-ssh-client.js';
|
|
|
37
41
|
export { UnknownCallsSfuHostKeyError, toCallsSfuHostPort, applyCallsSfuOverSession, } from './calls-sfu-ssh-adapter.js';
|
|
38
42
|
export type { LivekitCredentials } from './calls-sfu-keys.js';
|
|
39
43
|
export { mintLivekitCredentials } from './calls-sfu-keys.js';
|
|
44
|
+
export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './plan/index.js';
|
|
45
|
+
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './plan/index.js';
|
|
40
46
|
export type { MigrateSql, MigrationResult } from './migrate.js';
|
|
41
47
|
export { MIGRATIONS_DIR, migrate } from './migrate.js';
|
|
42
48
|
export type { BootstrapCompany, BootstrapExecutorDeps, BootstrapOwner, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { validateBuyerIdentity, validateHosting, validateOwner, validateSite } from './org.js';
|
|
1
|
+
export { DEFAULT_MAIL_APPS, validateBuyerIdentity, validateHosting, validateMail, validateOwner, validateSite, } from './org.js';
|
|
2
2
|
export { PLATFORM_SERVICE_KEYS, defaultPlatformHostname, validatePlatform, } from './platform.js';
|
|
3
3
|
export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
|
|
4
4
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
@@ -8,15 +8,18 @@ export { declaredOrgs, foundryAppliedState, foundryAuditEvents, foundryDeclaredP
|
|
|
8
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
9
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
10
10
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
11
|
-
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './plan/index.js';
|
|
11
|
+
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
12
12
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
13
|
+
export { checkMailIdentityAudience } from './plan/index.js';
|
|
14
|
+
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
13
15
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
14
16
|
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,
|
|
17
|
+
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
|
16
18
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './plan/index.js';
|
|
17
19
|
export { connectCallsSfuSsh } from './calls-sfu-ssh-client.js';
|
|
18
20
|
export { UnknownCallsSfuHostKeyError, toCallsSfuHostPort, applyCallsSfuOverSession, } from './calls-sfu-ssh-adapter.js';
|
|
19
21
|
export { mintLivekitCredentials } from './calls-sfu-keys.js';
|
|
22
|
+
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './plan/index.js';
|
|
20
23
|
export { MIGRATIONS_DIR, migrate } from './migrate.js';
|
|
21
24
|
export { bootstrapPlatform } from './bootstrap/index.js';
|
|
22
25
|
export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- @wtfalch/foundry: #162 (decided 2026-09-29) -- the owner's mailbox
|
|
2
|
+
-- address is now declared, not guessed (replacing the hardcoded
|
|
3
|
+
-- `owner@<domain>` PR 163 shipped first), and the org's send-only no-reply
|
|
4
|
+
-- address gets its own configurable local part. Mirrors
|
|
5
|
+
-- `src/store/schema.ts`'s `declaredOrgs` table additions.
|
|
6
|
+
--
|
|
7
|
+
-- owner_mailbox_address is nullable: an org can have mail on with no
|
|
8
|
+
-- address declared yet, and `plan/diff.ts`'s `planMail` reports the
|
|
9
|
+
-- mailbox alone as blocked in that case rather than this column refusing
|
|
10
|
+
-- the row. no_reply_local_part defaults to 'no-reply' so every existing
|
|
11
|
+
-- row keeps today's behaviour unchanged.
|
|
12
|
+
|
|
13
|
+
alter table foundry_declared_orgs
|
|
14
|
+
add column if not exists owner_mailbox_address text,
|
|
15
|
+
add column if not exists no_reply_local_part text not null default 'no-reply';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- @wtfalch/foundry: which ZITADEL apps in the org's audience project may
|
|
2
|
+
-- read a person's mail (#34). Mirrors `src/store/schema.ts`'s `mailApps`.
|
|
3
|
+
-- Defaults to 'agora' and 'webmail' so every existing org keeps its current
|
|
4
|
+
-- behaviour.
|
|
5
|
+
|
|
6
|
+
alter table foundry_declared_orgs
|
|
7
|
+
add column if not exists mail_apps jsonb not null default '["agora","webmail"]'::jsonb;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the role mailboxes an org wants pre-created on its mail
|
|
2
|
+
-- domain (#230). Mirrors `src/store/schema.ts`'s `roleMailboxes`. Defaults to
|
|
3
|
+
-- an empty list so every existing org keeps its current behaviour.
|
|
4
|
+
|
|
5
|
+
alter table foundry_declared_orgs
|
|
6
|
+
add column if not exists role_mailboxes jsonb not null default '[]'::jsonb;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- @wtfalch/foundry: chat is not in v1 (decided 2026-10-01). Mirrors
|
|
2
|
+
-- `src/store/schema.ts`'s `chatEnabled`. Defaults false, so every existing org
|
|
3
|
+
-- reads as chat off and `apply` leaves any live chat resource alone.
|
|
4
|
+
|
|
5
|
+
alter table foundry_declared_orgs
|
|
6
|
+
add column if not exists chat_enabled boolean not null default false;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the bespoke app's own env var names for values the plan
|
|
2
|
+
-- already mints (Files, AI, mail), each a key `pushAppEnv` resolves (#243).
|
|
3
|
+
-- Mirrors `src/store/schema.ts`'s `hostingAppEnv`. Defaults to an empty
|
|
4
|
+
-- object so every existing org keeps its current behaviour: no app env
|
|
5
|
+
-- pushed from this map.
|
|
6
|
+
|
|
7
|
+
alter table foundry_declared_orgs
|
|
8
|
+
add column if not exists hosting_app_env jsonb not null default '{}'::jsonb;
|