@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
package/dist/plan/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCop
|
|
|
24
24
|
export type { ScopeResolver, RecordedIds } from './providers-readers.js';
|
|
25
25
|
export { ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
26
26
|
export type { AgoraHostPort, AgoraHostValues } from './agora.js';
|
|
27
|
+
export type { LoginHostBinding, LoginHostBindingsPort } from './login-host.js';
|
|
28
|
+
export { loginHostnameFor } from './login-host.js';
|
|
27
29
|
export type { MailIdentityApp, MailIdentityAudienceCheck } from './mail-identity.js';
|
|
28
30
|
export { checkMailIdentityAudience } from './mail-identity.js';
|
|
29
31
|
export type { MailAllowlistApp } from './mail-identity-allowlist.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
|
12
12
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
13
13
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
14
14
|
export { ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
15
|
+
export { loginHostnameFor } from './login-host.js';
|
|
15
16
|
export { checkMailIdentityAudience } from './mail-identity.js';
|
|
16
17
|
export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
|
|
17
18
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
|
+
/**
|
|
3
|
+
* #267: what both the planner (`diff.ts`) and the executor (`executor.ts`)
|
|
4
|
+
* need to agree on for an org's own company login host -- the hostname and
|
|
5
|
+
* the shape of zitadel-login's own routing entry -- the same split
|
|
6
|
+
* `agora.ts` already makes for Agora's own hostname and env.
|
|
7
|
+
*
|
|
8
|
+
* Decided in https://github.com/wtfalch/auth/issues/137 (William,
|
|
9
|
+
* 2026-10-01): foundry owns company sign-in setup through
|
|
10
|
+
* `@wtfalch/providers`, not `auth`'s own `scripts/namespace-provisioner.mjs`.
|
|
11
|
+
* `auth/docs/company-login-hosts.md` is the enforcement side this hostname
|
|
12
|
+
* and binding feed.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* `auth.<domain>` once the org has its own domain and `issuer.usesDomain` is on, `<slug>-auth.oikos.host`
|
|
16
|
+
* otherwise -- the same per-tier shape `agora.ts`'s own `agoraHostnameFor`
|
|
17
|
+
* uses for Agora, and the two tiers #267's issue names directly
|
|
18
|
+
* ("auth.<domain>" on the org's own domain, "<slug>-auth.oikos.host" for
|
|
19
|
+
* tier 1, riding the shared `*.oikos.host` wildcard tunnel the same way
|
|
20
|
+
* `<slug>-agora.oikos.host` does). `../slug.ts`'s
|
|
21
|
+
* `RESERVED_AUTH_AND_ENVIRONMENTS` already reserves the `auth` *slug* so no
|
|
22
|
+
* org's own `<slug>.oikos.host` can collide with this subdomain, the same
|
|
23
|
+
* way `AGORA_SUFFIX` reserves `-agora`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loginHostnameFor(org: DeclaredOrg): string;
|
|
26
|
+
/**
|
|
27
|
+
* One entry in zitadel-login's own `LOGIN_HOST_BINDINGS`
|
|
28
|
+
* (`auth/docs/company-login-hosts.md`) -- a single-line JSON document naming
|
|
29
|
+
* every company-bound host, its ZITADEL organization and the client ids
|
|
30
|
+
* allowed to use it. `host` is {@link loginHostnameFor}'s own value;
|
|
31
|
+
* `organizationId` is the org's ZITADEL org id (`executor.ts`'s
|
|
32
|
+
* `zitadelIssuer` recorded it as `zitadel_org`).
|
|
33
|
+
*/
|
|
34
|
+
export interface LoginHostBinding {
|
|
35
|
+
host: string;
|
|
36
|
+
organizationId: string;
|
|
37
|
+
clientIds: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Host-supplied write access to zitadel-login's own `LOGIN_HOST_BINDINGS`
|
|
41
|
+
* env (`auth/docs/company-login-hosts.md`'s own "Set `LOGIN_HOST_BINDINGS`
|
|
42
|
+
* on zitadel-login", "today set by hand"). Optional on
|
|
43
|
+
* {@link FoundryStepExecutorPorts} the same way `jobsScheduler`/`mailCopy`
|
|
44
|
+
* are, but without it the login-host step is blocked, not partly applied:
|
|
45
|
+
* isolation between orgs rests on the binding, and an app repointed at a host
|
|
46
|
+
* with no binding cannot sign in. Wiring the port means the host routes to
|
|
47
|
+
* zitadel-login (DNS and ingress), because the first apply after wiring
|
|
48
|
+
* repoints the org's bound apps.
|
|
49
|
+
*/
|
|
50
|
+
export interface LoginHostBindingsPort {
|
|
51
|
+
upsertBinding(binding: LoginHostBinding): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* #274: removes a host's entry from zitadel-login's own `LOGIN_HOST_BINDINGS`
|
|
54
|
+
* -- the old host's binding, once a host change or an org destroy has
|
|
55
|
+
* moved or removed what it pointed at. Treat "not found" as done, the same
|
|
56
|
+
* rule `executor.ts`'s `isNotFoundError` gives every other removal in this
|
|
57
|
+
* package.
|
|
58
|
+
*/
|
|
59
|
+
deleteBinding(host: string): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Scope id holding what the binding was last written with
|
|
63
|
+
* ({@link loginBindingState}). Client ids are not secrets. The planner
|
|
64
|
+
* replans the login-host step when the org's recorded client ids no longer
|
|
65
|
+
* match it, so an app created after the first apply is added to the binding
|
|
66
|
+
* on the next run.
|
|
67
|
+
*/
|
|
68
|
+
export declare const LOGIN_BINDING_SCOPE_KEY = "login_host_binding";
|
|
69
|
+
/** The org's recorded app client ids (hosted app, cms, agora), sorted. */
|
|
70
|
+
export declare function loginHostClientIds(recorded: Readonly<Record<string, string>>): string[];
|
|
71
|
+
/** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
|
|
72
|
+
export declare function loginBindingState(host: string, clientIds: readonly string[]): string;
|
|
73
|
+
/**
|
|
74
|
+
* #274 fix round 2: hosts a company-login-host change (or an org destroy)
|
|
75
|
+
* tried to remove and could not yet -- some live app's own ZITADEL Login V2
|
|
76
|
+
* base URI still pointed at one of them, so removing it would have broken
|
|
77
|
+
* that app's sign-in. `zitadelLoginHost` always advances
|
|
78
|
+
* `zitadel:login-host:<key>` to the org's *current* desired host on every
|
|
79
|
+
* successful run (never leaving it on an old one), so this is the only
|
|
80
|
+
* record of a removal still owed -- lost otherwise the moment a second host
|
|
81
|
+
* change moves `previousHost` on. Comma-joined, sorted, the same shape
|
|
82
|
+
* `MAIL_ROLE_MAILBOXES_SCOPE_KEY` (`executor.ts`) holds a list in.
|
|
83
|
+
*/
|
|
84
|
+
export declare const LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY = "zitadel_login_host_pending_removal";
|
|
85
|
+
/** The hosts {@link LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY} currently names, parsed back out. */
|
|
86
|
+
export declare function pendingRemovalHosts(recorded: Readonly<Record<string, string>>): string[];
|
|
87
|
+
/** {@link pendingRemovalHosts}' own serialisation, sorted so the recorded value is stable regardless of check order. */
|
|
88
|
+
export declare function pendingRemovalState(hosts: readonly string[]): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, } from './agora.js';
|
|
2
|
+
/**
|
|
3
|
+
* #267: what both the planner (`diff.ts`) and the executor (`executor.ts`)
|
|
4
|
+
* need to agree on for an org's own company login host -- the hostname and
|
|
5
|
+
* the shape of zitadel-login's own routing entry -- the same split
|
|
6
|
+
* `agora.ts` already makes for Agora's own hostname and env.
|
|
7
|
+
*
|
|
8
|
+
* Decided in https://github.com/wtfalch/auth/issues/137 (William,
|
|
9
|
+
* 2026-10-01): foundry owns company sign-in setup through
|
|
10
|
+
* `@wtfalch/providers`, not `auth`'s own `scripts/namespace-provisioner.mjs`.
|
|
11
|
+
* `auth/docs/company-login-hosts.md` is the enforcement side this hostname
|
|
12
|
+
* and binding feed.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* `auth.<domain>` once the org has its own domain and `issuer.usesDomain` is on, `<slug>-auth.oikos.host`
|
|
16
|
+
* otherwise -- the same per-tier shape `agora.ts`'s own `agoraHostnameFor`
|
|
17
|
+
* uses for Agora, and the two tiers #267's issue names directly
|
|
18
|
+
* ("auth.<domain>" on the org's own domain, "<slug>-auth.oikos.host" for
|
|
19
|
+
* tier 1, riding the shared `*.oikos.host` wildcard tunnel the same way
|
|
20
|
+
* `<slug>-agora.oikos.host` does). `../slug.ts`'s
|
|
21
|
+
* `RESERVED_AUTH_AND_ENVIRONMENTS` already reserves the `auth` *slug* so no
|
|
22
|
+
* org's own `<slug>.oikos.host` can collide with this subdomain, the same
|
|
23
|
+
* way `AGORA_SUFFIX` reserves `-agora`.
|
|
24
|
+
*/
|
|
25
|
+
export function loginHostnameFor(org) {
|
|
26
|
+
// `issuer.usesDomain` is the issuer's opt-in into the org's domain
|
|
27
|
+
// (`org.ts`'s `IssuerConfig`): a domain alone never moves it, so a domain
|
|
28
|
+
// with `usesDomain` false stays on the tier 1 host.
|
|
29
|
+
return org.domain && org.issuer.usesDomain ? `auth.${org.domain}` : `${org.slug}-auth.oikos.host`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Scope id holding what the binding was last written with
|
|
33
|
+
* ({@link loginBindingState}). Client ids are not secrets. The planner
|
|
34
|
+
* replans the login-host step when the org's recorded client ids no longer
|
|
35
|
+
* match it, so an app created after the first apply is added to the binding
|
|
36
|
+
* on the next run.
|
|
37
|
+
*/
|
|
38
|
+
export const LOGIN_BINDING_SCOPE_KEY = 'login_host_binding';
|
|
39
|
+
/** The org's recorded app client ids (hosted app, cms, agora), sorted. */
|
|
40
|
+
export function loginHostClientIds(recorded) {
|
|
41
|
+
return [
|
|
42
|
+
recorded[APP_CLIENT_ID_SCOPE_KEY],
|
|
43
|
+
recorded[CMS_CLIENT_ID_SCOPE_KEY],
|
|
44
|
+
recorded[AGORA_CLIENT_ID_SCOPE_KEY],
|
|
45
|
+
]
|
|
46
|
+
.filter((id) => Boolean(id))
|
|
47
|
+
.sort();
|
|
48
|
+
}
|
|
49
|
+
/** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
|
|
50
|
+
export function loginBindingState(host, clientIds) {
|
|
51
|
+
return `${host} ${clientIds.join(',')}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* #274 fix round 2: hosts a company-login-host change (or an org destroy)
|
|
55
|
+
* tried to remove and could not yet -- some live app's own ZITADEL Login V2
|
|
56
|
+
* base URI still pointed at one of them, so removing it would have broken
|
|
57
|
+
* that app's sign-in. `zitadelLoginHost` always advances
|
|
58
|
+
* `zitadel:login-host:<key>` to the org's *current* desired host on every
|
|
59
|
+
* successful run (never leaving it on an old one), so this is the only
|
|
60
|
+
* record of a removal still owed -- lost otherwise the moment a second host
|
|
61
|
+
* change moves `previousHost` on. Comma-joined, sorted, the same shape
|
|
62
|
+
* `MAIL_ROLE_MAILBOXES_SCOPE_KEY` (`executor.ts`) holds a list in.
|
|
63
|
+
*/
|
|
64
|
+
export const LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY = 'zitadel_login_host_pending_removal';
|
|
65
|
+
/** The hosts {@link LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY} currently names, parsed back out. */
|
|
66
|
+
export function pendingRemovalHosts(recorded) {
|
|
67
|
+
const raw = recorded[LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY];
|
|
68
|
+
return raw ? raw.split(',').filter((host) => host.length > 0) : [];
|
|
69
|
+
}
|
|
70
|
+
/** {@link pendingRemovalHosts}' own serialisation, sorted so the recorded value is stable regardless of check order. */
|
|
71
|
+
export function pendingRemovalState(hosts) {
|
|
72
|
+
return [...hosts].sort().join(',');
|
|
73
|
+
}
|
|
@@ -17,14 +17,19 @@ export interface AuditActor {
|
|
|
17
17
|
/**
|
|
18
18
|
* The seam onto `@wtfalch/audit`.
|
|
19
19
|
*
|
|
20
|
-
* #19
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
20
|
+
* #19 asked for `@wtfalch/audit` itself, but at the time that package's
|
|
21
|
+
* `record`/`ledger` were bound to one app's Drizzle table, session model and
|
|
22
|
+
* closed `EventName`/`ActorClass` vocabulary -- none of which foundry's SDK
|
|
23
|
+
* package had. This interface was the stand-in, logged as a Finding on #19
|
|
24
|
+
* rather than a half adoption.
|
|
25
|
+
*
|
|
26
|
+
* #232: `@wtfalch/audit` now supports a service with no tenant database of
|
|
27
|
+
* its own (`ledgerVocabulary`, `tenantId: null`, the org as `target`) -- see
|
|
28
|
+
* `store/audit.ts`'s `createPostgresAuditLog`, which binds `ledger.sign` to
|
|
29
|
+
* foundry's own closed vocabulary and `foundry_audit_events`' replacement,
|
|
30
|
+
* `audit_events` (`@wtfalch/audit`'s own table, through migrations
|
|
31
|
+
* 0022-0026). This interface itself is unchanged -- `bootstrap.ts` and
|
|
32
|
+
* `redactingAudit` need no change either.
|
|
28
33
|
*/
|
|
29
34
|
export interface AuditLog {
|
|
30
35
|
recordPlan(entry: {
|
|
@@ -187,6 +192,8 @@ export interface PlatformStepExecutor {
|
|
|
187
192
|
ai?: string;
|
|
188
193
|
archon?: string;
|
|
189
194
|
mail?: string;
|
|
195
|
+
/** #257: see `PlanPlatformOptions.blocked.backupExecutorMissing` (`platform-diff.ts`). */
|
|
196
|
+
backupExecutorMissing?: boolean;
|
|
190
197
|
}>;
|
|
191
198
|
/**
|
|
192
199
|
* Steps whose live state differs from the declared one in a way the recorded
|
|
@@ -281,6 +288,13 @@ export interface FoundryPlanApplyOptions {
|
|
|
281
288
|
* step never runs (and fails) where no scheduler is deployed.
|
|
282
289
|
*/
|
|
283
290
|
jobsScheduler?: boolean;
|
|
291
|
+
/**
|
|
292
|
+
* #267: true when the host's executor was built with a `loginHostBindings`
|
|
293
|
+
* port. Optional, and false without it: each org's login host is then
|
|
294
|
+
* reported blocked instead of planned, so nothing is trusted or repointed
|
|
295
|
+
* before zitadel-login's binding can be written.
|
|
296
|
+
*/
|
|
297
|
+
loginHostBindings?: boolean;
|
|
284
298
|
/**
|
|
285
299
|
* #177: true when the host's executor was built with its own `filesOrganisation`
|
|
286
300
|
* / `aiOrganisation` port. Without one, the org step needs the platform's
|
|
@@ -334,6 +348,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
334
348
|
private readonly actor;
|
|
335
349
|
private readonly agora;
|
|
336
350
|
private readonly jobsScheduler;
|
|
351
|
+
private readonly loginHostBindings;
|
|
337
352
|
private readonly hostFiles;
|
|
338
353
|
private readonly hostAi;
|
|
339
354
|
private readonly now;
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
3
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
3
4
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
4
5
|
import { readLivePlatformResources, readLiveResources, } from './live.js';
|
|
@@ -78,6 +79,7 @@ export class FoundryPlanApply {
|
|
|
78
79
|
actor;
|
|
79
80
|
agora;
|
|
80
81
|
jobsScheduler;
|
|
82
|
+
loginHostBindings;
|
|
81
83
|
hostFiles;
|
|
82
84
|
hostAi;
|
|
83
85
|
now;
|
|
@@ -92,6 +94,7 @@ export class FoundryPlanApply {
|
|
|
92
94
|
this.actor = options.actor;
|
|
93
95
|
this.agora = options.agora;
|
|
94
96
|
this.jobsScheduler = options.jobsScheduler ?? false;
|
|
97
|
+
this.loginHostBindings = options.loginHostBindings ?? false;
|
|
95
98
|
this.hostFiles = options.filesOrganisation ?? false;
|
|
96
99
|
this.hostAi = options.aiOrganisation ?? false;
|
|
97
100
|
this.now = options.now ?? (() => new Date());
|
|
@@ -351,7 +354,7 @@ export class FoundryPlanApply {
|
|
|
351
354
|
this.orgPlatformReadiness(),
|
|
352
355
|
this.orgAgoraHost(id),
|
|
353
356
|
]);
|
|
354
|
-
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness);
|
|
357
|
+
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, { loginHostBindings: this.loginHostBindings });
|
|
355
358
|
return { org, plan: toPlan(detail), detail };
|
|
356
359
|
}
|
|
357
360
|
/**
|
|
@@ -426,6 +429,15 @@ export class FoundryPlanApply {
|
|
|
426
429
|
async sfuBlockedReason(platform) {
|
|
427
430
|
if (!platform.sfu.enabled)
|
|
428
431
|
return null;
|
|
432
|
+
// #248: both images the SFU compose actually runs, checked before the
|
|
433
|
+
// connection -- a floating tag is a reason to block independent of
|
|
434
|
+
// whether a connection is held.
|
|
435
|
+
const livekitBlocked = floatingTagBlocked('LiveKit', VERSIONS.livekit, 'v1.13.7');
|
|
436
|
+
if (livekitBlocked)
|
|
437
|
+
return `the platform SFU is blocked, not planned: ${livekitBlocked}`;
|
|
438
|
+
const cloudflaredBlocked = floatingTagBlocked('cloudflared', VERSIONS.cloudflared, '2026.9.3');
|
|
439
|
+
if (cloudflaredBlocked)
|
|
440
|
+
return `the platform SFU is blocked, not planned: ${cloudflaredBlocked}`;
|
|
429
441
|
const port = this.requirePlatformDeps().sfuConnection;
|
|
430
442
|
if (!port) {
|
|
431
443
|
return 'the platform SFU is blocked, not planned: the host supplies no SFU connection port (#170)';
|
|
@@ -510,7 +522,10 @@ const ZITADEL_ORG_SCOPE_KEY = 'zitadel_org';
|
|
|
510
522
|
/**
|
|
511
523
|
* Every resource `FoundryPlanApply.destroy` tears down, built from
|
|
512
524
|
* `AppliedState.serviceIds`, in the order the goal line and this campaign's
|
|
513
|
-
* report settle on: the
|
|
525
|
+
* report settle on: the org's own company login host first (#274 -- an
|
|
526
|
+
* instance-wide ZITADEL trust and a zitadel-login binding, neither depending
|
|
527
|
+
* on anything else this org owns, so nothing is lost by going first), then
|
|
528
|
+
* the Cloudflare tunnel, then the Access gate (stop routing traffic to the
|
|
514
529
|
* app before tearing it down), then the org's own Coolify database and app,
|
|
515
530
|
* then the org's customers ZITADEL org (plan gap A9), then every ZITADEL app
|
|
516
531
|
* foundry recorded, then the ZITADEL project, then the
|
|
@@ -539,6 +554,7 @@ function destroyTargets(serviceIds) {
|
|
|
539
554
|
// collected in this order, then every ZITADEL app (there can be several,
|
|
540
555
|
// #71), then the two scope ids last.
|
|
541
556
|
const ORDERED_PREFIXES = [
|
|
557
|
+
'zitadel:login-host:',
|
|
542
558
|
'cloudflare:tunnel:',
|
|
543
559
|
'cloudflare:access:',
|
|
544
560
|
'coolify:database:',
|
|
@@ -617,12 +633,13 @@ function stepField(action) {
|
|
|
617
633
|
*
|
|
618
634
|
* Three sources, in this order:
|
|
619
635
|
* - fields no planner step exists for yet, so no apply can make them true:
|
|
620
|
-
* SSO, guests and solo when turned on,
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
636
|
+
* SSO, guests and solo when turned on, an adopted app in the `repo` form
|
|
637
|
+
* (#7's `#15` comment: "declared but refused by `apply`"), and calls or
|
|
638
|
+
* hosting on a domain the org has not recorded (#145: calls on the org's
|
|
639
|
+
* own domain now has a planner step, the same way hosting's own domain
|
|
640
|
+
* does, so it only fails for that one missing-domain reason, not
|
|
641
|
+
* unconditionally). `bespokeAppRepo` is not one of these: `coolify:app`
|
|
642
|
+
* reads it (#240), so it converges with that step like any other field;
|
|
626
643
|
* - chat's reverse re-key, which the planner surfaces but which is not built
|
|
627
644
|
* (#7 amendment 3), so it counts as failed even when no step threw;
|
|
628
645
|
* - on a failed apply, the field of every planned step from the one that
|
|
@@ -646,8 +663,6 @@ function failedFields(org, detail, completed, serviceIds) {
|
|
|
646
663
|
failed.add('guests.enabled');
|
|
647
664
|
if (org.solo.enabled)
|
|
648
665
|
failed.add('solo.enabled');
|
|
649
|
-
if (org.bespokeAppRepo !== null)
|
|
650
|
-
failed.add('bespokeAppRepo');
|
|
651
666
|
if (org.adoptedApp?.form === 'repo')
|
|
652
667
|
failed.add('adoptedApp');
|
|
653
668
|
// `planCallsService`/`planTunnel`/`planZone` all share this same domain
|
|
@@ -688,6 +703,11 @@ function canonical(plan) {
|
|
|
688
703
|
orgId: plan.orgId,
|
|
689
704
|
steps: [...plan.steps].sort((x, y) => x.description.localeCompare(y.description)),
|
|
690
705
|
unmanaged: [...plan.unmanaged].sort((x, y) => x.description.localeCompare(y.description)),
|
|
706
|
+
// #251: display-only, but still part of what a plan says -- live state
|
|
707
|
+
// moving between `plan()` and `apply()` in a way that only changes a
|
|
708
|
+
// version-change line must still be caught as stale, the same as any
|
|
709
|
+
// other field here.
|
|
710
|
+
versionChanges: [...(plan.versionChanges ?? [])].sort((x, y) => x.description.localeCompare(y.description)),
|
|
691
711
|
};
|
|
692
712
|
}
|
|
693
713
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { OrgId } from '../org.js';
|
|
2
|
-
import type
|
|
1
|
+
import type { DeclaredOrg, OrgId } from '../org.js';
|
|
2
|
+
import { type Plan, type UnmanagedItem, type VersionChangeItem } from '../plan.js';
|
|
3
3
|
import type { DeclaredPlatform } from '../platform.js';
|
|
4
4
|
import type { PlannedStep } from './diff.js';
|
|
5
5
|
import type { LiveResource } from './live.js';
|
|
@@ -14,6 +14,8 @@ import type { LiveResource } from './live.js';
|
|
|
14
14
|
export interface PlatformPlanDetail {
|
|
15
15
|
plannedSteps: PlannedStep[];
|
|
16
16
|
unmanaged: UnmanagedItem[];
|
|
17
|
+
/** #251: see {@link VersionChangeItem}. Never folded into `plannedSteps`. */
|
|
18
|
+
versionChanges: VersionChangeItem[];
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
|
|
@@ -33,6 +35,47 @@ export declare function toPlatformPlan(detail: PlatformPlanDetail, ownerOrgId: O
|
|
|
33
35
|
* an org's own (which is keyed by that org's id).
|
|
34
36
|
*/
|
|
35
37
|
export declare const PLATFORM_AUTH_KEY = "platform:auth";
|
|
38
|
+
/**
|
|
39
|
+
* #251: the ZITADEL image tag the last successful auth run actually set
|
|
40
|
+
* live. `provisionPlatformAuth` records this only when it set the tag --
|
|
41
|
+
* today that is creation alone, `findOrCreatePlatformApp`'s own
|
|
42
|
+
* `justCreated`, since nothing yet patches `docker_registry_image_tag` on
|
|
43
|
+
* an app found already running (unlike mail, which does). Recording it on
|
|
44
|
+
* every run regardless, the way the app id itself is recorded, would make
|
|
45
|
+
* {@link versionNotAppliedLine} compare the pinned version against a value
|
|
46
|
+
* that was never actually applied.
|
|
47
|
+
*/
|
|
48
|
+
export declare const PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY = "auth_zitadel_applied_image_tag";
|
|
49
|
+
/**
|
|
50
|
+
* #251: the Synapse/MAS image tags an org's calls Matrix stack last
|
|
51
|
+
* successful apply actually set live. `coolifyCalls` (`executor.ts`)
|
|
52
|
+
* records these only at creation, for the same reason
|
|
53
|
+
* {@link PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY} is: nothing yet
|
|
54
|
+
* patches either tag on an app `findOrCreateCallsApp` found already
|
|
55
|
+
* running.
|
|
56
|
+
*/
|
|
57
|
+
export declare const CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY = "calls_synapse_applied_image_tag";
|
|
58
|
+
export declare const CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY = "calls_mas_applied_image_tag";
|
|
59
|
+
/**
|
|
60
|
+
* One line for a service whose own step genuinely redeploys when its
|
|
61
|
+
* recorded applied ref differs from the declared or pinned one (mail,
|
|
62
|
+
* archon, ai, files, integrations) -- `from` undefined (nothing applied
|
|
63
|
+
* yet, a first-ever apply) or already equal to `to` means nothing to
|
|
64
|
+
* report.
|
|
65
|
+
*/
|
|
66
|
+
export declare function versionChangeLine(service: string, from: string | undefined, to: string, note: string): VersionChangeItem | null;
|
|
67
|
+
/**
|
|
68
|
+
* One line for a service whose own step never patches a running app's
|
|
69
|
+
* image tag once created -- zitadel, synapse, mas. #251 round 3: a prior
|
|
70
|
+
* attempt worded this line the same as {@link versionChangeLine}'s
|
|
71
|
+
* "X -> Y (image, redeploy)", which promised a redeploy `apply` can never
|
|
72
|
+
* actually perform for these three. `appLive` false means nothing live yet
|
|
73
|
+
* to compare against (no line, the same first-apply case
|
|
74
|
+
* {@link versionChangeLine} has); a live app with no recorded `applied`
|
|
75
|
+
* value predates this tracking and is reported as `running tag unknown`
|
|
76
|
+
* rather than silently showing nothing.
|
|
77
|
+
*/
|
|
78
|
+
export declare function versionNotAppliedLine(service: string, appLive: boolean, applied: string | undefined, pinned: string): VersionChangeItem | null;
|
|
36
79
|
/**
|
|
37
80
|
* Scope keys (colon-free, so never mistaken for a `system:kind:key` resource
|
|
38
81
|
* id) for foundry's ZITADEL admin machine user. The delivered key is the guard
|
|
@@ -268,6 +311,28 @@ export declare function platformMailMxHost(apex: string): string;
|
|
|
268
311
|
*/
|
|
269
312
|
export declare const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = "mail_applied_apex";
|
|
270
313
|
export declare const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = "mail_applied_host";
|
|
314
|
+
/**
|
|
315
|
+
* #243: the org's `hosting.appEnv` values the last `coolify:app` apply pushed,
|
|
316
|
+
* recorded as one JSON object (env name to value; ids and a domain, never a
|
|
317
|
+
* secret). `planApp` plans the step again when {@link resolveAppEnv} differs
|
|
318
|
+
* from it, so a changed map reaches an app that is already live.
|
|
319
|
+
*/
|
|
320
|
+
export declare const APP_ENV_APPLIED_SCOPE_KEY = "app_env_applied";
|
|
321
|
+
/**
|
|
322
|
+
* #239: set to '1' once an app has been restarted after its database URLs
|
|
323
|
+
* were pushed. Not a URL. Without it a crash between the push and the restart
|
|
324
|
+
* would leave a stale container that no plan notices.
|
|
325
|
+
*/
|
|
326
|
+
export declare const DB_ENV_APPLIED_SCOPE_KEY = "db_env_applied";
|
|
327
|
+
/**
|
|
328
|
+
* The `hosting.appEnv` entries whose source has a recorded value, in env-name
|
|
329
|
+
* order, and the env names whose source has none yet. Shared by `planApp` and
|
|
330
|
+
* the executor's `pushAppEnv` so both read the same recorded ids.
|
|
331
|
+
*/
|
|
332
|
+
export declare function resolveAppEnv(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): {
|
|
333
|
+
values: Record<string, string>;
|
|
334
|
+
unresolved: Array<[string, string]>;
|
|
335
|
+
};
|
|
271
336
|
/**
|
|
272
337
|
* #172: the resource key `planAi` plans under and `FoundryPlatformStepExecutor`
|
|
273
338
|
* dispatches on, the same `platform:<service>` shape as
|
|
@@ -419,6 +484,13 @@ export interface PlanPlatformOptions {
|
|
|
419
484
|
ai?: string;
|
|
420
485
|
archon?: string;
|
|
421
486
|
mail?: string;
|
|
487
|
+
/**
|
|
488
|
+
* #257: true once a service's database exists but the host built the
|
|
489
|
+
* platform executor with no backup dependencies -- every such service's
|
|
490
|
+
* `coolify:backup:<service>` step is blocked, not planned, the same
|
|
491
|
+
* "executor missing" shape {@link authExecutorMissing} has for auth.
|
|
492
|
+
*/
|
|
493
|
+
backupExecutorMissing?: boolean;
|
|
422
494
|
};
|
|
423
495
|
/**
|
|
424
496
|
* Steps whose live state differs from the declared one in a way the recorded
|
|
@@ -464,6 +536,34 @@ export declare function planPlatform(platform: DeclaredPlatform, live: LiveResou
|
|
|
464
536
|
* it, so the planner and the executor/reader can never key it apart).
|
|
465
537
|
*/
|
|
466
538
|
export declare const JOBS_SCHEDULER_KEY = "jobs";
|
|
539
|
+
/**
|
|
540
|
+
* #257: the six platform databases foundry itself creates, each keyed by
|
|
541
|
+
* the scope id `platform-providers.ts`'s own `createPostgresDatabase` call
|
|
542
|
+
* site records once that service's database exists --
|
|
543
|
+
* `JOBS_DATABASE_SCOPE_KEY`, `PLATFORM_AUTH_DB_SCOPE_KEY`,
|
|
544
|
+
* `PLATFORM_ARCHON_DB_SCOPE_KEY`, `PLATFORM_AI_DB_SCOPE_KEY`,
|
|
545
|
+
* `PLATFORM_FILES_DB_SCOPE_KEY`, `INTEGRATIONS_DATABASE_SCOPE_KEY`. Those
|
|
546
|
+
* constants stay module-private in `platform-providers.ts`, grouped with
|
|
547
|
+
* each service's other scope keys; the string values here are a second,
|
|
548
|
+
* documented copy rather than an import, so `planBackups` below never
|
|
549
|
+
* reaches into that file and `platform-providers.ts` keeps importing *from*
|
|
550
|
+
* this one, never the other way (every other scope key constant already
|
|
551
|
+
* flows that direction). `platform-providers.test.ts`'s own
|
|
552
|
+
* `coolify:backup` suite exercises both the executor's real constants and
|
|
553
|
+
* this planner end to end, so a renamed key on either side shows up as a
|
|
554
|
+
* database the planner can no longer see -- reported blocked, per rule 2,
|
|
555
|
+
* never a wrong step.
|
|
556
|
+
*/
|
|
557
|
+
export declare const PLATFORM_BACKUP_DATABASE_SCOPE_KEYS: {
|
|
558
|
+
readonly jobs: "coolify_jobs_database";
|
|
559
|
+
readonly auth: "coolify_platform_auth_database";
|
|
560
|
+
readonly archon: "coolify_platform_archon_database";
|
|
561
|
+
readonly ai: "coolify_platform_ai_database";
|
|
562
|
+
readonly files: "coolify_platform_files_database";
|
|
563
|
+
readonly integrations: "coolify_integrations_database";
|
|
564
|
+
};
|
|
565
|
+
export type PlatformBackupServiceKey = keyof typeof PLATFORM_BACKUP_DATABASE_SCOPE_KEYS;
|
|
566
|
+
export declare const PLATFORM_BACKUP_SERVICE_KEYS: readonly PlatformBackupServiceKey[];
|
|
467
567
|
/**
|
|
468
568
|
* #174: the resource key the integrations host is planned and recorded under,
|
|
469
569
|
* `platform:integrations`, like {@link PLATFORM_AUTH_KEY}: the step is
|