@wtfalch/foundry 0.7.1 → 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/versions.d.ts +8 -0
- package/dist/bootstrap/versions.js +31 -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 +5 -4
- package/dist/calls-sfu.js +5 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +4 -1
- package/dist/migrations/0024_audit_events.sql +1 -1
- package/dist/migrations/0025_audit_display.sql +1 -1
- package/dist/migrations/0026_audit_erase_email_guard.sql +1 -1
- package/dist/migrations/0027_audit_chain.sql +1 -1
- package/dist/migrations/0028_audit_rls.sql +2 -2
- package/dist/migrations-dir.d.ts +9 -0
- package/dist/migrations-dir.js +10 -0
- package/dist/plan/calls-matrix.d.ts +2 -0
- package/dist/plan/calls-matrix.js +2 -0
- package/dist/plan/diff.d.ts +3 -1
- package/dist/plan/diff.js +76 -14
- package/dist/plan/executor.d.ts +305 -16
- package/dist/plan/executor.js +606 -9
- package/dist/plan/login-host.d.ts +24 -0
- package/dist/plan/login-host.js +21 -0
- package/dist/plan/plan-apply.d.ts +2 -0
- package/dist/plan/plan-apply.js +20 -1
- package/dist/plan/platform-diff.d.ts +79 -1
- package/dist/plan/platform-diff.js +197 -1
- package/dist/plan/platform-providers.d.ts +36 -1
- package/dist/plan/platform-providers.js +153 -21
- package/dist/plan.d.ts +43 -0
- package/dist/plan.js +16 -1
- package/dist/store/audit.d.ts +13 -17
- package/dist/store/audit.js +14 -18
- package/dist/store/postgres.d.ts +14 -0
- package/dist/store/postgres.js +13 -8
- package/package.json +6 -3
- package/versions.json +1 -1
- 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 },
|
|
@@ -15,3 +15,11 @@ export interface PinnedVersions {
|
|
|
15
15
|
}
|
|
16
16
|
export declare function loadVersions(): PinnedVersions;
|
|
17
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;
|
|
@@ -10,3 +10,34 @@ export function loadVersions() {
|
|
|
10
10
|
return JSON.parse(readFileSync(VERSIONS_PATH, 'utf8'));
|
|
11
11
|
}
|
|
12
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,10 +114,11 @@ 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
|
-
*
|
|
120
|
-
*
|
|
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).
|
|
121
122
|
*/
|
|
122
123
|
export declare const LIVEKIT_IMAGE_REF: string;
|
|
123
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). */
|
package/dist/calls-sfu.js
CHANGED
|
@@ -104,10 +104,11 @@ export const LIVEKIT_CONFIG_PATH = '/opt/livekit/livekit.yaml';
|
|
|
104
104
|
/** Holds `TUNNEL_TOKEN=...` -- the one secret the `cloudflared` compose service reads (`env_file:`, below). */
|
|
105
105
|
export const CLOUDFLARED_ENV_PATH = '/opt/livekit/cloudflared.env';
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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).
|
|
111
112
|
*/
|
|
112
113
|
export const LIVEKIT_IMAGE_REF = `livekit/livekit-server:${VERSIONS.livekit}`;
|
|
113
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). */
|
package/dist/index.d.ts
CHANGED
|
@@ -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
|
@@ -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';
|
|
@@ -164,7 +164,7 @@ create or replace function audit_erase_person(subject text, pseudonym text, subj
|
|
|
164
164
|
returns integer
|
|
165
165
|
language plpgsql
|
|
166
166
|
security definer
|
|
167
|
-
set search_path
|
|
167
|
+
set search_path from current
|
|
168
168
|
as $$
|
|
169
169
|
declare
|
|
170
170
|
touched integer;
|
|
@@ -102,7 +102,7 @@ create or replace function audit_erase_person(subject text, pseudonym text, subj
|
|
|
102
102
|
returns integer
|
|
103
103
|
language plpgsql
|
|
104
104
|
security definer
|
|
105
|
-
set search_path
|
|
105
|
+
set search_path from current
|
|
106
106
|
as $$
|
|
107
107
|
declare
|
|
108
108
|
touched integer;
|
|
@@ -145,7 +145,7 @@ create or replace function audit_seal_erasure(row_id bigint, erasure_hash_value
|
|
|
145
145
|
returns void
|
|
146
146
|
language plpgsql
|
|
147
147
|
security definer
|
|
148
|
-
set search_path
|
|
148
|
+
set search_path from current
|
|
149
149
|
as $$
|
|
150
150
|
begin
|
|
151
151
|
update audit_events
|
|
@@ -74,7 +74,7 @@ returns text
|
|
|
74
74
|
language sql
|
|
75
75
|
stable
|
|
76
76
|
security definer
|
|
77
|
-
set search_path
|
|
77
|
+
set search_path from current
|
|
78
78
|
as $$
|
|
79
79
|
select row_hash from audit_events order by id desc limit 1
|
|
80
80
|
$$;
|
|
@@ -88,7 +88,7 @@ returns table (id bigint, row_hash text, erased_at timestamptz)
|
|
|
88
88
|
language sql
|
|
89
89
|
stable
|
|
90
90
|
security definer
|
|
91
|
-
set search_path
|
|
91
|
+
set search_path from current
|
|
92
92
|
as $$
|
|
93
93
|
select e.id, e.row_hash, e.erased_at
|
|
94
94
|
from audit_events e
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute path of the directory holding this package's numbered `.sql`
|
|
3
|
+
* migrations. The host passes it to `runMigrationSources` as
|
|
4
|
+
* `{ name: 'foundry', dir: migrationsDir }`.
|
|
5
|
+
*
|
|
6
|
+
* Its own subpath (`@wtfalch/foundry/migrations-dir`) and no other imports: a
|
|
7
|
+
* migration image imports this without the rest of the package.
|
|
8
|
+
*/
|
|
9
|
+
export declare const migrationsDir: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
/**
|
|
3
|
+
* Absolute path of the directory holding this package's numbered `.sql`
|
|
4
|
+
* migrations. The host passes it to `runMigrationSources` as
|
|
5
|
+
* `{ name: 'foundry', dir: migrationsDir }`.
|
|
6
|
+
*
|
|
7
|
+
* Its own subpath (`@wtfalch/foundry/migrations-dir`) and no other imports: a
|
|
8
|
+
* migration image imports this without the rest of the package.
|
|
9
|
+
*/
|
|
10
|
+
export const migrationsDir = fileURLToPath(new URL('./migrations/', import.meta.url));
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
export declare const SYNAPSE_IMAGE = "ghcr.io/element-hq/synapse";
|
|
48
48
|
export declare const SYNAPSE_TAG: string;
|
|
49
49
|
export declare const MAS_IMAGE: string;
|
|
50
|
+
/** #251: the version baked into {@link MAS_IMAGE}'s `FROM` line, named on its own so a version-change line can read it without re-parsing the image string. */
|
|
51
|
+
export declare const MAS_TAG: string;
|
|
50
52
|
/**
|
|
51
53
|
* Never resolved by DNS and never federates (`callsSynapseHomeserverYaml`'s
|
|
52
54
|
* `federation_domain_whitelist: []`) -- purely a logical identifier MAS and
|
|
@@ -49,6 +49,8 @@ import { VERSIONS } from '../bootstrap/versions.js';
|
|
|
49
49
|
export const SYNAPSE_IMAGE = 'ghcr.io/element-hq/synapse';
|
|
50
50
|
export const SYNAPSE_TAG = VERSIONS.synapse;
|
|
51
51
|
export const MAS_IMAGE = `ghcr.io/element-hq/matrix-authentication-service:${VERSIONS.mas}`;
|
|
52
|
+
/** #251: the version baked into {@link MAS_IMAGE}'s `FROM` line, named on its own so a version-change line can read it without re-parsing the image string. */
|
|
53
|
+
export const MAS_TAG = VERSIONS.mas;
|
|
52
54
|
/**
|
|
53
55
|
* Never resolved by DNS and never federates (`callsSynapseHomeserverYaml`'s
|
|
54
56
|
* `federation_domain_whitelist: []`) -- purely a logical identifier MAS and
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DeclaredOrg, OrgId } from '../org.js';
|
|
2
|
-
import type { Plan, PlanStep, UnmanagedItem } from '../plan.js';
|
|
2
|
+
import type { Plan, PlanStep, UnmanagedItem, VersionChangeItem } from '../plan.js';
|
|
3
3
|
import { type AgoraHostReading } from './agora.js';
|
|
4
4
|
import type { LiveResource } from './live.js';
|
|
5
5
|
import { type OrgPlatformReadiness } from './platform-org.js';
|
|
@@ -39,6 +39,8 @@ export interface PlanDetail {
|
|
|
39
39
|
orgId: OrgId;
|
|
40
40
|
plannedSteps: PlannedStep[];
|
|
41
41
|
unmanaged: UnmanagedItem[];
|
|
42
|
+
/** #251: see `VersionChangeItem` (`../plan.ts`). Never folded into `plannedSteps`. */
|
|
43
|
+
versionChanges: VersionChangeItem[];
|
|
42
44
|
}
|
|
43
45
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
44
46
|
export declare function toPlan(detail: PlanDetail): Plan;
|
package/dist/plan/diff.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
1
2
|
import { NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraHostMissing, desiredAgoraEnv, } from './agora.js';
|
|
2
|
-
import { LOGIN_BINDING_SCOPE_KEY, loginBindingState, loginHostClientIds, loginHostnameFor, } from './login-host.js';
|
|
3
|
+
import { LOGIN_BINDING_SCOPE_KEY, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
3
4
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
4
|
-
import { APP_ENV_APPLIED_SCOPE_KEY, DB_ENV_APPLIED_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, resolveAppEnv, } from './platform-diff.js';
|
|
5
|
+
import { APP_ENV_APPLIED_SCOPE_KEY, CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, DB_ENV_APPLIED_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, resolveAppEnv, versionNotAppliedLine, } from './platform-diff.js';
|
|
5
6
|
import { DEFAULT_ORG_PLATFORM_READINESS } from './platform-org.js';
|
|
6
7
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
7
8
|
export function toPlan(detail) {
|
|
@@ -9,6 +10,7 @@ export function toPlan(detail) {
|
|
|
9
10
|
orgId: detail.orgId,
|
|
10
11
|
steps: detail.plannedSteps.map((p) => p.step),
|
|
11
12
|
unmanaged: detail.unmanaged,
|
|
13
|
+
versionChanges: detail.versionChanges,
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
function resourceKey(system, kind, key) {
|
|
@@ -115,6 +117,22 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
115
117
|
const callsService = planCallsService(org, byKey, claimed, platformReadiness.callsReady);
|
|
116
118
|
if (callsService.step)
|
|
117
119
|
plannedSteps.push(callsService.step);
|
|
120
|
+
// #251: same mechanism as the platform's own version-change lines -- see
|
|
121
|
+
// `planPlatform`'s own block in `platform-diff.ts`. Synapse and MAS never
|
|
122
|
+
// patch a running app's image tag once created, so this is
|
|
123
|
+
// `versionNotAppliedLine`, the same honest wording zitadel's own line
|
|
124
|
+
// uses, never `versionChangeLine`'s "X -> Y (redeploy)" -- nothing here
|
|
125
|
+
// ever performs that redeploy.
|
|
126
|
+
const versionChanges = [];
|
|
127
|
+
if (org.calls.enabled && !calls.blocked) {
|
|
128
|
+
const callsAppLive = Boolean(byKey.get(resourceKey('coolify', 'calls', org.id)));
|
|
129
|
+
const synapseChange = versionNotAppliedLine('synapse', callsAppLive, recordedIds[CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.synapse);
|
|
130
|
+
if (synapseChange)
|
|
131
|
+
versionChanges.push(synapseChange);
|
|
132
|
+
const masChange = versionNotAppliedLine('mas', callsAppLive, recordedIds[CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.mas);
|
|
133
|
+
if (masChange)
|
|
134
|
+
versionChanges.push(masChange);
|
|
135
|
+
}
|
|
118
136
|
const filesOrganisation = planFilesOrganisation(org, byKey, claimed, platformReadiness.filesReady, platformReadiness.missingKeys.files, platformReadiness.noKeyReader);
|
|
119
137
|
if (filesOrganisation.step)
|
|
120
138
|
plannedSteps.push(filesOrganisation.step);
|
|
@@ -269,7 +287,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
269
287
|
unmanaged.push(...zoneBlocked);
|
|
270
288
|
// #243: an appEnv source nothing will record is reported, not silently skipped.
|
|
271
289
|
unmanaged.push(...app.blocked);
|
|
272
|
-
return { orgId: org.id, plannedSteps, unmanaged };
|
|
290
|
+
return { orgId: org.id, plannedSteps, unmanaged, versionChanges };
|
|
273
291
|
}
|
|
274
292
|
/**
|
|
275
293
|
* The org's own ZITADEL org (issuer), a per-org singleton like every other
|
|
@@ -420,8 +438,12 @@ function planOwner(org, recordedIds, authReady) {
|
|
|
420
438
|
* for *this* apply, so an org moving from tier 1 onto its own domain (or
|
|
421
439
|
* losing one) replans this step rather than staying silent forever once
|
|
422
440
|
* first applied -- `executor.ts`'s `zitadelLoginHost` then also moves every
|
|
423
|
-
* already-live app onto the new base URI
|
|
424
|
-
*
|
|
441
|
+
* already-live app onto the new base URI, and (#274) removes the *old* host
|
|
442
|
+
* from ZITADEL's trusted domains and zitadel-login's binding, reading it
|
|
443
|
+
* back from this same recorded value before overwriting it -- no separate
|
|
444
|
+
* action field or planning step of its own needed for that half. Mints
|
|
445
|
+
* nothing either way: adding a trusted domain is not a one-time secret, so
|
|
446
|
+
* there is no "undelivered"
|
|
425
447
|
* guard to check the way `planJobsRegistration`'s own does.
|
|
426
448
|
*
|
|
427
449
|
* Ordered right after `planOwner` (not after mail, chat, or any hosting
|
|
@@ -437,11 +459,16 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
437
459
|
const none = { step: null, blocked: null };
|
|
438
460
|
const key = resourceKey('zitadel', 'login-host', org.id);
|
|
439
461
|
const host = loginHostnameFor(org);
|
|
440
|
-
// Already applied -- a re-run plans nothing unless the host changed
|
|
441
|
-
// client id appeared since the binding was last written (#267, rule 4)
|
|
462
|
+
// Already applied -- a re-run plans nothing unless the host changed, an app
|
|
463
|
+
// client id appeared since the binding was last written (#267, rule 4), or
|
|
464
|
+
// an earlier host change still has a removal owed (#274 fix round 2):
|
|
465
|
+
// `zitadelLoginHost` always advances the recorded host to the current one,
|
|
466
|
+
// so a pending removal is the only way to know one is still owed, and
|
|
467
|
+
// nothing else here would replan the step to retry it.
|
|
442
468
|
if (recordedIds[key] === host &&
|
|
443
469
|
recordedIds[LOGIN_BINDING_SCOPE_KEY] ===
|
|
444
|
-
loginBindingState(host, loginHostClientIds(recordedIds))
|
|
470
|
+
loginBindingState(host, loginHostClientIds(recordedIds)) &&
|
|
471
|
+
pendingRemovalHosts(recordedIds).length === 0) {
|
|
445
472
|
return none;
|
|
446
473
|
}
|
|
447
474
|
// #177: nothing to add a trusted domain to yet -- `planIssuer` already
|
|
@@ -494,6 +521,8 @@ const MAIL_OWNER_MAILBOX_SCOPE_KEY = 'mail_owner_mailbox';
|
|
|
494
521
|
const MAIL_ROLE_MAILBOXES_SCOPE_KEY = 'mail_role_mailboxes';
|
|
495
522
|
/** Restated from `executor.ts`, the same rule: the app id of the org tunnel's cloudflared connector. */
|
|
496
523
|
const CLOUDFLARE_TUNNEL_CONNECTOR_APP_SCOPE_KEY = 'cloudflare_tunnel_connector_app';
|
|
524
|
+
/** #236: restated from `executor.ts`, the same rule: the domain the org tunnel's DNS was last published under. */
|
|
525
|
+
const CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY = 'cloudflare_tunnel_dns_domain';
|
|
497
526
|
/** Restated from `executor.ts`, the same rule: the mail domain the last apply resolved. */
|
|
498
527
|
const MAIL_DOMAIN_SCOPE_KEY = 'mail_domain';
|
|
499
528
|
/** Restated from `executor.ts`, the same rule: the id of an ai key minted but not delivered. */
|
|
@@ -754,6 +783,26 @@ function planCalls(org, byKey, claimed, callsReady) {
|
|
|
754
783
|
blocked: `${org.slug}'s calls Matrix stack is blocked, not planned: the platform's shared calls SFU is off`,
|
|
755
784
|
};
|
|
756
785
|
}
|
|
786
|
+
// #248: the two images this stack's own apply would run, checked the same
|
|
787
|
+
// way the platform's SFU checks LiveKit/cloudflared and ZITADEL checks its
|
|
788
|
+
// own tag -- a re-apply of the same declaration must not silently run a
|
|
789
|
+
// different version with no diff shown anywhere.
|
|
790
|
+
if (org.calls.enabled && !byKey.get(key)) {
|
|
791
|
+
const synapseBlocked = floatingTagBlocked('Synapse', VERSIONS.synapse, 'v1.161.0');
|
|
792
|
+
if (synapseBlocked) {
|
|
793
|
+
return {
|
|
794
|
+
step: null,
|
|
795
|
+
blocked: `${org.slug}'s calls Matrix stack is blocked, not planned: ${synapseBlocked}`,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const masBlocked = floatingTagBlocked('matrix-authentication-service', VERSIONS.mas, '1.24.0');
|
|
799
|
+
if (masBlocked) {
|
|
800
|
+
return {
|
|
801
|
+
step: null,
|
|
802
|
+
blocked: `${org.slug}'s calls Matrix stack is blocked, not planned: ${masBlocked}`,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
757
806
|
return {
|
|
758
807
|
step: planHostingToggle('coolify', 'calls', org.calls.enabled, org, byKey, claimed, () => `provision ${org.slug}'s calls Matrix stack (Synapse + matrix-authentication-service)`, () => `park ${org.slug}'s calls Matrix stack (stops serving; data is retained, not deleted -- #7 corrections 1)`),
|
|
759
808
|
blocked: null,
|
|
@@ -1549,17 +1598,30 @@ function planTunnel(org, byKey, claimed, recordedIds) {
|
|
|
1549
1598
|
const toggled = planHostingToggle('cloudflare', 'tunnel', desired, org, byKey, claimed, () => `provision a Cloudflare tunnel for ${org.slug}'s app`, () => `park ${org.slug}'s Cloudflare tunnel (configuration is retained, not deleted -- #7 corrections 1)`);
|
|
1550
1599
|
if (toggled || !desired)
|
|
1551
1600
|
return toggled;
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1601
|
+
// A live, enabled tunnel re-plans the same step again when either:
|
|
1602
|
+
// #235, no connector recorded -- a first connector create that failed, or
|
|
1603
|
+
// a tunnel that predates connectors; or #236, its DNS was last published
|
|
1604
|
+
// under a domain other than the one now declared -- never published at
|
|
1605
|
+
// all (unset), or the org's domain has since changed. `org.domain` is set
|
|
1606
|
+
// whenever `desired` is true above, in every branch. The step itself
|
|
1607
|
+
// (`executor.ts`'s `cloudflareTunnel`) finds or creates the connector by
|
|
1608
|
+
// name and upserts DNS idempotently (and, on a domain change, cleans up
|
|
1609
|
+
// the old one once the new one is published -- round 3: publishing first
|
|
1610
|
+
// means a conflict on the new domain leaves the old one still live rather
|
|
1611
|
+
// than taking the org dark), so re-running it never duplicates either.
|
|
1556
1612
|
const live = byKey.get(resourceKey('cloudflare', 'tunnel', org.id));
|
|
1557
|
-
if (!live
|
|
1613
|
+
if (!live)
|
|
1614
|
+
return null;
|
|
1615
|
+
const connectorMissing = !recordedIds[CLOUDFLARE_TUNNEL_CONNECTOR_APP_SCOPE_KEY];
|
|
1616
|
+
const dnsStale = recordedIds[CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY] !== org.domain;
|
|
1617
|
+
if (!connectorMissing && !dnsStale)
|
|
1558
1618
|
return null;
|
|
1559
1619
|
return {
|
|
1560
1620
|
action: { system: 'cloudflare', kind: 'tunnel', key: org.id, desired: { enabled: true } },
|
|
1561
1621
|
step: {
|
|
1562
|
-
description:
|
|
1622
|
+
description: connectorMissing
|
|
1623
|
+
? `run a cloudflared connector for ${org.slug}'s Cloudflare tunnel`
|
|
1624
|
+
: `point ${org.slug}'s hostnames at its Cloudflare tunnel`,
|
|
1563
1625
|
destroys: false,
|
|
1564
1626
|
migrates: false,
|
|
1565
1627
|
},
|