@wtfalch/foundry 0.17.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/declare.js +6 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0043_platform_names.sql +25 -0
- package/dist/migrations/0044_platform_mail_lettermint_project.sql +11 -0
- package/dist/org.d.ts +74 -3
- package/dist/org.js +143 -0
- package/dist/plan/chat-matrix.d.ts +0 -1
- package/dist/plan/chat-matrix.js +4 -1
- package/dist/plan/diff.d.ts +16 -0
- package/dist/plan/diff.js +237 -24
- package/dist/plan/edge-worker.d.ts +114 -0
- package/dist/plan/edge-worker.js +201 -0
- package/dist/plan/entry-names-key.d.ts +73 -0
- package/dist/plan/entry-names-key.js +236 -0
- package/dist/plan/executor.d.ts +235 -1
- package/dist/plan/executor.js +611 -27
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/live.d.ts +4 -2
- package/dist/plan/login-host.d.ts +12 -1
- package/dist/plan/login-host.js +20 -2
- package/dist/plan/plan-apply.d.ts +14 -0
- package/dist/plan/plan-apply.js +95 -4
- package/dist/plan/platform-diff.d.ts +92 -1
- package/dist/plan/platform-diff.js +166 -1
- package/dist/plan/platform-org.d.ts +29 -1
- package/dist/plan/platform-org.js +17 -2
- package/dist/plan/platform-providers.d.ts +44 -0
- package/dist/plan/platform-providers.js +514 -7
- package/dist/plan/step-system.js +1 -0
- package/dist/platform.d.ts +22 -3
- package/dist/platform.js +6 -0
- package/dist/store/postgres.js +15 -1
- package/dist/store/schema.d.ts +70 -2
- package/dist/store/schema.js +6 -0
- package/package.json +1 -1
package/dist/plan/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { computePlanDetail, toPlan } from './diff.js';
|
|
|
5
5
|
export type { StepSystem } from './step-system.js';
|
|
6
6
|
export { stepSystemOf } from './step-system.js';
|
|
7
7
|
export type { PlatformPlanDetail } from './platform-diff.js';
|
|
8
|
-
export { 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_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, } from './platform-diff.js';
|
|
8
|
+
export { 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_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_NAMES_KEY, PLATFORM_NAMES_KEY_REF, PLATFORM_NAMES_CLOUDFLARE_TOKEN_REF, } from './platform-diff.js';
|
|
9
9
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
|
|
10
10
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
11
11
|
export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
|
@@ -15,7 +15,7 @@ export { AdoptRefusedError } from './adopt.js';
|
|
|
15
15
|
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan-apply.js';
|
|
16
16
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
17
17
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
18
|
-
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './platform-providers.js';
|
|
18
|
+
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformNamesDeps, PlatformNamesRuntime, } from './platform-providers.js';
|
|
19
19
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
|
|
20
20
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
21
21
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
2
2
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
3
3
|
export { stepSystemOf } from './step-system.js';
|
|
4
|
-
export { 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_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, } from './platform-diff.js';
|
|
4
|
+
export { 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_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_NAMES_KEY, PLATFORM_NAMES_KEY_REF, PLATFORM_NAMES_CLOUDFLARE_TOKEN_REF, } from './platform-diff.js';
|
|
5
5
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
6
6
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
7
7
|
export { AdoptRefusedError } from './adopt.js';
|
package/dist/plan/live.d.ts
CHANGED
|
@@ -26,9 +26,11 @@ export interface LiveResource {
|
|
|
26
26
|
* `platform-diff.ts`'s `planSfu` always plans its step when
|
|
27
27
|
* `DeclaredPlatform.sfu.enabled` is true, rather than diffing against a
|
|
28
28
|
* live resource the way every other system here does. It is still part of
|
|
29
|
-
* this union so `StepAction.system` (`diff.ts`) can name it.
|
|
29
|
+
* this union so `StepAction.system` (`diff.ts`) can name it. `'names'`
|
|
30
|
+
* (#317) is the same: a names key has no live lookup of its own, the same
|
|
31
|
+
* shape `ai`/`files` entry keys already follow.
|
|
30
32
|
*/
|
|
31
|
-
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu';
|
|
33
|
+
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu' | 'names';
|
|
32
34
|
kind: string;
|
|
33
35
|
key: string;
|
|
34
36
|
config: Record<string, unknown>;
|
|
@@ -21,8 +21,19 @@ import type { DeclaredOrg } from '../org.js';
|
|
|
21
21
|
* `RESERVED_AUTH_AND_ENVIRONMENTS` already reserves the `auth` *slug* so no
|
|
22
22
|
* org's own `<slug>.oikos.host` can collide with this subdomain, the same
|
|
23
23
|
* way `AGORA_SUFFIX` reserves `-agora`.
|
|
24
|
+
*
|
|
25
|
+
* #488: `auth.<domain>` is refused when it would equal `platformAuthHostname`
|
|
26
|
+
* -- the platform's own org, declared with-domain on the platform's own
|
|
27
|
+
* domain, would otherwise compute the shared ZITADEL instance's own login
|
|
28
|
+
* host (`platform.auth.hostname`, e.g. `auth.companyincriminated.com` ->
|
|
29
|
+
* `auth.companyincluded.com`). Archon's login-host write throws on that
|
|
30
|
+
* duplicate host (`archon`'s `validateLoginHostRegistry`); if it were ever
|
|
31
|
+
* written instead, `auth`'s own `company-host.ts` denies every request
|
|
32
|
+
* through a duplicate host, the shared one included. `platformAuthHostname`
|
|
33
|
+
* is `null` when the caller has none to compare against (no platform
|
|
34
|
+
* declared, or sign-in not ready yet), in which case this never refuses.
|
|
24
35
|
*/
|
|
25
|
-
export declare function loginHostnameFor(org: DeclaredOrg): string;
|
|
36
|
+
export declare function loginHostnameFor(org: DeclaredOrg, platformAuthHostname?: string | null): string;
|
|
26
37
|
/**
|
|
27
38
|
* One entry in zitadel-login's own `LOGIN_HOST_BINDINGS`
|
|
28
39
|
* (`auth/docs/company-login-hosts.md`) -- a single-line JSON document naming
|
package/dist/plan/login-host.js
CHANGED
|
@@ -22,12 +22,30 @@ import { BOULE_CLIENT_ID_SCOPE_KEY } from './boule.js';
|
|
|
22
22
|
* `RESERVED_AUTH_AND_ENVIRONMENTS` already reserves the `auth` *slug* so no
|
|
23
23
|
* org's own `<slug>.oikos.host` can collide with this subdomain, the same
|
|
24
24
|
* way `AGORA_SUFFIX` reserves `-agora`.
|
|
25
|
+
*
|
|
26
|
+
* #488: `auth.<domain>` is refused when it would equal `platformAuthHostname`
|
|
27
|
+
* -- the platform's own org, declared with-domain on the platform's own
|
|
28
|
+
* domain, would otherwise compute the shared ZITADEL instance's own login
|
|
29
|
+
* host (`platform.auth.hostname`, e.g. `auth.companyincriminated.com` ->
|
|
30
|
+
* `auth.companyincluded.com`). Archon's login-host write throws on that
|
|
31
|
+
* duplicate host (`archon`'s `validateLoginHostRegistry`); if it were ever
|
|
32
|
+
* written instead, `auth`'s own `company-host.ts` denies every request
|
|
33
|
+
* through a duplicate host, the shared one included. `platformAuthHostname`
|
|
34
|
+
* is `null` when the caller has none to compare against (no platform
|
|
35
|
+
* declared, or sign-in not ready yet), in which case this never refuses.
|
|
25
36
|
*/
|
|
26
|
-
export function loginHostnameFor(org) {
|
|
37
|
+
export function loginHostnameFor(org, platformAuthHostname = null) {
|
|
27
38
|
// `issuer.usesDomain` is the issuer's opt-in into the org's domain
|
|
28
39
|
// (`org.ts`'s `IssuerConfig`): a domain alone never moves it, so a domain
|
|
29
40
|
// with `usesDomain` false stays on the tier 1 host.
|
|
30
|
-
|
|
41
|
+
if (org.domain && org.issuer.usesDomain) {
|
|
42
|
+
const host = `auth.${org.domain}`;
|
|
43
|
+
if (platformAuthHostname && host === platformAuthHostname) {
|
|
44
|
+
return `${org.slug}-auth.oikos.host`;
|
|
45
|
+
}
|
|
46
|
+
return host;
|
|
47
|
+
}
|
|
48
|
+
return `${org.slug}-auth.oikos.host`;
|
|
31
49
|
}
|
|
32
50
|
/**
|
|
33
51
|
* Scope id holding what the binding was last written with
|
|
@@ -333,6 +333,12 @@ export interface FoundryPlanApplyOptions {
|
|
|
333
333
|
* reported blocked instead of planned.
|
|
334
334
|
*/
|
|
335
335
|
broker?: boolean;
|
|
336
|
+
/**
|
|
337
|
+
* #318: true when the host's executor was built with a `WorkersPort`.
|
|
338
|
+
* Optional, and false without it: each entry's `edge` Worker is then
|
|
339
|
+
* reported blocked instead of planned.
|
|
340
|
+
*/
|
|
341
|
+
edgeWorkers?: boolean;
|
|
336
342
|
/**
|
|
337
343
|
* #177: true when the host's executor was built with its own `filesOrganisation`
|
|
338
344
|
* / `aiOrganisation` port. Without one, the org step needs the platform's
|
|
@@ -340,6 +346,12 @@ export interface FoundryPlanApplyOptions {
|
|
|
340
346
|
*/
|
|
341
347
|
filesOrganisation?: boolean;
|
|
342
348
|
aiOrganisation?: boolean;
|
|
349
|
+
/**
|
|
350
|
+
* #317: true when the host's executor was built with a `NamesPort`.
|
|
351
|
+
* Optional, and false without it: each entry's `uses.names` step is then
|
|
352
|
+
* reported blocked instead of planned to fail.
|
|
353
|
+
*/
|
|
354
|
+
names?: boolean;
|
|
343
355
|
/** Overridable for tests; defaults to `() => new Date()`. */
|
|
344
356
|
now?: () => Date;
|
|
345
357
|
/** #441: how long a mutation lease lasts before another caller may take it over. Defaults to 30 minutes. */
|
|
@@ -416,8 +428,10 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
416
428
|
private readonly jobsScheduler;
|
|
417
429
|
private readonly loginHostBindings;
|
|
418
430
|
private readonly broker;
|
|
431
|
+
private readonly edgeWorkers;
|
|
419
432
|
private readonly hostFiles;
|
|
420
433
|
private readonly hostAi;
|
|
434
|
+
private readonly hostNames;
|
|
421
435
|
private readonly now;
|
|
422
436
|
private readonly leaseTtlMs;
|
|
423
437
|
private readonly appliedVersions;
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -7,8 +7,10 @@ import { StaleLeaseError, } from '../store.js';
|
|
|
7
7
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
8
8
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
9
9
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
10
|
+
import { edgeWorkerTraceScopeKeys, liveEdgeWorkers } from './edge-worker.js';
|
|
10
11
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
11
12
|
import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
|
|
13
|
+
import { entryNamesRetiredScopeKey, entryNamesUndeliveredScopeKey } from './entry-names-key.js';
|
|
12
14
|
import { readLivePlatformResources, readLiveResources, } from './live.js';
|
|
13
15
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
14
16
|
import { PLATFORM_ARCHON_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
@@ -97,6 +99,9 @@ const HOSTNAME_STEPS = new Set([
|
|
|
97
99
|
'cloudflare:tunnel',
|
|
98
100
|
'cloudflare:access',
|
|
99
101
|
'jobs:registration',
|
|
102
|
+
// #318 (F8): the route is itself hostname-shaped, validated against the
|
|
103
|
+
// org's own domain by `validateEntryEdge`.
|
|
104
|
+
'cloudflare:worker',
|
|
100
105
|
]);
|
|
101
106
|
/**
|
|
102
107
|
* The #19 implementation of {@link PlanApply}: computes a plan against live
|
|
@@ -116,8 +121,10 @@ export class FoundryPlanApply {
|
|
|
116
121
|
jobsScheduler;
|
|
117
122
|
loginHostBindings;
|
|
118
123
|
broker;
|
|
124
|
+
edgeWorkers;
|
|
119
125
|
hostFiles;
|
|
120
126
|
hostAi;
|
|
127
|
+
hostNames;
|
|
121
128
|
now;
|
|
122
129
|
leaseTtlMs;
|
|
123
130
|
appliedVersions;
|
|
@@ -135,8 +142,10 @@ export class FoundryPlanApply {
|
|
|
135
142
|
this.jobsScheduler = options.jobsScheduler ?? false;
|
|
136
143
|
this.loginHostBindings = options.loginHostBindings ?? false;
|
|
137
144
|
this.broker = options.broker ?? false;
|
|
145
|
+
this.edgeWorkers = options.edgeWorkers ?? false;
|
|
138
146
|
this.hostFiles = options.filesOrganisation ?? false;
|
|
139
147
|
this.hostAi = options.aiOrganisation ?? false;
|
|
148
|
+
this.hostNames = options.names ?? false;
|
|
140
149
|
this.now = options.now ?? (() => new Date());
|
|
141
150
|
this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
142
151
|
this.appliedVersions = options.appliedVersions;
|
|
@@ -477,7 +486,7 @@ export class FoundryPlanApply {
|
|
|
477
486
|
const serviceIds = { ...(state?.serviceIds ?? {}) };
|
|
478
487
|
let targets;
|
|
479
488
|
try {
|
|
480
|
-
targets = destroyTargets(serviceIds);
|
|
489
|
+
targets = destroyTargets(serviceIds, id);
|
|
481
490
|
}
|
|
482
491
|
catch (e) {
|
|
483
492
|
await this.refuseDestroy(id, e instanceof Error ? e.message : String(e));
|
|
@@ -530,6 +539,14 @@ export class FoundryPlanApply {
|
|
|
530
539
|
// gone" itself.
|
|
531
540
|
await this.destroyer.destroy(target, { org, recordedIds: { ...serviceIds } });
|
|
532
541
|
delete serviceIds[destroyServiceIdKey(target)];
|
|
542
|
+
if (target.system === 'cloudflare' && target.kind === 'worker') {
|
|
543
|
+
// #318: its partial-deploy traces too, or a retry after a later
|
|
544
|
+
// step fails would read this Worker as live again.
|
|
545
|
+
const entryKey = target.key.slice(id.length + 1);
|
|
546
|
+
for (const scopeKey of edgeWorkerTraceScopeKeys(entryKey)) {
|
|
547
|
+
delete serviceIds[scopeKey];
|
|
548
|
+
}
|
|
549
|
+
}
|
|
533
550
|
steps[current] = {
|
|
534
551
|
step: descriptions[current],
|
|
535
552
|
status: 'finished',
|
|
@@ -593,7 +610,7 @@ export class FoundryPlanApply {
|
|
|
593
610
|
const state = await this.store.getAppliedState(id);
|
|
594
611
|
const serviceIds = { ...(state?.serviceIds ?? {}) };
|
|
595
612
|
return {
|
|
596
|
-
steps: destroyStepDescriptions(destroyTargets(serviceIds)),
|
|
613
|
+
steps: destroyStepDescriptions(destroyTargets(serviceIds, id)),
|
|
597
614
|
kept: keptDescriptions(serviceIds),
|
|
598
615
|
};
|
|
599
616
|
}
|
|
@@ -855,6 +872,7 @@ export class FoundryPlanApply {
|
|
|
855
872
|
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, {
|
|
856
873
|
loginHostBindings: this.loginHostBindings,
|
|
857
874
|
broker: this.broker,
|
|
875
|
+
edgeWorkers: this.edgeWorkers,
|
|
858
876
|
otherSlugs,
|
|
859
877
|
now: () => this.now().getTime(),
|
|
860
878
|
archonUrl,
|
|
@@ -909,7 +927,7 @@ export class FoundryPlanApply {
|
|
|
909
927
|
refs.add(ORG_PLATFORM_KEY_REFS.ai);
|
|
910
928
|
if (this.jobsScheduler)
|
|
911
929
|
refs.add(ORG_PLATFORM_KEY_REFS.jobs);
|
|
912
|
-
const readiness = orgPlatformReadinessFrom(platform, recorded, refs, !heldKeys);
|
|
930
|
+
const readiness = orgPlatformReadinessFrom(platform, recorded, refs, !heldKeys, this.hostNames);
|
|
913
931
|
// Entry env from the platform's mail key or a shared secret (#T8).
|
|
914
932
|
let mailKeyHeld = false;
|
|
915
933
|
try {
|
|
@@ -1112,7 +1130,7 @@ const ZITADEL_ORG_SCOPE_KEY = 'zitadel_org';
|
|
|
1112
1130
|
* belongs to is already left in place -- deleting only the rule and not the
|
|
1113
1131
|
* zone is a decision left for whoever wires teardown in, not made here.
|
|
1114
1132
|
*/
|
|
1115
|
-
function destroyTargets(serviceIds) {
|
|
1133
|
+
function destroyTargets(serviceIds, orgId) {
|
|
1116
1134
|
// Reverse-teardown order: everything under one of these prefixes is
|
|
1117
1135
|
// collected in this order, then every ZITADEL app (there can be several,
|
|
1118
1136
|
// #71), then the two scope ids last.
|
|
@@ -1120,6 +1138,8 @@ function destroyTargets(serviceIds) {
|
|
|
1120
1138
|
'zitadel:login-host:',
|
|
1121
1139
|
'cloudflare:tunnel:',
|
|
1122
1140
|
'cloudflare:access:',
|
|
1141
|
+
// #318: an entry's edge Worker and its route.
|
|
1142
|
+
'cloudflare:worker:',
|
|
1123
1143
|
'coolify:database:',
|
|
1124
1144
|
'coolify:app:',
|
|
1125
1145
|
'zitadel:customers:',
|
|
@@ -1127,10 +1147,27 @@ function destroyTargets(serviceIds) {
|
|
|
1127
1147
|
'ai:key:',
|
|
1128
1148
|
// F6b: an entry's files key, `<orgId>:<entry>`; the org's own key has no `:` and is left alone.
|
|
1129
1149
|
'files:key:',
|
|
1150
|
+
// F7 (#317): an entry's names key, `<orgId>:<entry>` -- names has no org-root key.
|
|
1151
|
+
'names:key:',
|
|
1130
1152
|
];
|
|
1131
1153
|
const targets = [];
|
|
1132
1154
|
const adopted = adoptedResourceIds(serviceIds);
|
|
1133
1155
|
for (const prefix of ORDERED_PREFIXES) {
|
|
1156
|
+
if (prefix === 'cloudflare:worker:') {
|
|
1157
|
+
// #318 re-review: not the main ids alone -- `liveEdgeWorkers` also
|
|
1158
|
+
// finds an upload-only or route-only partial deploy, and leaves out an
|
|
1159
|
+
// entry whose Worker is already retired (nothing live, so a host with
|
|
1160
|
+
// no WorkersPort is not blocked from destroying the org by it).
|
|
1161
|
+
for (const [entryKey, scriptName] of liveEdgeWorkers(orgId, serviceIds)) {
|
|
1162
|
+
targets.push({
|
|
1163
|
+
system: 'cloudflare',
|
|
1164
|
+
kind: 'worker',
|
|
1165
|
+
key: `${orgId}:${entryKey}`,
|
|
1166
|
+
id: scriptName,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1134
1171
|
for (const [resourceKey, id] of Object.entries(serviceIds)) {
|
|
1135
1172
|
if (!resourceKey.startsWith(prefix))
|
|
1136
1173
|
continue;
|
|
@@ -1141,10 +1178,49 @@ function destroyTargets(serviceIds) {
|
|
|
1141
1178
|
continue;
|
|
1142
1179
|
if (prefix === 'files:key:' && !resourceKey.slice(prefix.length).includes(':'))
|
|
1143
1180
|
continue;
|
|
1181
|
+
// A key a retire step already revoked before org destroy ever ran
|
|
1182
|
+
// (#317 review): `runSteps` never clears this main id on a step that
|
|
1183
|
+
// returns no resourceId, so without this check destroy would target
|
|
1184
|
+
// an already-revoked key and get stuck on names' 404 forever.
|
|
1185
|
+
if (prefix === 'names:key:') {
|
|
1186
|
+
// `resourceKey.slice(prefix.length)` is `<orgId>:<entryKey>`, but
|
|
1187
|
+
// `entryNamesRetiredScopeKey` takes the bare entry key alone (as
|
|
1188
|
+
// every caller in entry-names-key.ts/executor.ts gives it) --
|
|
1189
|
+
// entry keys hold no `:` of their own, so the first one found is
|
|
1190
|
+
// exactly the org id/entry key boundary.
|
|
1191
|
+
const fullKey = resourceKey.slice(prefix.length);
|
|
1192
|
+
const entryKey = fullKey.slice(fullKey.indexOf(':') + 1);
|
|
1193
|
+
if (serviceIds[entryNamesRetiredScopeKey(entryKey)] === id)
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1144
1196
|
const [system, kind] = prefix.split(':');
|
|
1145
1197
|
targets.push({ system, kind, key: resourceKey.slice(prefix.length), id });
|
|
1146
1198
|
}
|
|
1147
1199
|
}
|
|
1200
|
+
// F7 (#317): a key minted but never delivered never gets the main
|
|
1201
|
+
// `names:key:<org>:<entry>` id recorded (the mint step throws before
|
|
1202
|
+
// `runSteps` sees a `resourceId`), so the loop above never finds it to
|
|
1203
|
+
// revoke on destroy. Skipped if a later mint already succeeded and holds
|
|
1204
|
+
// its own, still-live main id -- entry keys hold no `:`, so matching the
|
|
1205
|
+
// suffix is unambiguous. A main id that equals the entry's retired marker
|
|
1206
|
+
// does not count as "later": mint A, deliver, retire (main id A, retired
|
|
1207
|
+
// marker A); re-declare, mint B, fail delivery three times (undelivered
|
|
1208
|
+
// B, main id stays A). Without this check, A's stale-but-truthy main id
|
|
1209
|
+
// hid B from destroy, leaking it the same way the gone-scan in
|
|
1210
|
+
// `entry-names-key.ts` once did (#317 re-review).
|
|
1211
|
+
const undeliveredNamesPrefix = 'names_key_undelivered_';
|
|
1212
|
+
for (const [resourceKey, id] of Object.entries(serviceIds)) {
|
|
1213
|
+
if (!id || !resourceKey.startsWith(undeliveredNamesPrefix))
|
|
1214
|
+
continue;
|
|
1215
|
+
const entryKey = resourceKey.slice(undeliveredNamesPrefix.length);
|
|
1216
|
+
const hasLaterMainId = Object.entries(serviceIds).some(([k, v]) => v &&
|
|
1217
|
+
k.startsWith('names:key:') &&
|
|
1218
|
+
k.endsWith(`:${entryKey}`) &&
|
|
1219
|
+
serviceIds[entryNamesRetiredScopeKey(entryKey)] !== v);
|
|
1220
|
+
if (hasLaterMainId)
|
|
1221
|
+
continue;
|
|
1222
|
+
targets.push({ system: 'names', kind: 'key', key: entryKey, id });
|
|
1223
|
+
}
|
|
1148
1224
|
for (const [resourceKey, id] of Object.entries(serviceIds)) {
|
|
1149
1225
|
if (!resourceKey.startsWith('zitadel:app:'))
|
|
1150
1226
|
continue;
|
|
@@ -1226,6 +1302,7 @@ const STEP_FIELDS = {
|
|
|
1226
1302
|
'cloudflare:zone': 'hosting.usesDomain',
|
|
1227
1303
|
'cloudflare:access': 'hosting.accessEmails',
|
|
1228
1304
|
'cloudflare:ratelimit': 'hosting.usesDomain',
|
|
1305
|
+
'cloudflare:worker': 'hosting.entries.edge',
|
|
1229
1306
|
};
|
|
1230
1307
|
function stepField(action) {
|
|
1231
1308
|
// An entry's ai key (F6a) is keyed `<orgId>:<entry>`; the org's own is `<orgId>`.
|
|
@@ -1235,6 +1312,9 @@ function stepField(action) {
|
|
|
1235
1312
|
// The org's own files key (F6b) is keyed `<orgId>`, an entry's `<orgId>:<entry>`.
|
|
1236
1313
|
if (action.system === 'files' && action.kind === 'key')
|
|
1237
1314
|
return 'hosting.entries.uses.files';
|
|
1315
|
+
// An entry's names key (F7): always `<orgId>:<entry>`, names has no org-root key.
|
|
1316
|
+
if (action.system === 'names' && action.kind === 'key')
|
|
1317
|
+
return 'hosting.entries.uses.names';
|
|
1238
1318
|
// An org declares several issuer apps, each keyed by its name (#71).
|
|
1239
1319
|
if (action.system === 'zitadel' && action.kind === 'app') {
|
|
1240
1320
|
return `hosting.issuerApps.${action.key}`;
|
|
@@ -1283,6 +1363,17 @@ function failedFields(org, detail, completed, serviceIds) {
|
|
|
1283
1363
|
}
|
|
1284
1364
|
}
|
|
1285
1365
|
}
|
|
1366
|
+
// The same for an entry's names key (F7): minted, never delivered is no
|
|
1367
|
+
// longer blocked forever on its own (the next apply revokes it and
|
|
1368
|
+
// mints again, #317 re-review) -- but while that retry step is itself
|
|
1369
|
+
// not planned this run (no names service wired, say), the leftover
|
|
1370
|
+
// undelivered id must still read as unresolved rather than the org
|
|
1371
|
+
// converging silently around it.
|
|
1372
|
+
for (const entry of org.hosting.entries) {
|
|
1373
|
+
if (entry.uses?.names && serviceIds[entryNamesUndeliveredScopeKey(entry.key)]) {
|
|
1374
|
+
failed.add('hosting.entries.uses.names');
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1286
1377
|
// The same for a jobs registration whose credentials never reached the host.
|
|
1287
1378
|
if (serviceIds[JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY] &&
|
|
1288
1379
|
!serviceIds[`jobs:registration:${org.id}`]) {
|
|
@@ -118,6 +118,23 @@ export declare const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = "archon_applied
|
|
|
118
118
|
* unrecorded and the old key stays pinned.
|
|
119
119
|
*/
|
|
120
120
|
export declare const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = "archon_integrations_key_synced";
|
|
121
|
+
/**
|
|
122
|
+
* #467: sha256 of the exact env set the *last confirmed* Archon deploy ran
|
|
123
|
+
* with, recorded only once that deploy's `waitForHealthyVersion` passed --
|
|
124
|
+
* mirroring {@link PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY}'s own
|
|
125
|
+
* confirm-after-health-check timing, generalized from one env to all of
|
|
126
|
+
* them. `provisionPlatformArchon` clears this to `''` the moment it decides
|
|
127
|
+
* a redeploy is needed, before attempting it: a deploy that then fails (or
|
|
128
|
+
* a crash before the health check passes) must leave the attempt
|
|
129
|
+
* unconfirmed, and the env hash alone cannot tell a failed retry's intended
|
|
130
|
+
* content apart from a successful one's, since both intend the same bytes.
|
|
131
|
+
* Without this, a run that writes new envs and then fails before Archon
|
|
132
|
+
* redeploys -- `envsDiffer` comparing desired envs against the *live*
|
|
133
|
+
* store, which this run's own write already updated -- leaves a retry
|
|
134
|
+
* reading no difference at all, and the container stays on the stale
|
|
135
|
+
* values until someone redeploys by hand (#467).
|
|
136
|
+
*/
|
|
137
|
+
export declare const PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY = "archon_envs_deployed_hash";
|
|
121
138
|
/**
|
|
122
139
|
* #211 round 4: the broker app id Archon last registered its entry with.
|
|
123
140
|
* `provisionPlatformArchon` reads the broker to register with from
|
|
@@ -136,6 +153,21 @@ export declare const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = "archon_applied_
|
|
|
136
153
|
* has recorded it.
|
|
137
154
|
*/
|
|
138
155
|
export declare const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = "archon_applied_bootstrap_operator";
|
|
156
|
+
/**
|
|
157
|
+
* #475: whether the last successful Archon apply patched the app with
|
|
158
|
+
* `custom_network_aliases` and `is_consistent_container_name_enabled: false`
|
|
159
|
+
* instead of the old fixed `custom_internal_name` alone. Coolify only runs a
|
|
160
|
+
* rolling, health-checked deploy -- start the new container, wait, then stop
|
|
161
|
+
* the old one -- when the consistent-name flag is off; with it on (set by
|
|
162
|
+
* foundry's own pre-#475 applies, or by hand in the Coolify UI) every deploy
|
|
163
|
+
* stops the old container before the new one exists, which is what took
|
|
164
|
+
* Archon down for #475's incident. Absent on every platform applied before
|
|
165
|
+
* this fix landed; `planArchon` plans the step again until
|
|
166
|
+
* `provisionPlatformArchon` records it, which only happens once that run's
|
|
167
|
+
* redeploy has passed its health check -- the same confirm-after-health-check
|
|
168
|
+
* timing {@link PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY} uses.
|
|
169
|
+
*/
|
|
170
|
+
export declare const PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY = "archon_applied_rolling_update";
|
|
139
171
|
/**
|
|
140
172
|
* The owner's ZITADEL user id Archon takes as its first operator, from
|
|
141
173
|
* {@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY}. Undefined when none is
|
|
@@ -409,6 +441,18 @@ export declare function appEnvAppliedScopeKey(entryKey: string): string;
|
|
|
409
441
|
* would leave a stale container that no plan notices.
|
|
410
442
|
*/
|
|
411
443
|
export declare function dbEnvAppliedScopeKey(entryKey: string): string;
|
|
444
|
+
/**
|
|
445
|
+
* #456: the same marker as {@link dbEnvAppliedScopeKey}, for the chat and
|
|
446
|
+
* calls-service sibling platform services' own owner/runtime database split
|
|
447
|
+
* -- in its own `service_db_env_applied.` namespace, never
|
|
448
|
+
* `dbEnvAppliedScopeKey`'s `db_env_applied.`. Those two services are not
|
|
449
|
+
* hosting entries, but `dbEnvAppliedScopeKey`'s `entryKey` is an arbitrary
|
|
450
|
+
* caller-chosen string with no reserved words (`validateHostingEntry` only
|
|
451
|
+
* requires a DNS label), so a hosting entry a company declares with the key
|
|
452
|
+
* `chat` or `calls-service` would otherwise read or clear the sibling
|
|
453
|
+
* service's own marker, and vice versa.
|
|
454
|
+
*/
|
|
455
|
+
export declare function serviceDbEnvAppliedScopeKey(service: 'chat' | 'calls-service'): string;
|
|
412
456
|
/**
|
|
413
457
|
* The settings `coolifyApp` owns on an entry's app (`folder`, `port`,
|
|
414
458
|
* `autoDeploy`) as the JSON the marker below holds. `coolifyApp` records it
|
|
@@ -577,6 +621,50 @@ export declare const PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY = "files_ma
|
|
|
577
621
|
*/
|
|
578
622
|
export declare const PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY = "files_applied_hostname";
|
|
579
623
|
export declare const PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY = "files_applied_git_ref";
|
|
624
|
+
/**
|
|
625
|
+
* #478: the resource key `planNames` plans under and
|
|
626
|
+
* `FoundryPlatformStepExecutor` dispatches on -- the {@link PLATFORM_FILES_KEY}
|
|
627
|
+
* shape for the names host (https://github.com/wtfalch/names). `runPlatformSteps`
|
|
628
|
+
* records the app's uuid under `coolify:app:platform:names` once the step
|
|
629
|
+
* succeeds, which is what stops `planNames` planning it again.
|
|
630
|
+
*/
|
|
631
|
+
export declare const PLATFORM_NAMES_KEY = "platform:names";
|
|
632
|
+
/**
|
|
633
|
+
* #478: the held-key ref the names platform secret's full value is
|
|
634
|
+
* delivered under, once, by {@link FoundryStepExecutorPorts.deliverSecret} --
|
|
635
|
+
* never the host's app env, which gets only its SHA-256 hash
|
|
636
|
+
* (`NAMES_PLATFORM_KEY_HASH`). Archon's own `NamesPort` reads the full
|
|
637
|
+
* secret back from this ref to call names' `mintKey`/`revokeKey`/
|
|
638
|
+
* `setReservedLabels`.
|
|
639
|
+
*/
|
|
640
|
+
export declare const PLATFORM_NAMES_KEY_REF = "names:platform:key";
|
|
641
|
+
/**
|
|
642
|
+
* #478: the held-key ref for names' own Cloudflare API token (tunnel edit +
|
|
643
|
+
* DNS edit on one account) -- the issue's own "never Archon's token": this
|
|
644
|
+
* is a narrower, names-only credential a human provisions and holds
|
|
645
|
+
* separately from whatever broader token `PlatformNamesDeps.cloudflare`
|
|
646
|
+
* uses to stand the host itself up on Coolify.
|
|
647
|
+
*/
|
|
648
|
+
export declare const PLATFORM_NAMES_CLOUDFLARE_TOKEN_REF = "cloudflare:names:apiToken";
|
|
649
|
+
/**
|
|
650
|
+
* #478: scope keys (colon-free) for the names platform key, the
|
|
651
|
+
* {@link PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY} pattern once:
|
|
652
|
+
* the undelivered id is recorded and persisted the moment the secret is
|
|
653
|
+
* minted, before delivery; the guard is recorded only once it was delivered
|
|
654
|
+
* AND its hash written to the runner's env. An undelivered id with no guard
|
|
655
|
+
* means the secret is lost (never kept past that point): the planner
|
|
656
|
+
* reports it and never mints again. Neither key holds the secret itself.
|
|
657
|
+
*/
|
|
658
|
+
export declare const PLATFORM_NAMES_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY = "names_platform_key_undelivered";
|
|
659
|
+
export declare const PLATFORM_NAMES_PLATFORM_KEY_SET_SCOPE_KEY = "names_platform_key_set";
|
|
660
|
+
/**
|
|
661
|
+
* #478: the hostname and `gitRef` the last successful names run applied.
|
|
662
|
+
* `planNames` plans the step again when either differs from the declared
|
|
663
|
+
* value; a re-run updates `git_branch` and the runner env and redeploys,
|
|
664
|
+
* and mints no new platform key (the guards above hold).
|
|
665
|
+
*/
|
|
666
|
+
export declare const PLATFORM_NAMES_APPLIED_HOSTNAME_SCOPE_KEY = "names_applied_hostname";
|
|
667
|
+
export declare const PLATFORM_NAMES_APPLIED_GIT_REF_SCOPE_KEY = "names_applied_git_ref";
|
|
580
668
|
/**
|
|
581
669
|
* Options for {@link planPlatform}, replacing four positional arguments that
|
|
582
670
|
* shifted on every merge that added a platform service (#186, #187, #188) --
|
|
@@ -615,6 +703,8 @@ export interface PlanPlatformOptions {
|
|
|
615
703
|
ai?: string;
|
|
616
704
|
archon?: string;
|
|
617
705
|
mail?: string;
|
|
706
|
+
/** #478: why the names host is blocked, or absent when it is not. */
|
|
707
|
+
names?: string;
|
|
618
708
|
/**
|
|
619
709
|
* #257: true once a service's database exists but the host built the
|
|
620
710
|
* platform executor with no backup dependencies -- every such service's
|
|
@@ -666,7 +756,8 @@ export interface PlanPlatformOptions {
|
|
|
666
756
|
* Scope for #167: the frame plus one step, {@link planApexZone}. #173 adds
|
|
667
757
|
* {@link planJobs}, #170 adds {@link planSfu}, #168 adds {@link planAuth},
|
|
668
758
|
* #169 adds {@link planMail}, #174 adds {@link planIntegrations}, #171 adds
|
|
669
|
-
* {@link planFiles}, #172 adds {@link planAi}
|
|
759
|
+
* {@link planFiles}, #172 adds {@link planAi}, #478 adds {@link planNames}
|
|
760
|
+
* and #175 adds
|
|
670
761
|
* {@link planArchon}, the same way #19 shipped org plan/apply against
|
|
671
762
|
* three singletons before #27, #71, #80 and the rest added the remaining
|
|
672
763
|
* twelve. Until then, a live resource under one of those eventual keys is
|