@wtfalch/foundry 0.17.0 → 0.18.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/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/migrations/0045_names_to_edge.sql +107 -0
- package/dist/org.d.ts +74 -3
- package/dist/org.js +133 -0
- package/dist/plan/chat-matrix.d.ts +0 -1
- package/dist/plan/chat-matrix.js +4 -1
- package/dist/plan/diff.d.ts +18 -0
- package/dist/plan/diff.js +239 -26
- package/dist/plan/entry-edge-key.d.ts +79 -0
- package/dist/plan/entry-edge-key.js +260 -0
- package/dist/plan/executor.d.ts +246 -1
- package/dist/plan/executor.js +684 -32
- 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 +15 -0
- package/dist/plan/plan-apply.js +102 -9
- 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 +45 -1
- package/dist/plan/platform-providers.js +514 -7
- package/dist/plan/step-system.js +1 -0
- package/dist/plan/worker.d.ts +134 -0
- package/dist/plan/worker.js +239 -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_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_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, PlatformEdgeDeps, PlatformEdgeRuntime, } 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_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_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. `'edge'`
|
|
30
|
+
* (#317) is the same: a edge 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' | 'edge';
|
|
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 `worker` 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 `EdgePort`.
|
|
351
|
+
* Optional, and false without it: each entry's `uses.edge` step is then
|
|
352
|
+
* reported blocked instead of planned to fail.
|
|
353
|
+
*/
|
|
354
|
+
edge?: 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 hostEdge;
|
|
421
435
|
private readonly now;
|
|
422
436
|
private readonly leaseTtlMs;
|
|
423
437
|
private readonly appliedVersions;
|
|
@@ -566,6 +580,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
566
580
|
* which only the plan can read.
|
|
567
581
|
*/
|
|
568
582
|
private invalidHosting;
|
|
583
|
+
private platformServiceHostnames;
|
|
569
584
|
/**
|
|
570
585
|
* #177: an org's plan reads the shared platform's own addresses and
|
|
571
586
|
* readiness through the very `platform` deps `planPlatform`/`applyPlatform`
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -8,12 +8,14 @@ import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScope
|
|
|
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
10
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
11
|
+
import { entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey } from './entry-edge-key.js';
|
|
11
12
|
import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
|
|
12
13
|
import { readLivePlatformResources, readLiveResources, } from './live.js';
|
|
13
14
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
14
15
|
import { PLATFORM_ARCHON_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
15
16
|
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
16
17
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
18
|
+
import { liveWorkers, workerTraceScopeKeys } from './worker.js';
|
|
17
19
|
/** #441: the lease scope that serializes every mutation of one company, and the one for the shared platform. */
|
|
18
20
|
const companyScope = (orgId) => `company:${orgId}`;
|
|
19
21
|
const PLATFORM_SCOPE = 'platform';
|
|
@@ -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 `validateEntryWorker`.
|
|
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
|
+
hostEdge;
|
|
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.hostEdge = options.edge ?? 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 workerTraceScopeKeys(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
|
}
|
|
@@ -852,12 +869,15 @@ export class FoundryPlanApply {
|
|
|
852
869
|
this.orgArchonUrl(),
|
|
853
870
|
]);
|
|
854
871
|
const otherSlugs = (await this.store.list()).filter((o) => o.id !== id).map((o) => o.slug);
|
|
872
|
+
const platformHostnames = await this.platformServiceHostnames();
|
|
855
873
|
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, {
|
|
856
874
|
loginHostBindings: this.loginHostBindings,
|
|
857
875
|
broker: this.broker,
|
|
876
|
+
edgeWorkers: this.edgeWorkers,
|
|
858
877
|
otherSlugs,
|
|
859
878
|
now: () => this.now().getTime(),
|
|
860
879
|
archonUrl,
|
|
880
|
+
platformHostnames,
|
|
861
881
|
});
|
|
862
882
|
const invalid = await this.invalidHosting(org);
|
|
863
883
|
if (invalid) {
|
|
@@ -877,15 +897,15 @@ export class FoundryPlanApply {
|
|
|
877
897
|
* which only the plan can read.
|
|
878
898
|
*/
|
|
879
899
|
async invalidHosting(org) {
|
|
880
|
-
const platform = await this.platformDeps?.store.get();
|
|
881
|
-
const platformHostnames = platform
|
|
882
|
-
? PLATFORM_SERVICE_KEYS.map((k) => platform[k].hostname)
|
|
883
|
-
: [];
|
|
884
900
|
return validateHosting(org.hosting, {
|
|
885
901
|
domain: org.domain,
|
|
886
|
-
locked: lockedHostnames(org,
|
|
902
|
+
locked: lockedHostnames(org, await this.platformServiceHostnames()),
|
|
887
903
|
});
|
|
888
904
|
}
|
|
905
|
+
async platformServiceHostnames() {
|
|
906
|
+
const platform = await this.platformDeps?.store.get();
|
|
907
|
+
return platform ? PLATFORM_SERVICE_KEYS.map((k) => platform[k].hostname) : [];
|
|
908
|
+
}
|
|
889
909
|
/**
|
|
890
910
|
* #177: an org's plan reads the shared platform's own addresses and
|
|
891
911
|
* readiness through the very `platform` deps `planPlatform`/`applyPlatform`
|
|
@@ -909,7 +929,7 @@ export class FoundryPlanApply {
|
|
|
909
929
|
refs.add(ORG_PLATFORM_KEY_REFS.ai);
|
|
910
930
|
if (this.jobsScheduler)
|
|
911
931
|
refs.add(ORG_PLATFORM_KEY_REFS.jobs);
|
|
912
|
-
const readiness = orgPlatformReadinessFrom(platform, recorded, refs, !heldKeys);
|
|
932
|
+
const readiness = orgPlatformReadinessFrom(platform, recorded, refs, !heldKeys, this.hostEdge);
|
|
913
933
|
// Entry env from the platform's mail key or a shared secret (#T8).
|
|
914
934
|
let mailKeyHeld = false;
|
|
915
935
|
try {
|
|
@@ -1112,7 +1132,7 @@ const ZITADEL_ORG_SCOPE_KEY = 'zitadel_org';
|
|
|
1112
1132
|
* belongs to is already left in place -- deleting only the rule and not the
|
|
1113
1133
|
* zone is a decision left for whoever wires teardown in, not made here.
|
|
1114
1134
|
*/
|
|
1115
|
-
function destroyTargets(serviceIds) {
|
|
1135
|
+
function destroyTargets(serviceIds, orgId) {
|
|
1116
1136
|
// Reverse-teardown order: everything under one of these prefixes is
|
|
1117
1137
|
// collected in this order, then every ZITADEL app (there can be several,
|
|
1118
1138
|
// #71), then the two scope ids last.
|
|
@@ -1120,6 +1140,8 @@ function destroyTargets(serviceIds) {
|
|
|
1120
1140
|
'zitadel:login-host:',
|
|
1121
1141
|
'cloudflare:tunnel:',
|
|
1122
1142
|
'cloudflare:access:',
|
|
1143
|
+
// #318: an entry's Worker and its route.
|
|
1144
|
+
'cloudflare:worker:',
|
|
1123
1145
|
'coolify:database:',
|
|
1124
1146
|
'coolify:app:',
|
|
1125
1147
|
'zitadel:customers:',
|
|
@@ -1127,10 +1149,27 @@ function destroyTargets(serviceIds) {
|
|
|
1127
1149
|
'ai:key:',
|
|
1128
1150
|
// F6b: an entry's files key, `<orgId>:<entry>`; the org's own key has no `:` and is left alone.
|
|
1129
1151
|
'files:key:',
|
|
1152
|
+
// F7 (#317): an entry's edge key, `<orgId>:<entry>` -- edge has no org-root key.
|
|
1153
|
+
'edge:key:',
|
|
1130
1154
|
];
|
|
1131
1155
|
const targets = [];
|
|
1132
1156
|
const adopted = adoptedResourceIds(serviceIds);
|
|
1133
1157
|
for (const prefix of ORDERED_PREFIXES) {
|
|
1158
|
+
if (prefix === 'cloudflare:worker:') {
|
|
1159
|
+
// #318 re-review: not the main ids alone -- `liveWorkers` also
|
|
1160
|
+
// finds an upload-only or route-only partial deploy, and leaves out an
|
|
1161
|
+
// entry whose Worker is already retired (nothing live, so a host with
|
|
1162
|
+
// no WorkersPort is not blocked from destroying the org by it).
|
|
1163
|
+
for (const [entryKey, scriptName] of liveWorkers(orgId, serviceIds)) {
|
|
1164
|
+
targets.push({
|
|
1165
|
+
system: 'cloudflare',
|
|
1166
|
+
kind: 'worker',
|
|
1167
|
+
key: `${orgId}:${entryKey}`,
|
|
1168
|
+
id: scriptName,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1134
1173
|
for (const [resourceKey, id] of Object.entries(serviceIds)) {
|
|
1135
1174
|
if (!resourceKey.startsWith(prefix))
|
|
1136
1175
|
continue;
|
|
@@ -1141,10 +1180,49 @@ function destroyTargets(serviceIds) {
|
|
|
1141
1180
|
continue;
|
|
1142
1181
|
if (prefix === 'files:key:' && !resourceKey.slice(prefix.length).includes(':'))
|
|
1143
1182
|
continue;
|
|
1183
|
+
// A key a retire step already revoked before org destroy ever ran
|
|
1184
|
+
// (#317 review): `runSteps` never clears this main id on a step that
|
|
1185
|
+
// returns no resourceId, so without this check destroy would target
|
|
1186
|
+
// an already-revoked key and get stuck on edge' 404 forever.
|
|
1187
|
+
if (prefix === 'edge:key:') {
|
|
1188
|
+
// `resourceKey.slice(prefix.length)` is `<orgId>:<entryKey>`, but
|
|
1189
|
+
// `entryEdgeRetiredScopeKey` takes the bare entry key alone (as
|
|
1190
|
+
// every caller in entry-edge-key.ts/executor.ts gives it) --
|
|
1191
|
+
// entry keys hold no `:` of their own, so the first one found is
|
|
1192
|
+
// exactly the org id/entry key boundary.
|
|
1193
|
+
const fullKey = resourceKey.slice(prefix.length);
|
|
1194
|
+
const entryKey = fullKey.slice(fullKey.indexOf(':') + 1);
|
|
1195
|
+
if (serviceIds[entryEdgeRetiredScopeKey(entryKey)] === id)
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1144
1198
|
const [system, kind] = prefix.split(':');
|
|
1145
1199
|
targets.push({ system, kind, key: resourceKey.slice(prefix.length), id });
|
|
1146
1200
|
}
|
|
1147
1201
|
}
|
|
1202
|
+
// F7 (#317): a key minted but never delivered never gets the main
|
|
1203
|
+
// `edge:key:<org>:<entry>` id recorded (the mint step throws before
|
|
1204
|
+
// `runSteps` sees a `resourceId`), so the loop above never finds it to
|
|
1205
|
+
// revoke on destroy. Skipped if a later mint already succeeded and holds
|
|
1206
|
+
// its own, still-live main id -- entry keys hold no `:`, so matching the
|
|
1207
|
+
// suffix is unambiguous. A main id that equals the entry's retired marker
|
|
1208
|
+
// does not count as "later": mint A, deliver, retire (main id A, retired
|
|
1209
|
+
// marker A); re-declare, mint B, fail delivery three times (undelivered
|
|
1210
|
+
// B, main id stays A). Without this check, A's stale-but-truthy main id
|
|
1211
|
+
// hid B from destroy, leaking it the same way the gone-scan in
|
|
1212
|
+
// `entry-edge-key.ts` once did (#317 re-review).
|
|
1213
|
+
const undeliveredEdgePrefix = 'edge_key_undelivered_';
|
|
1214
|
+
for (const [resourceKey, id] of Object.entries(serviceIds)) {
|
|
1215
|
+
if (!id || !resourceKey.startsWith(undeliveredEdgePrefix))
|
|
1216
|
+
continue;
|
|
1217
|
+
const entryKey = resourceKey.slice(undeliveredEdgePrefix.length);
|
|
1218
|
+
const hasLaterMainId = Object.entries(serviceIds).some(([k, v]) => v &&
|
|
1219
|
+
k.startsWith('edge:key:') &&
|
|
1220
|
+
k.endsWith(`:${entryKey}`) &&
|
|
1221
|
+
serviceIds[entryEdgeRetiredScopeKey(entryKey)] !== v);
|
|
1222
|
+
if (hasLaterMainId)
|
|
1223
|
+
continue;
|
|
1224
|
+
targets.push({ system: 'edge', kind: 'key', key: entryKey, id });
|
|
1225
|
+
}
|
|
1148
1226
|
for (const [resourceKey, id] of Object.entries(serviceIds)) {
|
|
1149
1227
|
if (!resourceKey.startsWith('zitadel:app:'))
|
|
1150
1228
|
continue;
|
|
@@ -1226,6 +1304,7 @@ const STEP_FIELDS = {
|
|
|
1226
1304
|
'cloudflare:zone': 'hosting.usesDomain',
|
|
1227
1305
|
'cloudflare:access': 'hosting.accessEmails',
|
|
1228
1306
|
'cloudflare:ratelimit': 'hosting.usesDomain',
|
|
1307
|
+
'cloudflare:worker': 'hosting.entries.worker',
|
|
1229
1308
|
};
|
|
1230
1309
|
function stepField(action) {
|
|
1231
1310
|
// An entry's ai key (F6a) is keyed `<orgId>:<entry>`; the org's own is `<orgId>`.
|
|
@@ -1235,6 +1314,9 @@ function stepField(action) {
|
|
|
1235
1314
|
// The org's own files key (F6b) is keyed `<orgId>`, an entry's `<orgId>:<entry>`.
|
|
1236
1315
|
if (action.system === 'files' && action.kind === 'key')
|
|
1237
1316
|
return 'hosting.entries.uses.files';
|
|
1317
|
+
// An entry's edge key (F7): always `<orgId>:<entry>`, edge has no org-root key.
|
|
1318
|
+
if (action.system === 'edge' && action.kind === 'key')
|
|
1319
|
+
return 'hosting.entries.uses.edge';
|
|
1238
1320
|
// An org declares several issuer apps, each keyed by its name (#71).
|
|
1239
1321
|
if (action.system === 'zitadel' && action.kind === 'app') {
|
|
1240
1322
|
return `hosting.issuerApps.${action.key}`;
|
|
@@ -1283,6 +1365,17 @@ function failedFields(org, detail, completed, serviceIds) {
|
|
|
1283
1365
|
}
|
|
1284
1366
|
}
|
|
1285
1367
|
}
|
|
1368
|
+
// The same for an entry's edge key (F7): minted, never delivered is no
|
|
1369
|
+
// longer blocked forever on its own (the next apply revokes it and
|
|
1370
|
+
// mints again, #317 re-review) -- but while that retry step is itself
|
|
1371
|
+
// not planned this run (no edge service wired, say), the leftover
|
|
1372
|
+
// undelivered id must still read as unresolved rather than the org
|
|
1373
|
+
// converging silently around it.
|
|
1374
|
+
for (const entry of org.hosting.entries) {
|
|
1375
|
+
if (entry.uses?.edge && serviceIds[entryEdgeUndeliveredScopeKey(entry.key)]) {
|
|
1376
|
+
failed.add('hosting.entries.uses.edge');
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1286
1379
|
// The same for a jobs registration whose credentials never reached the host.
|
|
1287
1380
|
if (serviceIds[JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY] &&
|
|
1288
1381
|
!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 `planEdge` plans under and
|
|
626
|
+
* `FoundryPlatformStepExecutor` dispatches on -- the {@link PLATFORM_FILES_KEY}
|
|
627
|
+
* shape for the edge host (https://github.com/wtfalch/edge). `runPlatformSteps`
|
|
628
|
+
* records the app's uuid under `coolify:app:platform:edge` once the step
|
|
629
|
+
* succeeds, which is what stops `planEdge` planning it again.
|
|
630
|
+
*/
|
|
631
|
+
export declare const PLATFORM_EDGE_KEY = "platform:edge";
|
|
632
|
+
/**
|
|
633
|
+
* #478: the held-key ref the edge 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
|
+
* (`EDGE_PLATFORM_KEY_HASH`). Archon's own `EdgePort` reads the full
|
|
637
|
+
* secret back from this ref to call edge' `mintKey`/`revokeKey`/
|
|
638
|
+
* `setReservedLabels`.
|
|
639
|
+
*/
|
|
640
|
+
export declare const PLATFORM_EDGE_KEY_REF = "edge:platform:key";
|
|
641
|
+
/**
|
|
642
|
+
* #478: the held-key ref for edge' 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, edge-only credential a human provisions and holds
|
|
645
|
+
* separately from whatever broader token `PlatformEdgeDeps.cloudflare`
|
|
646
|
+
* uses to stand the host itself up on Coolify.
|
|
647
|
+
*/
|
|
648
|
+
export declare const PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF = "cloudflare:edge:apiToken";
|
|
649
|
+
/**
|
|
650
|
+
* #478: scope keys (colon-free) for the edge 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_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY = "edge_platform_key_undelivered";
|
|
659
|
+
export declare const PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY = "edge_platform_key_set";
|
|
660
|
+
/**
|
|
661
|
+
* #478: the hostname and `gitRef` the last successful names run applied.
|
|
662
|
+
* `planEdge` 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_EDGE_APPLIED_HOSTNAME_SCOPE_KEY = "edge_applied_hostname";
|
|
667
|
+
export declare const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = "edge_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 edge host is blocked, or absent when it is not. */
|
|
707
|
+
edge?: 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 planEdge}
|
|
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
|