@wtfalch/foundry 0.6.0 → 0.8.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 +12 -8
- package/dist/bootstrap/bootstrap.js +4 -12
- package/dist/bootstrap/declare.d.ts +3 -1
- package/dist/bootstrap/declare.js +6 -2
- package/dist/bootstrap/versions.d.ts +25 -0
- package/dist/bootstrap/versions.js +43 -0
- package/dist/calls-sfu-deploy-support.d.ts +1 -1
- package/dist/calls-sfu-deploy-support.js +1 -1
- package/dist/calls-sfu-ssh-adapter.d.ts +1 -1
- package/dist/calls-sfu-ssh-adapter.js +1 -1
- package/dist/calls-sfu.d.ts +8 -6
- package/dist/calls-sfu.js +9 -6
- package/dist/index.d.ts +3 -4
- package/dist/index.js +5 -2
- 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/migrations-dir.d.ts +9 -0
- package/dist/migrations-dir.js +10 -0
- package/dist/org.d.ts +24 -0
- package/dist/org.js +13 -0
- package/dist/plan/agora.d.ts +21 -0
- package/dist/plan/agora.js +23 -0
- package/dist/plan/calls-matrix.d.ts +4 -2
- package/dist/plan/calls-matrix.js +5 -2
- package/dist/plan/cloudflared-connector.d.ts +3 -2
- package/dist/plan/cloudflared-connector.js +5 -3
- package/dist/plan/diff.d.ts +18 -3
- package/dist/plan/diff.js +369 -42
- package/dist/plan/executor.d.ts +378 -44
- package/dist/plan/executor.js +816 -57
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/login-host.d.ts +88 -0
- package/dist/plan/login-host.js +73 -0
- package/dist/plan/plan-apply.d.ts +23 -8
- package/dist/plan/plan-apply.js +30 -10
- package/dist/plan/platform-diff.d.ts +102 -2
- package/dist/plan/platform-diff.js +232 -1
- package/dist/plan/platform-providers.d.ts +37 -2
- package/dist/plan/platform-providers.js +223 -36
- package/dist/plan/providers-readers.js +44 -1
- package/dist/plan.d.ts +43 -0
- package/dist/plan.js +16 -1
- package/dist/store/audit.d.ts +36 -4
- package/dist/store/audit.js +110 -29
- package/dist/store/postgres.d.ts +14 -0
- package/dist/store/postgres.js +17 -9
- package/dist/store/schema.d.ts +37 -1
- package/dist/store/schema.js +7 -0
- package/package.json +10 -5
- package/versions.json +8 -0
- package/dist/migrate.d.ts +0 -38
- package/dist/migrate.js +0 -47
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { MigrateSql } from '../migrate.js';
|
|
2
1
|
import type { PlatformLiveReaders, PlatformStepExecutor } from '../plan/index.js';
|
|
3
2
|
import type { FoundryPlatformStepExecutorOptions } from '../plan/platform-providers.js';
|
|
4
3
|
import type { FoundryStoreDb } from '../store/postgres.js';
|
|
@@ -27,7 +26,7 @@ export interface RootCredentials {
|
|
|
27
26
|
}
|
|
28
27
|
/** What Archon's onboarding screen shows: one event per phase or round. */
|
|
29
28
|
export interface BootstrapProgress {
|
|
30
|
-
phase: '
|
|
29
|
+
phase: 'declare' | 'plan' | 'apply' | 'done';
|
|
31
30
|
round?: number;
|
|
32
31
|
message: string;
|
|
33
32
|
steps?: string[];
|
|
@@ -35,10 +34,8 @@ export interface BootstrapProgress {
|
|
|
35
34
|
/** Everything foundry's per-step executors take besides the two clients and `deliverSecret`. */
|
|
36
35
|
export type BootstrapExecutorDeps = Omit<FoundryPlatformStepExecutorOptions, 'coolify' | 'deliverSecret'>;
|
|
37
36
|
export interface BootstrapPlatformDeps {
|
|
38
|
-
/** Foundry's tables live here; the store, the audit log and the plan all use it. */
|
|
37
|
+
/** Foundry's tables live here, already migrated by the host; the store, the audit log and the plan all use it. */
|
|
39
38
|
db: FoundryStoreDb;
|
|
40
|
-
/** The same database, for the migration runner. */
|
|
41
|
-
migrateSql: MigrateSql;
|
|
42
39
|
rootCredentials: () => Promise<RootCredentials>;
|
|
43
40
|
deliverSecret: (ref: string, secret: string) => Promise<void>;
|
|
44
41
|
/**
|
|
@@ -52,6 +49,13 @@ export interface BootstrapPlatformDeps {
|
|
|
52
49
|
};
|
|
53
50
|
/** Per-step dependencies (auth, mail, files, ai, integrations, archon, sfu). A step without its own is reported blocked. */
|
|
54
51
|
executor?: BootstrapExecutorDeps;
|
|
52
|
+
/**
|
|
53
|
+
* Passed to `createPostgresAuditLog`. Omit it in a real deployment: `db` is
|
|
54
|
+
* the runtime role the host made with `ensureRuntimeRole`, and the default
|
|
55
|
+
* (on) checks that. Only a fixture without that role (PGlite, whose one user
|
|
56
|
+
* is a superuser) needs `false`.
|
|
57
|
+
*/
|
|
58
|
+
checkRuntimeRole?: boolean;
|
|
55
59
|
onProgress?: (event: BootstrapProgress) => void;
|
|
56
60
|
/** Rounds before the run gives up. Default 20. */
|
|
57
61
|
maxRounds?: number;
|
|
@@ -97,9 +101,9 @@ export declare class Redactor {
|
|
|
97
101
|
/** Wraps the secret-carrying ports inside one step's deps so what they deliver or resolve is redacted. */
|
|
98
102
|
export declare function guardStepDeps(executor: BootstrapExecutorDeps | undefined, redactor: Redactor, deliverSecret: (ref: string, secret: string) => Promise<void>, heldKeys: BootstrapPlatformDeps['heldKeys']): BootstrapExecutorDeps;
|
|
99
103
|
/**
|
|
100
|
-
* The platform's first apply, run from Archon's onboarding flow
|
|
101
|
-
*
|
|
102
|
-
* overwriting a row already there), then plans and applies round by round
|
|
104
|
+
* The platform's first apply, run from Archon's onboarding flow, against a
|
|
105
|
+
* database the host has already migrated: declares Company Included and the
|
|
106
|
+
* platform (never overwriting a row already there), then plans and applies round by round
|
|
103
107
|
* until every step is applied or reported blocked. Reads no env and no file:
|
|
104
108
|
* the database, root credentials, held keys and every per-step dependency
|
|
105
109
|
* come in through `deps`.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CloudflareClient } from '@wtfalch/providers/cloudflare';
|
|
2
2
|
import { CoolifyClient } from '@wtfalch/providers/coolify';
|
|
3
|
-
import { migrate } from '../migrate.js';
|
|
4
3
|
import { FoundryPlanApply, FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, } from '../plan/index.js';
|
|
5
4
|
import { createPostgresAuditLog } from '../store/audit.js';
|
|
6
5
|
import { createPostgresDeclaredOrgStore, createPostgresPlatformStore } from '../store/postgres.js';
|
|
@@ -140,9 +139,9 @@ function archonMasterKeys(guarded, credentials) {
|
|
|
140
139
|
};
|
|
141
140
|
}
|
|
142
141
|
/**
|
|
143
|
-
* The platform's first apply, run from Archon's onboarding flow
|
|
144
|
-
*
|
|
145
|
-
* overwriting a row already there), then plans and applies round by round
|
|
142
|
+
* The platform's first apply, run from Archon's onboarding flow, against a
|
|
143
|
+
* database the host has already migrated: declares Company Included and the
|
|
144
|
+
* platform (never overwriting a row already there), then plans and applies round by round
|
|
146
145
|
* until every step is applied or reported blocked. Reads no env and no file:
|
|
147
146
|
* the database, root credentials, held keys and every per-step dependency
|
|
148
147
|
* come in through `deps`.
|
|
@@ -187,13 +186,6 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
187
186
|
}
|
|
188
187
|
: {}),
|
|
189
188
|
};
|
|
190
|
-
const migrated = await migrate(deps.migrateSql);
|
|
191
|
-
emit({
|
|
192
|
-
phase: 'migrate',
|
|
193
|
-
message: migrated.applied.length === 0
|
|
194
|
-
? 'foundry tables already up to date'
|
|
195
|
-
: `migrated: ${migrated.applied.join(', ')}`,
|
|
196
|
-
});
|
|
197
189
|
const orgStore = createPostgresDeclaredOrgStore(deps.db);
|
|
198
190
|
const platformStore = createPostgresPlatformStore(deps.db);
|
|
199
191
|
if (await orgStore.get(COMPANY_INCLUDED_ORG_ID)) {
|
|
@@ -264,7 +256,7 @@ export async function bootstrapPlatform(input, deps) {
|
|
|
264
256
|
},
|
|
265
257
|
executor: unusedOrgExecutor,
|
|
266
258
|
destroyer: unusedOrgExecutor,
|
|
267
|
-
audit: redactingAudit(createPostgresAuditLog(deps.db), redactor),
|
|
259
|
+
audit: redactingAudit(createPostgresAuditLog(deps.db, { checkRuntimeRole: deps.checkRuntimeRole }), redactor),
|
|
268
260
|
authz: allowPlatformApply,
|
|
269
261
|
actor: { id: 'bootstrap-platform', display: 'bootstrapPlatform' },
|
|
270
262
|
platform: { store: platformStore, readers: runtime.readers, executor },
|
|
@@ -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
|
|
@@ -55,11 +58,12 @@ export function declaredCompanyOrg(company, owner) {
|
|
|
55
58
|
ownerMailboxAddress: null,
|
|
56
59
|
noReplyLocalPart: 'no-reply',
|
|
57
60
|
},
|
|
58
|
-
chat: { usesDomain: false },
|
|
61
|
+
chat: { enabled: false, usesDomain: false },
|
|
59
62
|
calls: { enabled: false, usesDomain: false },
|
|
60
63
|
issuer: { usesDomain: false },
|
|
61
64
|
site: { enabled: false, usesDomain: false },
|
|
62
65
|
hosting: {
|
|
66
|
+
appEnv: {},
|
|
63
67
|
enabled: false,
|
|
64
68
|
usesDomain: false,
|
|
65
69
|
database: false,
|
|
@@ -0,0 +1,25 @@
|
|
|
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;
|
|
18
|
+
/**
|
|
19
|
+
* Why an image tag cannot be used, or `null`. #248: generalised out of what
|
|
20
|
+
* was `platform-providers.ts`'s `mailImageTagBlocked`, the one guard
|
|
21
|
+
* `versions.json` had before -- `livekit/livekit-server:latest` was the live
|
|
22
|
+
* case with none. `label` names the image in the message (`"Stalwart"`,
|
|
23
|
+
* `"ZITADEL"`, `"LiveKit"`, ...); `example` is a real pinned tag to suggest.
|
|
24
|
+
*/
|
|
25
|
+
export declare function floatingTagBlocked(label: string, tag: string, example: string): string | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
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();
|
|
13
|
+
/**
|
|
14
|
+
* Tag spellings that resolve to whatever the registry currently has, not a
|
|
15
|
+
* fixed image -- a re-apply of the same declaration can silently run a
|
|
16
|
+
* different version with no diff shown anywhere (#248). `main`/`master` are
|
|
17
|
+
* default-branch Docker Hub aliases some of these images publish alongside
|
|
18
|
+
* their release tags, not git refs.
|
|
19
|
+
*/
|
|
20
|
+
const FLOATING_TAGS = new Set([
|
|
21
|
+
'latest',
|
|
22
|
+
'main',
|
|
23
|
+
'master',
|
|
24
|
+
'stable',
|
|
25
|
+
'dev',
|
|
26
|
+
'edge',
|
|
27
|
+
'nightly',
|
|
28
|
+
'head',
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Why an image tag cannot be used, or `null`. #248: generalised out of what
|
|
32
|
+
* was `platform-providers.ts`'s `mailImageTagBlocked`, the one guard
|
|
33
|
+
* `versions.json` had before -- `livekit/livekit-server:latest` was the live
|
|
34
|
+
* case with none. `label` names the image in the message (`"Stalwart"`,
|
|
35
|
+
* `"ZITADEL"`, `"LiveKit"`, ...); `example` is a real pinned tag to suggest.
|
|
36
|
+
*/
|
|
37
|
+
export function floatingTagBlocked(label, tag, example) {
|
|
38
|
+
const trimmed = tag.trim();
|
|
39
|
+
if (trimmed === '' || FLOATING_TAGS.has(trimmed.toLowerCase())) {
|
|
40
|
+
return `the ${label} image tag "${tag}" floats; declare a pinned release (for example ${example})`;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* -- the one place both `scripts/calls-sfu-deploy.mjs` (#122) and the
|
|
8
8
|
* platform `sfu` step (`plan/platform-sfu.ts`, #170) use it. The real SSH
|
|
9
9
|
* session -- `ssh2`'s `Client`/`sftp`/`exec` wiring -- stays untested, same
|
|
10
|
-
* as `foundry-db-provision.mjs
|
|
10
|
+
* as `foundry-db-provision.mjs`: real I/O, no branch
|
|
11
11
|
* logic worth a fake session over.
|
|
12
12
|
*
|
|
13
13
|
* Each export here answers one finding from PR #125's review (#122):
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* -- the one place both `scripts/calls-sfu-deploy.mjs` (#122) and the
|
|
8
8
|
* platform `sfu` step (`plan/platform-sfu.ts`, #170) use it. The real SSH
|
|
9
9
|
* session -- `ssh2`'s `Client`/`sftp`/`exec` wiring -- stays untested, same
|
|
10
|
-
* as `foundry-db-provision.mjs
|
|
10
|
+
* as `foundry-db-provision.mjs`: real I/O, no branch
|
|
11
11
|
* logic worth a fake session over.
|
|
12
12
|
*
|
|
13
13
|
* Each export here answers one finding from PR #125's review (#122):
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* host-key refusal and the 0600 file mode without ever opening a socket.
|
|
12
12
|
* `calls-sfu-ssh-client.ts`'s `connectCallsSfuSsh` is the one real
|
|
13
13
|
* implementation, over `ssh2`, and stays untested for the same reason
|
|
14
|
-
* `foundry-db-provision.mjs
|
|
14
|
+
* `foundry-db-provision.mjs` does: real I/O, no
|
|
15
15
|
* branch logic worth a fake session over.
|
|
16
16
|
*/
|
|
17
17
|
import { type CallsSfuConfig, type CallsSfuHostPort } from './calls-sfu.js';
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* host-key refusal and the 0600 file mode without ever opening a socket.
|
|
12
12
|
* `calls-sfu-ssh-client.ts`'s `connectCallsSfuSsh` is the one real
|
|
13
13
|
* implementation, over `ssh2`, and stays untested for the same reason
|
|
14
|
-
* `foundry-db-provision.mjs
|
|
14
|
+
* `foundry-db-provision.mjs` does: real I/O, no
|
|
15
15
|
* branch logic worth a fake session over.
|
|
16
16
|
*/
|
|
17
17
|
import { dirname } from 'node:path';
|
package/dist/calls-sfu.d.ts
CHANGED
|
@@ -114,13 +114,15 @@ export declare const LIVEKIT_CONFIG_PATH = "/opt/livekit/livekit.yaml";
|
|
|
114
114
|
/** Holds `TUNNEL_TOKEN=...` -- the one secret the `cloudflared` compose service reads (`env_file:`, below). */
|
|
115
115
|
export declare const CLOUDFLARED_ENV_PATH = "/opt/livekit/cloudflared.env";
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
117
|
+
* Pinned to a release tag (#248) -- `versions.json`'s `livekit` entry ran
|
|
118
|
+
* `:latest` until then, the one live case `floatingTagBlocked`
|
|
119
|
+
* (`bootstrap/versions.ts`) exists for: a re-apply of the same declaration
|
|
120
|
+
* silently running a different version, with no diff shown anywhere.
|
|
121
|
+
* Pinned in `versions.json` (#247).
|
|
120
122
|
*/
|
|
121
|
-
export declare const LIVEKIT_IMAGE_REF
|
|
122
|
-
/** 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). */
|
|
123
|
-
export declare const CLOUDFLARED_IMAGE_REF
|
|
123
|
+
export declare const LIVEKIT_IMAGE_REF: string;
|
|
124
|
+
/** 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). */
|
|
125
|
+
export declare const CLOUDFLARED_IMAGE_REF: string;
|
|
124
126
|
/** LiveKit signalling -- reachable only through the tunnel as of #127 (see {@link CALLS_SFU_PRIVATE_CIDR}). */
|
|
125
127
|
export declare const LIVEKIT_SIGNALLING_PORT = 7880;
|
|
126
128
|
/** Media fallback for networks that block UDP -- reaches the SFU's own address directly. */
|
package/dist/calls-sfu.js
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
* `calls-sfu-deploy-support.ts`'s `fileWriteMode`), never logged, scrubbed
|
|
53
53
|
* from any error `calls-sfu-deploy.mjs` prints.
|
|
54
54
|
*/
|
|
55
|
+
import { VERSIONS } from './bootstrap/versions.js';
|
|
55
56
|
// ---------------------------------------------------------------------------
|
|
56
57
|
// The guard
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
@@ -103,13 +104,15 @@ export const LIVEKIT_CONFIG_PATH = '/opt/livekit/livekit.yaml';
|
|
|
103
104
|
/** Holds `TUNNEL_TOKEN=...` -- the one secret the `cloudflared` compose service reads (`env_file:`, below). */
|
|
104
105
|
export const CLOUDFLARED_ENV_PATH = '/opt/livekit/cloudflared.env';
|
|
105
106
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
107
|
+
* Pinned to a release tag (#248) -- `versions.json`'s `livekit` entry ran
|
|
108
|
+
* `:latest` until then, the one live case `floatingTagBlocked`
|
|
109
|
+
* (`bootstrap/versions.ts`) exists for: a re-apply of the same declaration
|
|
110
|
+
* silently running a different version, with no diff shown anywhere.
|
|
111
|
+
* Pinned in `versions.json` (#247).
|
|
109
112
|
*/
|
|
110
|
-
export const LIVEKIT_IMAGE_REF =
|
|
111
|
-
/** 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). */
|
|
112
|
-
export const CLOUDFLARED_IMAGE_REF =
|
|
113
|
+
export const LIVEKIT_IMAGE_REF = `livekit/livekit-server:${VERSIONS.livekit}`;
|
|
114
|
+
/** 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). */
|
|
115
|
+
export const CLOUDFLARED_IMAGE_REF = `cloudflare/cloudflared:${VERSIONS.cloudflared}`;
|
|
113
116
|
/** LiveKit signalling -- reachable only through the tunnel as of #127 (see {@link CALLS_SFU_PRIVATE_CIDR}). */
|
|
114
117
|
export const LIVEKIT_SIGNALLING_PORT = 7880;
|
|
115
118
|
/** Media fallback for networks that block UDP -- reaches the SFU's own address directly. */
|
package/dist/index.d.ts
CHANGED
|
@@ -20,8 +20,8 @@ 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
26
|
export type { MailIdentityApp, MailIdentityAudienceCheck } from './plan/index.js';
|
|
27
27
|
export { checkMailIdentityAudience } from './plan/index.js';
|
|
@@ -43,8 +43,7 @@ export type { LivekitCredentials } from './calls-sfu-keys.js';
|
|
|
43
43
|
export { mintLivekitCredentials } from './calls-sfu-keys.js';
|
|
44
44
|
export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './plan/index.js';
|
|
45
45
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './plan/index.js';
|
|
46
|
-
export
|
|
47
|
-
export { MIGRATIONS_DIR, migrate } from './migrate.js';
|
|
46
|
+
export { migrationsDir as MIGRATIONS_DIR } from './migrations-dir.js';
|
|
48
47
|
export type { BootstrapCompany, BootstrapExecutorDeps, BootstrapOwner, BootstrapPlatformDeps, BootstrapPlatformInput, BootstrapProgress, BootstrapResult, RootCredentials, } from './bootstrap/index.js';
|
|
49
48
|
export { bootstrapPlatform } from './bootstrap/index.js';
|
|
50
49
|
export type { RunnerDb } from './runner/db.js';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ 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
13
|
export { checkMailIdentityAudience } from './plan/index.js';
|
|
14
14
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
@@ -20,7 +20,10 @@ export { connectCallsSfuSsh } from './calls-sfu-ssh-client.js';
|
|
|
20
20
|
export { UnknownCallsSfuHostKeyError, toCallsSfuHostPort, applyCallsSfuOverSession, } from './calls-sfu-ssh-adapter.js';
|
|
21
21
|
export { mintLivekitCredentials } from './calls-sfu-keys.js';
|
|
22
22
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './plan/index.js';
|
|
23
|
-
|
|
23
|
+
// The shipped migrations' directory; hosts apply them with `@wtfalch/db`'s
|
|
24
|
+
// `runMigrationSources`. Also at `@wtfalch/foundry/migrations-dir`, which
|
|
25
|
+
// imports nothing else.
|
|
26
|
+
export { migrationsDir as MIGRATIONS_DIR } from './migrations-dir.js';
|
|
24
27
|
export { bootstrapPlatform } from './bootstrap/index.js';
|
|
25
28
|
export { AlreadyRunningError, RunNotFoundError, StepFailedError, StepMismatchError, } from './runner/errors.js';
|
|
26
29
|
export { createJobRunner } from './runner/runner.js';
|
|
@@ -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;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
-- Cutover from foundry_audit_events (0012_audit.sql): it stops receiving writes
|
|
2
|
+
-- from this migration on, and its old rows are left in place.
|
|
3
|
+
-- Copied verbatim from @wtfalch/audit 0.6.0's own migrations/ (#232), the
|
|
4
|
+
-- same way that package's own audit-migrations CLI copies them into a host's
|
|
5
|
+
-- drizzle/ directory -- foundry has no drizzle/ journal, so this file is
|
|
6
|
+
-- that copy, hand-placed at the next free number instead. Never edit the
|
|
7
|
+
-- copied SQL below; a fix ships as a new numbered file, same as upstream.
|
|
8
|
+
--
|
|
9
|
+
-- @wtfalch/audit: the append-only audit ledger. Mirrors src/tables.ts.
|
|
10
|
+
--
|
|
11
|
+
-- Every statement is idempotent. Applied by the host's own migrate script
|
|
12
|
+
-- after the host copies this file into its drizzle/ directory as the next
|
|
13
|
+
-- number (audit-migrations); never edited there.
|
|
14
|
+
--
|
|
15
|
+
-- What this file enforces is the ledger's SHAPE and its WALLS: bounded
|
|
16
|
+
-- columns, JSON under 64 KB, the pairs that must appear together, the
|
|
17
|
+
-- break-glass rule that a session row carries its session, reason and
|
|
18
|
+
-- reference, and the three triggers plus the runtime-role revoke that make
|
|
19
|
+
-- "append-only" true for a role that is not a superuser. The CLOSED SETS
|
|
20
|
+
-- (which event names, actor classes, contexts, outcomes and reason codes a
|
|
21
|
+
-- host admits) are the host's own vocabulary: enforced in TypeScript by the
|
|
22
|
+
-- ledger at write time, and in the database by the host's own CHECKs when it
|
|
23
|
+
-- has them (wtfalch/app-template's 0003 and 0007 do). A host with only this
|
|
24
|
+
-- file gets the shape and the walls.
|
|
25
|
+
--
|
|
26
|
+
-- Estate-shaped: the DO block assumes a runtime role named <database>_rt that
|
|
27
|
+
-- owns nothing and serves the app. Without it, the tables and the function
|
|
28
|
+
-- exist and no revoke takes effect.
|
|
29
|
+
--
|
|
30
|
+
-- No foreign keys, on purpose: the trail outlives the tenant, the person and
|
|
31
|
+
-- the credential it describes. `tenant_id` is a uuid because the estate's
|
|
32
|
+
-- tenants are; a host with other ids alters the column in its own migration.
|
|
33
|
+
|
|
34
|
+
create table if not exists audit_events (
|
|
35
|
+
id bigint generated always as identity primary key,
|
|
36
|
+
occurred_at timestamptz not null default now(),
|
|
37
|
+
tenant_id uuid,
|
|
38
|
+
actor_class text not null,
|
|
39
|
+
actor_id text not null,
|
|
40
|
+
actor_display text not null,
|
|
41
|
+
action text not null,
|
|
42
|
+
target_type text not null,
|
|
43
|
+
target_id text not null,
|
|
44
|
+
outcome text not null,
|
|
45
|
+
context text not null,
|
|
46
|
+
session_id text,
|
|
47
|
+
reason text,
|
|
48
|
+
reference text,
|
|
49
|
+
request_id text,
|
|
50
|
+
ip text,
|
|
51
|
+
user_agent text,
|
|
52
|
+
tenant_visible boolean not null,
|
|
53
|
+
before jsonb,
|
|
54
|
+
after jsonb,
|
|
55
|
+
-- Set by audit_erase_person. The row and actor_id survive; who that was
|
|
56
|
+
-- does not, once this is set.
|
|
57
|
+
erased_at timestamptz,
|
|
58
|
+
schema_version smallint not null default 1,
|
|
59
|
+
-- The principal the event was ABOUT, when that is not the actor: an
|
|
60
|
+
-- invitation's invitee, a membership's holder. What lets "events affecting
|
|
61
|
+
-- me" be answered without guessing from payloads.
|
|
62
|
+
subject_class text,
|
|
63
|
+
subject_id text,
|
|
64
|
+
constraint audit_events_actor_class_check check (length(actor_class) between 1 and 64),
|
|
65
|
+
constraint audit_events_actor_id_check check (length(actor_id) between 1 and 256),
|
|
66
|
+
constraint audit_events_actor_display_check check (length(actor_display) between 1 and 256),
|
|
67
|
+
constraint audit_events_action_shape_check check (action ~ '^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$'),
|
|
68
|
+
constraint audit_events_target_type_check check (length(target_type) between 1 and 64),
|
|
69
|
+
constraint audit_events_target_id_check check (length(target_id) between 1 and 256),
|
|
70
|
+
constraint audit_events_outcome_check check (length(outcome) between 1 and 32),
|
|
71
|
+
constraint audit_events_context_check check (length(context) between 1 and 32),
|
|
72
|
+
constraint audit_events_session_id_check check (session_id is null or length(session_id) <= 256),
|
|
73
|
+
constraint audit_events_reason_check check (reason is null or length(reason) <= 512),
|
|
74
|
+
constraint audit_events_reference_check check (reference is null or length(reference) <= 512),
|
|
75
|
+
constraint audit_events_request_id_check check (request_id is null or length(request_id) <= 128),
|
|
76
|
+
constraint audit_events_ip_check check (ip is null or length(ip) <= 64),
|
|
77
|
+
constraint audit_events_user_agent_check check (user_agent is null or length(user_agent) <= 1024),
|
|
78
|
+
constraint audit_events_before_check check (before is null or octet_length(before::text) <= 65536),
|
|
79
|
+
constraint audit_events_after_check check (after is null or octet_length(after::text) <= 65536),
|
|
80
|
+
-- A support-session row names its session, its reason and its reference.
|
|
81
|
+
-- Which reasons are admissible is the host's closed set.
|
|
82
|
+
constraint audit_events_break_glass_shape_check check (
|
|
83
|
+
context <> 'break_glass'
|
|
84
|
+
or (session_id is not null and reason is not null and reference is not null)
|
|
85
|
+
),
|
|
86
|
+
constraint audit_events_subject_pair_check check ((subject_id is null) = (subject_class is null)),
|
|
87
|
+
constraint audit_events_subject_class_check check (subject_class is null or length(subject_class) between 1 and 64),
|
|
88
|
+
constraint audit_events_subject_id_check check (subject_id is null or length(subject_id) between 1 and 256)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
-- (occurred_at, id) is the ordering; every reader pages on it.
|
|
92
|
+
create index if not exists audit_events_tenant_time_idx
|
|
93
|
+
on audit_events (tenant_id, occurred_at desc, id desc);
|
|
94
|
+
create index if not exists audit_events_actor_time_idx
|
|
95
|
+
on audit_events (actor_id, occurred_at desc);
|
|
96
|
+
create index if not exists audit_events_action_time_idx
|
|
97
|
+
on audit_events (action, occurred_at desc);
|
|
98
|
+
create index if not exists audit_events_subject_time_idx
|
|
99
|
+
on audit_events (subject_class, subject_id, occurred_at desc, id desc) where subject_id is not null;
|
|
100
|
+
create index if not exists audit_events_request_idx
|
|
101
|
+
on audit_events (request_id, occurred_at desc) where request_id is not null;
|
|
102
|
+
|
|
103
|
+
-- The second wall. The first is the revoke below, which stops the server; this
|
|
104
|
+
-- stops the owner's own mistakes at a psql prompt. An UPDATE may change only
|
|
105
|
+
-- the four columns erasure touches, and DELETE and TRUNCATE are refused
|
|
106
|
+
-- outright. audit_erase_person passes through this trigger, not around it.
|
|
107
|
+
create or replace function audit_events_guard() returns trigger
|
|
108
|
+
language plpgsql as $$
|
|
109
|
+
begin
|
|
110
|
+
if tg_op = 'DELETE' then
|
|
111
|
+
raise exception 'audit_events is append-only: delete refused';
|
|
112
|
+
elsif tg_op = 'TRUNCATE' then
|
|
113
|
+
raise exception 'audit_events is append-only: truncate refused';
|
|
114
|
+
elsif tg_op = 'UPDATE' then
|
|
115
|
+
if new.id is distinct from old.id
|
|
116
|
+
or new.occurred_at is distinct from old.occurred_at
|
|
117
|
+
or new.tenant_id is distinct from old.tenant_id
|
|
118
|
+
or new.actor_class is distinct from old.actor_class
|
|
119
|
+
or new.actor_id is distinct from old.actor_id
|
|
120
|
+
or new.action is distinct from old.action
|
|
121
|
+
or new.target_type is distinct from old.target_type
|
|
122
|
+
or new.target_id is distinct from old.target_id
|
|
123
|
+
or new.outcome is distinct from old.outcome
|
|
124
|
+
or new.context is distinct from old.context
|
|
125
|
+
or new.session_id is distinct from old.session_id
|
|
126
|
+
or new.reason is distinct from old.reason
|
|
127
|
+
or new.reference is distinct from old.reference
|
|
128
|
+
or new.request_id is distinct from old.request_id
|
|
129
|
+
or new.ip is distinct from old.ip
|
|
130
|
+
or new.user_agent is distinct from old.user_agent
|
|
131
|
+
or new.tenant_visible is distinct from old.tenant_visible
|
|
132
|
+
or new.schema_version is distinct from old.schema_version
|
|
133
|
+
or new.subject_class is distinct from old.subject_class
|
|
134
|
+
or new.subject_id is distinct from old.subject_id
|
|
135
|
+
then
|
|
136
|
+
raise exception 'audit_events is append-only: only actor_display, before, after and erased_at may change';
|
|
137
|
+
end if;
|
|
138
|
+
end if;
|
|
139
|
+
return new;
|
|
140
|
+
end
|
|
141
|
+
$$;
|
|
142
|
+
drop trigger if exists audit_events_guarded_update on audit_events;
|
|
143
|
+
create trigger audit_events_guarded_update
|
|
144
|
+
before update on audit_events
|
|
145
|
+
for each row execute function audit_events_guard();
|
|
146
|
+
drop trigger if exists audit_events_no_delete on audit_events;
|
|
147
|
+
create trigger audit_events_no_delete
|
|
148
|
+
before delete on audit_events
|
|
149
|
+
for each row execute function audit_events_guard();
|
|
150
|
+
drop trigger if exists audit_events_no_truncate on audit_events;
|
|
151
|
+
create trigger audit_events_no_truncate
|
|
152
|
+
before truncate on audit_events
|
|
153
|
+
for each statement execute function audit_events_guard();
|
|
154
|
+
|
|
155
|
+
-- The one sanctioned write. Rows the person wrote (actor_id), rows written
|
|
156
|
+
-- about them (subject_id), and rows whose payload carries their address:
|
|
157
|
+
-- actor_display becomes the pseudonym on their own rows, before and after
|
|
158
|
+
-- are replaced wholesale on every matched row, erased_at is set. actor_id
|
|
159
|
+
-- and subject_id stay, so "somebody with this id did this" survives and
|
|
160
|
+
-- "who that was" does not. Row counts never change. Ledger-only: a host's
|
|
161
|
+
-- own tables (a profile, an invitation) are the host's to clean, in the same
|
|
162
|
+
-- transaction, and its final `person.erased` row is written after this.
|
|
163
|
+
create or replace function audit_erase_person(subject text, pseudonym text, subject_email text)
|
|
164
|
+
returns integer
|
|
165
|
+
language plpgsql
|
|
166
|
+
security definer
|
|
167
|
+
set search_path from current
|
|
168
|
+
as $$
|
|
169
|
+
declare
|
|
170
|
+
touched integer;
|
|
171
|
+
begin
|
|
172
|
+
if subject is null or length(subject) = 0 then
|
|
173
|
+
raise exception 'audit_erase_person: a subject id is required';
|
|
174
|
+
end if;
|
|
175
|
+
if pseudonym is null or length(pseudonym) = 0 or length(pseudonym) > 256 then
|
|
176
|
+
raise exception 'audit_erase_person: a pseudonym of 1 to 256 characters is required';
|
|
177
|
+
end if;
|
|
178
|
+
update audit_events
|
|
179
|
+
set actor_display = case when actor_id = subject then pseudonym else actor_display end,
|
|
180
|
+
before = case when before is null then null else '{"erased":true}'::jsonb end,
|
|
181
|
+
after = case when after is null then null else '{"erased":true}'::jsonb end,
|
|
182
|
+
erased_at = now()
|
|
183
|
+
where erased_at is null
|
|
184
|
+
and (
|
|
185
|
+
actor_id = subject
|
|
186
|
+
or subject_id = subject
|
|
187
|
+
or (
|
|
188
|
+
subject_email is not null
|
|
189
|
+
and length(subject_email) > 0
|
|
190
|
+
and (
|
|
191
|
+
lower(coalesce(before::text, '')) like '%' || lower(subject_email) || '%'
|
|
192
|
+
or lower(coalesce(after::text, '')) like '%' || lower(subject_email) || '%'
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
get diagnostics touched = row_count;
|
|
197
|
+
return touched;
|
|
198
|
+
end
|
|
199
|
+
$$;
|
|
200
|
+
revoke all on function audit_erase_person(text, text, text) from public;
|
|
201
|
+
|
|
202
|
+
do $$
|
|
203
|
+
declare
|
|
204
|
+
rt text := current_database() || '_rt';
|
|
205
|
+
begin
|
|
206
|
+
if exists (select 1 from pg_roles where rolname = rt) then
|
|
207
|
+
execute format('revoke update, delete, truncate on audit_events from %I', rt);
|
|
208
|
+
execute format('grant execute on function audit_erase_person(text, text, text) to %I', rt);
|
|
209
|
+
end if;
|
|
210
|
+
end
|
|
211
|
+
$$;
|