@wtfalch/foundry 0.3.0 → 0.5.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 +96 -0
- package/dist/bootstrap/bootstrap.js +285 -0
- package/dist/bootstrap/declare.d.ts +36 -0
- package/dist/bootstrap/declare.js +94 -0
- package/dist/bootstrap/drive.d.ts +40 -0
- package/dist/bootstrap/drive.js +51 -0
- package/dist/bootstrap/index.d.ts +3 -0
- package/dist/bootstrap/index.js +1 -0
- package/dist/calls-sfu-deploy-support.d.ts +9 -6
- package/dist/calls-sfu-deploy-support.js +9 -6
- package/dist/calls-sfu-keys.d.ts +21 -0
- package/dist/calls-sfu-keys.js +27 -0
- package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
- package/dist/calls-sfu-ssh-adapter.js +78 -0
- package/dist/calls-sfu-ssh-client.d.ts +49 -0
- package/dist/calls-sfu-ssh-client.js +82 -0
- package/dist/calls-sfu.d.ts +22 -4
- package/dist/calls-sfu.js +22 -4
- package/dist/index.d.ts +31 -6
- package/dist/index.js +16 -3
- package/dist/migrations/0014_calls_enabled.sql +7 -0
- package/dist/migrations/0015_domain_unique.sql +39 -0
- package/dist/migrations/0016_platform.sql +61 -0
- package/dist/migrations/0017_requested_changes.sql +42 -0
- package/dist/migrations/0018_platform_apply.sql +19 -0
- package/dist/org.d.ts +18 -2
- package/dist/org.js +56 -0
- package/dist/plan/agora.d.ts +77 -0
- package/dist/plan/agora.js +127 -0
- package/dist/plan/calls-matrix.d.ts +135 -0
- package/dist/plan/calls-matrix.js +275 -0
- package/dist/plan/cloudflared-connector.d.ts +70 -0
- package/dist/plan/cloudflared-connector.js +106 -0
- package/dist/plan/diff.d.ts +26 -5
- package/dist/plan/diff.js +720 -106
- package/dist/plan/executor.d.ts +731 -36
- package/dist/plan/executor.js +1180 -41
- package/dist/plan/index.d.ts +22 -5
- package/dist/plan/index.js +12 -2
- package/dist/plan/jobs-scheduler-http.d.ts +29 -0
- package/dist/plan/jobs-scheduler-http.js +50 -0
- package/dist/plan/live.d.ts +51 -2
- package/dist/plan/live.js +8 -0
- package/dist/plan/plan-apply.d.ts +192 -3
- package/dist/plan/plan-apply.js +290 -14
- package/dist/plan/platform-diff.d.ts +312 -0
- package/dist/plan/platform-diff.js +867 -0
- package/dist/plan/platform-org-ports.d.ts +50 -0
- package/dist/plan/platform-org-ports.js +70 -0
- package/dist/plan/platform-org.d.ts +120 -0
- package/dist/plan/platform-org.js +115 -0
- package/dist/plan/platform-providers.d.ts +501 -0
- package/dist/plan/platform-providers.js +3029 -0
- package/dist/plan/platform-sfu.d.ts +93 -0
- package/dist/plan/platform-sfu.js +100 -0
- package/dist/plan/providers-readers.js +22 -1
- package/dist/plan/zitadel-machine-user.d.ts +34 -0
- package/dist/plan/zitadel-machine-user.js +95 -0
- package/dist/plan.d.ts +6 -0
- package/dist/platform.d.ts +55 -0
- package/dist/platform.js +46 -0
- package/dist/requested-changes.d.ts +108 -0
- package/dist/requested-changes.js +95 -0
- package/dist/slug.js +21 -2
- package/dist/store/audit.js +4 -2
- package/dist/store/postgres.d.ts +40 -7
- package/dist/store/postgres.js +257 -29
- package/dist/store/schema.d.ts +700 -0
- package/dist/store/schema.js +58 -0
- package/dist/store.d.ts +56 -0
- package/package.json +2 -2
package/dist/plan/index.d.ts
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, } from './live.js';
|
|
2
|
-
export { readLiveResources } from './live.js';
|
|
1
|
+
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, } from './live.js';
|
|
2
|
+
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
3
3
|
export type { StepAction, PlannedStep, PlanDetail } from './diff.js';
|
|
4
4
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
5
|
-
export type {
|
|
6
|
-
export {
|
|
5
|
+
export type { PlatformPlanDetail } from './platform-diff.js';
|
|
6
|
+
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, } from './platform-diff.js';
|
|
7
|
+
export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
|
|
8
|
+
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
9
|
+
export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
|
10
|
+
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
11
|
+
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan-apply.js';
|
|
12
|
+
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
13
|
+
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
|
|
14
|
+
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './platform-providers.js';
|
|
15
|
+
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
|
|
16
|
+
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './platform-providers.js';
|
|
17
|
+
export { PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, PLATFORM_AI_MINTER_MISSING, } from './platform-providers.js';
|
|
18
|
+
export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './platform-providers.js';
|
|
19
|
+
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
20
|
+
export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, CallsSfuSshConnector, CallsSfuPlatformDeps, } from './platform-sfu.js';
|
|
21
|
+
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
7
22
|
export type { ScopeResolver, RecordedIds } from './providers-readers.js';
|
|
8
23
|
export { ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
9
|
-
export type {
|
|
24
|
+
export type { AgoraHostPort, AgoraHostValues } from './agora.js';
|
|
25
|
+
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, } from './executor.js';
|
|
10
26
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
27
|
+
export { HttpJobsSchedulerClient } from './jobs-scheduler-http.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
export { readLiveResources } from './live.js';
|
|
1
|
+
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
2
2
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
3
|
-
export {
|
|
3
|
+
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, } from './platform-diff.js';
|
|
4
|
+
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
5
|
+
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
6
|
+
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
7
|
+
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
|
|
8
|
+
// #171: how a host signs files management requests with the delivered key.
|
|
9
|
+
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './platform-providers.js';
|
|
10
|
+
export { PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, PLATFORM_AI_MINTER_MISSING, } from './platform-providers.js';
|
|
11
|
+
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
12
|
+
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
4
13
|
export { ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
5
14
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
15
|
+
export { HttpJobsSchedulerClient } from './jobs-scheduler-http.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { JobsSchedulerPort } from './executor.js';
|
|
2
|
+
/**
|
|
3
|
+
* #173: the real {@link JobsSchedulerPort}, a thin client over the jobs
|
|
4
|
+
* scheduler's admin HTTP API (https://github.com/wtfalch/jobs README, "The
|
|
5
|
+
* admin API", `POST /admin/projects`). The scheduler ships no SDK for this,
|
|
6
|
+
* so it is plain `fetch`.
|
|
7
|
+
*
|
|
8
|
+
* `adminToken` is the scheduler's `SCHEDULER_ADMIN_TOKEN` (`coolifyJobs`
|
|
9
|
+
* generates it and hands it to the host through `deliverSecret`); the host
|
|
10
|
+
* resolves it and builds this client. This package holds no credential of
|
|
11
|
+
* its own. The token and the credentials in a response never appear in a
|
|
12
|
+
* thrown error -- only the status and the scheduler's own `error` text,
|
|
13
|
+
* which the API builds from the name and URLs alone.
|
|
14
|
+
*/
|
|
15
|
+
export declare class HttpJobsSchedulerClient implements JobsSchedulerPort {
|
|
16
|
+
private readonly adminToken;
|
|
17
|
+
private readonly fetchImpl;
|
|
18
|
+
private readonly baseUrl;
|
|
19
|
+
constructor(baseUrl: string, adminToken: string, fetchImpl?: typeof fetch);
|
|
20
|
+
registerProject(input: {
|
|
21
|
+
name: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
alertUrl?: string;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
id: string;
|
|
26
|
+
secret?: string;
|
|
27
|
+
syncKey?: string;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #173: the real {@link JobsSchedulerPort}, a thin client over the jobs
|
|
3
|
+
* scheduler's admin HTTP API (https://github.com/wtfalch/jobs README, "The
|
|
4
|
+
* admin API", `POST /admin/projects`). The scheduler ships no SDK for this,
|
|
5
|
+
* so it is plain `fetch`.
|
|
6
|
+
*
|
|
7
|
+
* `adminToken` is the scheduler's `SCHEDULER_ADMIN_TOKEN` (`coolifyJobs`
|
|
8
|
+
* generates it and hands it to the host through `deliverSecret`); the host
|
|
9
|
+
* resolves it and builds this client. This package holds no credential of
|
|
10
|
+
* its own. The token and the credentials in a response never appear in a
|
|
11
|
+
* thrown error -- only the status and the scheduler's own `error` text,
|
|
12
|
+
* which the API builds from the name and URLs alone.
|
|
13
|
+
*/
|
|
14
|
+
export class HttpJobsSchedulerClient {
|
|
15
|
+
adminToken;
|
|
16
|
+
fetchImpl;
|
|
17
|
+
baseUrl;
|
|
18
|
+
constructor(baseUrl, adminToken, fetchImpl = fetch) {
|
|
19
|
+
this.adminToken = adminToken;
|
|
20
|
+
this.fetchImpl = fetchImpl;
|
|
21
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
22
|
+
}
|
|
23
|
+
async registerProject(input) {
|
|
24
|
+
const response = await this.fetchImpl(`${this.baseUrl}/admin/projects`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
authorization: `Bearer ${this.adminToken}`,
|
|
28
|
+
'content-type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(input),
|
|
31
|
+
});
|
|
32
|
+
const body = await response.json().catch(() => undefined);
|
|
33
|
+
const fields = typeof body === 'object' && body !== null ? body : {};
|
|
34
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
35
|
+
const detail = typeof fields.error === 'string' ? `: ${fields.error}` : '';
|
|
36
|
+
throw new Error(`jobs scheduler: POST /admin/projects answered ${response.status}${detail}`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof fields.id !== 'string' || fields.id.length === 0) {
|
|
39
|
+
throw new Error(`jobs scheduler: POST /admin/projects answered ${response.status} with no project id`);
|
|
40
|
+
}
|
|
41
|
+
// 200 is a repeat call: the scheduler returns just the id, never a secret.
|
|
42
|
+
if (response.status === 200)
|
|
43
|
+
return { id: fields.id };
|
|
44
|
+
return {
|
|
45
|
+
id: fields.id,
|
|
46
|
+
secret: typeof fields.secret === 'string' ? fields.secret : undefined,
|
|
47
|
+
syncKey: typeof fields.syncKey === 'string' ? fields.syncKey : undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/plan/live.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OrgId } from '../org.js';
|
|
2
2
|
/**
|
|
3
|
-
* One resource as seen live in one of foundry's
|
|
3
|
+
* One resource as seen live in one of foundry's seven systems.
|
|
4
4
|
*
|
|
5
5
|
* `key` is unique within `(system, kind)`. Per-org singleton resources
|
|
6
6
|
* (issuer, mail, chat, storage's spend limit, AI's budget, the cms site, the
|
|
@@ -13,9 +13,22 @@ import type { OrgId } from '../org.js';
|
|
|
13
13
|
* is keyed however that system names it, so it can still be matched, or left
|
|
14
14
|
* {@link https://github.com/wtfalch/foundry/issues/10 unmanaged}, by
|
|
15
15
|
* `computePlanDetail`.
|
|
16
|
+
*
|
|
17
|
+
* #173 adds the seventh system, `jobs`: not a resource Coolify, Cloudflare or
|
|
18
|
+
* any of the others reports, but the jobs scheduler's own admin API
|
|
19
|
+
* (https://github.com/wtfalch/jobs, jobs#62) -- an org's registration with it,
|
|
20
|
+
* keyed by the {@link OrgId} like every other per-org singleton.
|
|
16
21
|
*/
|
|
17
22
|
export interface LiveResource {
|
|
18
|
-
|
|
23
|
+
/**
|
|
24
|
+
* `'sfu'` (#170) has no reader wired below -- there is no cheap way to
|
|
25
|
+
* probe the calls SFU VM's own state over SSH during planning, so
|
|
26
|
+
* `platform-diff.ts`'s `planSfu` always plans its step when
|
|
27
|
+
* `DeclaredPlatform.sfu.enabled` is true, rather than diffing against a
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu';
|
|
19
32
|
kind: string;
|
|
20
33
|
key: string;
|
|
21
34
|
config: Record<string, unknown>;
|
|
@@ -64,3 +77,39 @@ export interface LiveReaders {
|
|
|
64
77
|
}
|
|
65
78
|
/** Every live resource foundry can see for `orgId`, across all six systems. */
|
|
66
79
|
export declare function readLiveResources(orgId: OrgId, readers: LiveReaders): Promise<LiveResource[]>;
|
|
80
|
+
/**
|
|
81
|
+
* #167: one narrow interface per system reading the shared platform's own
|
|
82
|
+
* resources (`platform.ts`'s `DeclaredPlatform`) -- the platform-scoped
|
|
83
|
+
* counterpart to {@link ZitadelReader}/{@link CoolifyReader}/
|
|
84
|
+
* {@link CloudflareReader} above. There is no {@link OrgId} to scope a read
|
|
85
|
+
* by: `DeclaredPlatform` is one record, not one per org, so `listResources`
|
|
86
|
+
* here takes no argument at all.
|
|
87
|
+
*
|
|
88
|
+
* `cloudflare` is wired for #167's own `cloudflare:zone` step
|
|
89
|
+
* (`platform-diff.ts`'s `planApexZone`); #173 adds `coolify`, for the jobs
|
|
90
|
+
* scheduler's own Coolify app (`platform-diff.ts`'s `planJobs`). The rest of
|
|
91
|
+
* the platform's eventual surface (`platform-diff.ts`'s own doc comment:
|
|
92
|
+
* "every other service ... is left for its own issue to plan") adds a reader
|
|
93
|
+
* here, and to {@link PlatformLiveReaders} and {@link readLivePlatformResources}
|
|
94
|
+
* below, as each service's own step lands -- the same way `LiveReaders` above
|
|
95
|
+
* grew from three readers to six across #19, #27 and #80.
|
|
96
|
+
*/
|
|
97
|
+
export interface PlatformCloudflareReader {
|
|
98
|
+
listResources(): Promise<LiveResource[]>;
|
|
99
|
+
}
|
|
100
|
+
/** #173: the platform's own Coolify-hosted jobs scheduler (`platform-diff.ts`'s `planJobs`). */
|
|
101
|
+
export interface PlatformCoolifyReader {
|
|
102
|
+
listResources(): Promise<LiveResource[]>;
|
|
103
|
+
}
|
|
104
|
+
export interface PlatformLiveReaders {
|
|
105
|
+
cloudflare: PlatformCloudflareReader;
|
|
106
|
+
/**
|
|
107
|
+
* Optional so every #167-era caller -- everything that plans/applies only
|
|
108
|
+
* the apex zone -- is unaffected. Without one the scheduler could never be
|
|
109
|
+
* seen live, so `planJobs` plans no step and reports the scheduler as
|
|
110
|
+
* blocked (an `unmanaged` item) rather than re-planning it forever.
|
|
111
|
+
*/
|
|
112
|
+
coolify?: PlatformCoolifyReader;
|
|
113
|
+
}
|
|
114
|
+
/** Every live resource foundry can see for the platform, across every wired reader (#167, #173). */
|
|
115
|
+
export declare function readLivePlatformResources(readers: PlatformLiveReaders): Promise<LiveResource[]>;
|
package/dist/plan/live.js
CHANGED
|
@@ -10,3 +10,11 @@ export async function readLiveResources(orgId, readers) {
|
|
|
10
10
|
]);
|
|
11
11
|
return [...zitadel, ...coolify, ...cloudflare, ...files, ...ai, ...cms];
|
|
12
12
|
}
|
|
13
|
+
/** Every live resource foundry can see for the platform, across every wired reader (#167, #173). */
|
|
14
|
+
export async function readLivePlatformResources(readers) {
|
|
15
|
+
const [cloudflare, coolify] = await Promise.all([
|
|
16
|
+
readers.cloudflare.listResources(),
|
|
17
|
+
readers.coolify ? readers.coolify.listResources() : Promise.resolve([]),
|
|
18
|
+
]);
|
|
19
|
+
return [...cloudflare, ...coolify];
|
|
20
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { PlanApply } from '../apply.js';
|
|
2
2
|
import type { DeclaredOrg, OrgId } from '../org.js';
|
|
3
|
-
import type { Plan } from '../plan.js';
|
|
3
|
+
import type { Plan, PlanStep } from '../plan.js';
|
|
4
|
+
import type { DeclaredPlatform } from '../platform.js';
|
|
4
5
|
import type { AppliedState } from '../state.js';
|
|
5
|
-
import type { DeclaredOrgStore } from '../store.js';
|
|
6
|
+
import type { DeclaredOrgStore, PlatformStore } from '../store.js';
|
|
7
|
+
import { type AgoraHostPort } from './agora.js';
|
|
6
8
|
import { type StepAction } from './diff.js';
|
|
7
|
-
import { type LiveReaders, type LiveResource } from './live.js';
|
|
9
|
+
import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
|
|
10
|
+
import { type HeldKeyResolver } from './platform-org.js';
|
|
11
|
+
import type { CallsSfuConnectionPort } from './platform-sfu.js';
|
|
8
12
|
/** Who did it, for `@wtfalch/audit`'s "who, when, the plan, the result" (#9, #19). */
|
|
9
13
|
export interface AuditActor {
|
|
10
14
|
readonly id: string;
|
|
@@ -27,6 +31,8 @@ export interface AuditLog {
|
|
|
27
31
|
actor: AuditActor;
|
|
28
32
|
orgId: OrgId;
|
|
29
33
|
plan: Plan;
|
|
34
|
+
/** `'platform'` for the shared platform's plan (#167), whose `orgId` is the owner org's; absent for an org's own. */
|
|
35
|
+
target?: 'platform';
|
|
30
36
|
at: Date;
|
|
31
37
|
}): Promise<void>;
|
|
32
38
|
recordApply(entry: {
|
|
@@ -35,6 +41,8 @@ export interface AuditLog {
|
|
|
35
41
|
plan: Plan;
|
|
36
42
|
outcome: 'applied' | 'refused' | 'failed';
|
|
37
43
|
reason?: string;
|
|
44
|
+
/** `'platform'` for the shared platform's apply (#167), whose `orgId` is the owner org's; absent for an org's own. */
|
|
45
|
+
target?: 'platform';
|
|
38
46
|
at: Date;
|
|
39
47
|
}): Promise<void>;
|
|
40
48
|
/** `FoundryPlanApply.destroy`'s own record -- no `Plan` involved, so this is its own entry shape rather than a reuse of `recordApply`'s. */
|
|
@@ -61,6 +69,13 @@ export interface AuthzGate {
|
|
|
61
69
|
* own permission rather than reusing `canApply`.
|
|
62
70
|
*/
|
|
63
71
|
canDestroy(actor: AuditActor, orgId: OrgId): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Gates `applyPlatform` (#167): changing the shared platform every org
|
|
74
|
+
* runs on is a stronger action than applying one org, so it is its own
|
|
75
|
+
* permission. Optional so an existing host that only gates orgs keeps
|
|
76
|
+
* compiling; a gate without it refuses every platform apply.
|
|
77
|
+
*/
|
|
78
|
+
canApplyPlatform?(actor: AuditActor): Promise<boolean>;
|
|
64
79
|
}
|
|
65
80
|
/**
|
|
66
81
|
* What {@link StepExecutor.execute} needs beyond the {@link StepAction}
|
|
@@ -108,6 +123,13 @@ export interface StepExecutionContext {
|
|
|
108
123
|
* scope key can never collide with the `system:kind:key` form.
|
|
109
124
|
*/
|
|
110
125
|
recordScopeId(key: string, id: string): void;
|
|
126
|
+
/**
|
|
127
|
+
* Writes every id recorded so far to the store now, not only at the end of
|
|
128
|
+
* the run. For an id that must survive a crash mid-step: the ai key's id
|
|
129
|
+
* between its mint and its delivery. The end-of-run write is a superset of
|
|
130
|
+
* this one, since both write the same map.
|
|
131
|
+
*/
|
|
132
|
+
persistScopeIds(): Promise<void>;
|
|
111
133
|
}
|
|
112
134
|
/**
|
|
113
135
|
* Makes one {@link StepAction} true against a real system. No implementation
|
|
@@ -121,6 +143,58 @@ export interface StepExecutor {
|
|
|
121
143
|
resourceId?: string;
|
|
122
144
|
} | undefined>;
|
|
123
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* #167: {@link StepExecutionContext}'s platform-scoped counterpart -- what
|
|
148
|
+
* {@link PlatformStepExecutor.execute} needs beyond the {@link StepAction}
|
|
149
|
+
* itself, for the shared platform (`platform.ts`'s `DeclaredPlatform`) rather
|
|
150
|
+
* than one `DeclaredOrg`. A separate type rather than a reuse of
|
|
151
|
+
* `StepExecutionContext`: that type's `org` field is read directly by every
|
|
152
|
+
* real executor method in `executor.ts`, so widening it to also carry a
|
|
153
|
+
* platform would touch that whole file, which is out of scope here -- #167
|
|
154
|
+
* ships the plan/apply frame only, proven against fakes, the same way #19
|
|
155
|
+
* shipped org plan/apply before #20 wired `FoundryStepExecutor` against it.
|
|
156
|
+
*/
|
|
157
|
+
export interface PlatformStepExecutionContext {
|
|
158
|
+
readonly platform: DeclaredPlatform;
|
|
159
|
+
/** {@link StepExecutionContext.recordedIds}, for the platform's own run. */
|
|
160
|
+
recordedIds(): Readonly<Record<string, string>>;
|
|
161
|
+
/** {@link StepExecutionContext.recordScopeId}, for the platform's own run. */
|
|
162
|
+
recordScopeId(key: string, id: string): void;
|
|
163
|
+
/**
|
|
164
|
+
* Writes every id recorded so far to the platform's applied state now,
|
|
165
|
+
* rather than when the run ends or throws, so a crash between two calls
|
|
166
|
+
* cannot lose an id a provider already created. Optional: a caller that
|
|
167
|
+
* builds its own context (tests) may omit it.
|
|
168
|
+
*/
|
|
169
|
+
persist?(): Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
/** {@link StepExecutor}, for the platform (#167) rather than an org. */
|
|
172
|
+
export interface PlatformStepExecutor {
|
|
173
|
+
/**
|
|
174
|
+
* Steps this executor cannot run, as fixed text (part of the stale-plan
|
|
175
|
+
* comparison, so it must not vary per call). The planner reports each as an
|
|
176
|
+
* unmanaged item and plans no step for it, the way a blocked Agora is
|
|
177
|
+
* reported, instead of planning a step that throws on every apply.
|
|
178
|
+
*/
|
|
179
|
+
blockedSteps?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<{
|
|
180
|
+
auth?: string;
|
|
181
|
+
files?: string;
|
|
182
|
+
integrations?: string;
|
|
183
|
+
ai?: string;
|
|
184
|
+
archon?: string;
|
|
185
|
+
mail?: string;
|
|
186
|
+
}>;
|
|
187
|
+
/**
|
|
188
|
+
* Steps whose live state differs from the declared one in a way the recorded
|
|
189
|
+
* ids cannot show, read from the live system on each plan.
|
|
190
|
+
*/
|
|
191
|
+
driftedSteps?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<{
|
|
192
|
+
archon?: boolean;
|
|
193
|
+
}>;
|
|
194
|
+
execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
|
|
195
|
+
resourceId?: string;
|
|
196
|
+
} | undefined>;
|
|
197
|
+
}
|
|
124
198
|
/**
|
|
125
199
|
* One resource `FoundryPlanApply.destroy` has decided to tear down: an entry
|
|
126
200
|
* from `AppliedState.serviceIds` (`resourceKey`'s `system:kind:key` form, or
|
|
@@ -168,6 +242,22 @@ export declare class NotAuthorizedError extends Error {
|
|
|
168
242
|
export declare class NotATestOrgError extends Error {
|
|
169
243
|
constructor(orgId: OrgId);
|
|
170
244
|
}
|
|
245
|
+
/** #167: {@link DeclaredOrgNotFoundError}, for the platform's own singleton record (#166). */
|
|
246
|
+
export declare class DeclaredPlatformNotFoundError extends Error {
|
|
247
|
+
constructor();
|
|
248
|
+
}
|
|
249
|
+
/** #167: {@link StalePlanError}, for `planPlatform`/`applyPlatform`. */
|
|
250
|
+
export declare class PlatformStalePlanError extends Error {
|
|
251
|
+
constructor();
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* #167: {@link ConfirmationRequiredError}, for `applyPlatform` -- typed as
|
|
255
|
+
* the literal word `platform` rather than a declared name, since
|
|
256
|
+
* {@link DeclaredPlatform} has no `displayName` of its own to type (#166).
|
|
257
|
+
*/
|
|
258
|
+
export declare class PlatformConfirmationRequiredError extends Error {
|
|
259
|
+
constructor();
|
|
260
|
+
}
|
|
171
261
|
export interface FoundryPlanApplyOptions {
|
|
172
262
|
store: DeclaredOrgStore;
|
|
173
263
|
readers: LiveReaders;
|
|
@@ -177,8 +267,51 @@ export interface FoundryPlanApplyOptions {
|
|
|
177
267
|
authz: AuthzGate;
|
|
178
268
|
/** The operator running this plan/apply — every audit row's "who" (#9, #19). */
|
|
179
269
|
actor: AuditActor;
|
|
270
|
+
/** Host-supplied Agora values (#164). Optional: without it, or if it throws, the Agora is reported blocked and every other step still plans. */
|
|
271
|
+
agora?: AgoraHostPort;
|
|
272
|
+
/**
|
|
273
|
+
* #173: true when the host's executor was built with a `jobsScheduler`
|
|
274
|
+
* port. Optional, and false without it: each hosted org's registration with
|
|
275
|
+
* the jobs scheduler is then reported blocked instead of planned, so the
|
|
276
|
+
* step never runs (and fails) where no scheduler is deployed.
|
|
277
|
+
*/
|
|
278
|
+
jobsScheduler?: boolean;
|
|
279
|
+
/**
|
|
280
|
+
* #177: true when the host's executor was built with its own `filesOrganisation`
|
|
281
|
+
* / `aiOrganisation` port. Without one, the org step needs the platform's
|
|
282
|
+
* key, so it is blocked while `platform.heldKeys` is absent or does not hold it.
|
|
283
|
+
*/
|
|
284
|
+
filesOrganisation?: boolean;
|
|
285
|
+
aiOrganisation?: boolean;
|
|
180
286
|
/** Overridable for tests; defaults to `() => new Date()`. */
|
|
181
287
|
now?: () => Date;
|
|
288
|
+
/**
|
|
289
|
+
* #167: dependencies for planning/applying the shared platform (#166)
|
|
290
|
+
* alongside an org. Optional so an org-only caller — every #19-era caller,
|
|
291
|
+
* including every existing org test in this file — is unaffected;
|
|
292
|
+
* `planPlatform`/`applyPlatform` throw a plain `Error` if called without
|
|
293
|
+
* this set.
|
|
294
|
+
*/
|
|
295
|
+
platform?: {
|
|
296
|
+
store: PlatformStore;
|
|
297
|
+
readers: PlatformLiveReaders;
|
|
298
|
+
executor: PlatformStepExecutor;
|
|
299
|
+
/**
|
|
300
|
+
* #170: read-only look at the held SFU connection. When the platform
|
|
301
|
+
* declares the SFU but this is absent, or resolves `null`, the plan
|
|
302
|
+
* reports the SFU as blocked (in `unmanaged`) instead of planning a step
|
|
303
|
+
* that would throw and halt every step after it.
|
|
304
|
+
*/
|
|
305
|
+
sfuConnection?: CallsSfuConnectionPort;
|
|
306
|
+
/**
|
|
307
|
+
* #177: Archon's held keys. An org plan resolves the platform's files,
|
|
308
|
+
* AI and jobs keys through it and blocks the org step (naming the ref,
|
|
309
|
+
* never a value) while one is not held. Absent: every service without a
|
|
310
|
+
* host-supplied port is blocked (`filesOrganisation`, `aiOrganisation`,
|
|
311
|
+
* `jobsScheduler` on the options), never planned to fail at apply.
|
|
312
|
+
*/
|
|
313
|
+
heldKeys?: HeldKeyResolver;
|
|
314
|
+
};
|
|
182
315
|
}
|
|
183
316
|
/**
|
|
184
317
|
* The #19 implementation of {@link PlanApply}: computes a plan against live
|
|
@@ -194,10 +327,30 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
194
327
|
private readonly audit;
|
|
195
328
|
private readonly authz;
|
|
196
329
|
private readonly actor;
|
|
330
|
+
private readonly agora;
|
|
331
|
+
private readonly jobsScheduler;
|
|
332
|
+
private readonly hostFiles;
|
|
333
|
+
private readonly hostAi;
|
|
197
334
|
private readonly now;
|
|
335
|
+
private readonly platformDeps;
|
|
198
336
|
constructor(options: FoundryPlanApplyOptions);
|
|
199
337
|
plan(id: OrgId): Promise<Plan>;
|
|
200
338
|
apply(plan: Plan, confirmedOrgName: string): Promise<AppliedState>;
|
|
339
|
+
/**
|
|
340
|
+
* #167: {@link plan}, for the shared platform (#166) rather than an org --
|
|
341
|
+
* there is no id to pass, since `DeclaredPlatform` is one singleton record.
|
|
342
|
+
*/
|
|
343
|
+
planPlatform(): Promise<Plan>;
|
|
344
|
+
/**
|
|
345
|
+
* #167: {@link apply}, for the platform. `plan.orgId` is the platform's own
|
|
346
|
+
* `ownerOrgId` (`toPlatformPlan`'s own doc comment), so the same
|
|
347
|
+
* `AuditLog`/`AuthzGate` seams `apply` uses need no platform-specific
|
|
348
|
+
* shape of their own. `confirmedName` must be the literal word `platform`
|
|
349
|
+
* rather than a declared name -- `DeclaredPlatform` has none.
|
|
350
|
+
*/
|
|
351
|
+
applyPlatform(plan: Plan, confirmedName: string): Promise<{
|
|
352
|
+
serviceIds: Record<string, string>;
|
|
353
|
+
}>;
|
|
201
354
|
/**
|
|
202
355
|
* Deletes every resource foundry recorded for `id`, in reverse creation
|
|
203
356
|
* order, then the declared org and its applied state themselves (goal
|
|
@@ -211,7 +364,43 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
211
364
|
*/
|
|
212
365
|
destroy(id: OrgId, confirmedOrgName: string): Promise<void>;
|
|
213
366
|
private refuse;
|
|
367
|
+
private refusePlatform;
|
|
214
368
|
private refuseDestroy;
|
|
215
369
|
private runSteps;
|
|
216
370
|
private computeFreshPlan;
|
|
371
|
+
/**
|
|
372
|
+
* #177: an org's plan reads the shared platform's own addresses and
|
|
373
|
+
* readiness through the very `platform` deps `planPlatform`/`applyPlatform`
|
|
374
|
+
* already use. With no platform wired or declared, {@link
|
|
375
|
+
* DEFAULT_ORG_PLATFORM_READINESS} reports every service not ready, so every
|
|
376
|
+
* org step that needs one is blocked.
|
|
377
|
+
*/
|
|
378
|
+
private orgPlatformReadiness;
|
|
379
|
+
/**
|
|
380
|
+
* #177: `AgoraHostValues` from the applied platform, the same
|
|
381
|
+
* `platformDeps` {@link orgPlatformReadiness} reads -- but only when the
|
|
382
|
+
* host supplies no `agora` port of its own. An explicit `agora` (#164)
|
|
383
|
+
* always wins, unchanged, so a host that already has its own values
|
|
384
|
+
* (perhaps not yet wired to this platform) keeps them.
|
|
385
|
+
*/
|
|
386
|
+
private orgAgoraHost;
|
|
387
|
+
/** #167: `FoundryPlanApplyOptions.platform`, or a plain `Error` naming what's missing. */
|
|
388
|
+
private requirePlatformDeps;
|
|
389
|
+
private computeFreshPlatformPlan;
|
|
390
|
+
private sfuBlockedReason;
|
|
391
|
+
/**
|
|
392
|
+
* {@link runSteps}, for the platform. The ids the steps return are recorded
|
|
393
|
+
* in `PlatformStore`'s applied state (`system:kind:key`, like an org's
|
|
394
|
+
* `serviceIds`), so a later plan finds a resource by its recorded id even
|
|
395
|
+
* when a provider's list has not caught up with a create.
|
|
396
|
+
*/
|
|
397
|
+
private runPlatformSteps;
|
|
217
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* #9 (resolved): whether any step in a plan needs its name typed before
|
|
401
|
+
* `apply` runs it. Shared by `apply` (the org's `displayName`) and
|
|
402
|
+
* `applyPlatform` (the literal word `platform`, #167) so the platform gets
|
|
403
|
+
* the identical gate an org already has, proven end-to-end above, rather
|
|
404
|
+
* than a second copy that could drift from it.
|
|
405
|
+
*/
|
|
406
|
+
export declare function planNeedsConfirmation(steps: readonly PlanStep[]): boolean;
|