@wtfalch/foundry 0.16.16 → 0.17.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/apply.d.ts +22 -1
- package/dist/bootstrap/bootstrap.d.ts +6 -1
- package/dist/bootstrap/bootstrap.js +26 -3
- package/dist/bootstrap/declare.d.ts +11 -1
- package/dist/bootstrap/declare.js +12 -2
- package/dist/index.d.ts +9 -4
- package/dist/index.js +7 -0
- package/dist/migrations/0038_calls_version_pin.sql +8 -0
- package/dist/migrations/0039_destroy_record.sql +7 -0
- package/dist/migrations/0040_company_included_uuid.sql +72 -0
- package/dist/migrations/0041_company_included_uuid_keys.sql +132 -0
- package/dist/migrations/0042_mutation_leases.sql +12 -0
- package/dist/org.d.ts +54 -0
- package/dist/org.js +43 -1
- package/dist/plan/adopt.d.ts +76 -0
- package/dist/plan/adopt.js +184 -0
- package/dist/plan/agora.d.ts +21 -0
- package/dist/plan/agora.js +37 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +13 -5
- package/dist/plan/chat-matrix.d.ts +1 -1
- package/dist/plan/chat-matrix.js +11 -3
- package/dist/plan/core-files.d.ts +25 -0
- package/dist/plan/core-files.js +25 -0
- package/dist/plan/diff.d.ts +11 -3
- package/dist/plan/diff.js +27 -62
- package/dist/plan/entry-ai-key.d.ts +3 -3
- package/dist/plan/entry-files-key.d.ts +3 -3
- package/dist/plan/executor.d.ts +57 -23
- package/dist/plan/executor.js +134 -50
- package/dist/plan/index.d.ts +4 -0
- package/dist/plan/index.js +2 -0
- package/dist/plan/live.d.ts +5 -0
- package/dist/plan/login-host.d.ts +1 -1
- package/dist/plan/login-host.js +6 -5
- package/dist/plan/plan-apply.d.ts +139 -6
- package/dist/plan/plan-apply.js +546 -76
- package/dist/plan/platform-diff.d.ts +14 -0
- package/dist/plan/platform-diff.js +16 -1
- package/dist/plan/platform-org.d.ts +16 -0
- package/dist/plan/platform-org.js +17 -1
- package/dist/plan/platform-providers.d.ts +1 -1
- package/dist/plan/platform-providers.js +73 -11
- package/dist/plan/providers-readers.d.ts +2 -0
- package/dist/plan/providers-readers.js +4 -0
- package/dist/plan/rollout.d.ts +52 -0
- package/dist/plan/rollout.js +37 -0
- package/dist/plan/step-system.d.ts +22 -0
- package/dist/plan/step-system.js +37 -0
- package/dist/plan.d.ts +3 -0
- package/dist/state.d.ts +13 -0
- package/dist/store/audit.js +15 -1
- package/dist/store/postgres.d.ts +1 -1
- package/dist/store/postgres.js +127 -14
- package/dist/store/schema.d.ts +134 -5
- package/dist/store/schema.js +12 -1
- package/dist/store.d.ts +47 -4
- package/dist/store.js +9 -1
- package/package.json +1 -1
|
@@ -309,6 +309,20 @@ export declare const PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY = "coolify_platform
|
|
|
309
309
|
export declare const PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF = "platform:master:coolify-url";
|
|
310
310
|
export declare const PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF = "platform:master:coolify-token";
|
|
311
311
|
export declare const PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF = "platform:master:cloudflare-token";
|
|
312
|
+
/**
|
|
313
|
+
* #436: a Coolify API token scoped to `read` only, for Archon's own
|
|
314
|
+
* `/backups` page (`COOLIFY_URL`/`COOLIFY_READ_TOKEN`, Archon's
|
|
315
|
+
* `src/lib/backups/coolify.ts`) -- deliberately not
|
|
316
|
+
* {@link PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF}'s root token, which a
|
|
317
|
+
* read-only status page must never hold. Coolify's token UI has no API
|
|
318
|
+
* counterpart to create one (`@wtfalch/providers`'s `CoolifyClient` doc:
|
|
319
|
+
* "Keys & Tokens -> API tokens" only), so a human creates it in Coolify's
|
|
320
|
+
* console and holds it under this ref; `provisionPlatformArchon` only reads
|
|
321
|
+
* it back, and pushes nothing until it is held. `COOLIFY_URL` needs no ref
|
|
322
|
+
* of its own: it is not secret, and {@link PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF}
|
|
323
|
+
* already holds the same panel's address.
|
|
324
|
+
*/
|
|
325
|
+
export declare const PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF = "platform:archon:coolify-read-token";
|
|
312
326
|
/**
|
|
313
327
|
* #169: `platform:<service>` for the platform's own mail server (Stalwart),
|
|
314
328
|
* mirroring {@link PLATFORM_AUTH_KEY} exactly.
|
|
@@ -2,6 +2,7 @@ import { VERSIONS } from '../bootstrap/versions.js';
|
|
|
2
2
|
import { APP_ENTRY_KEY } from '../org.js';
|
|
3
3
|
import { VERSION_CHANGE_KIND, } from '../plan.js';
|
|
4
4
|
import { legacyAppHostname } from './hostnames.js';
|
|
5
|
+
import { withStepSystem } from './step-system.js';
|
|
5
6
|
/**
|
|
6
7
|
* The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
|
|
7
8
|
* mirroring `diff.ts`'s `toPlan`. There is no separate platform-shaped `Plan`
|
|
@@ -343,6 +344,20 @@ export const PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY = 'coolify_platform_archon_
|
|
|
343
344
|
export const PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF = 'platform:master:coolify-url';
|
|
344
345
|
export const PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF = 'platform:master:coolify-token';
|
|
345
346
|
export const PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF = 'platform:master:cloudflare-token';
|
|
347
|
+
/**
|
|
348
|
+
* #436: a Coolify API token scoped to `read` only, for Archon's own
|
|
349
|
+
* `/backups` page (`COOLIFY_URL`/`COOLIFY_READ_TOKEN`, Archon's
|
|
350
|
+
* `src/lib/backups/coolify.ts`) -- deliberately not
|
|
351
|
+
* {@link PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF}'s root token, which a
|
|
352
|
+
* read-only status page must never hold. Coolify's token UI has no API
|
|
353
|
+
* counterpart to create one (`@wtfalch/providers`'s `CoolifyClient` doc:
|
|
354
|
+
* "Keys & Tokens -> API tokens" only), so a human creates it in Coolify's
|
|
355
|
+
* console and holds it under this ref; `provisionPlatformArchon` only reads
|
|
356
|
+
* it back, and pushes nothing until it is held. `COOLIFY_URL` needs no ref
|
|
357
|
+
* of its own: it is not secret, and {@link PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF}
|
|
358
|
+
* already holds the same panel's address.
|
|
359
|
+
*/
|
|
360
|
+
export const PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF = 'platform:archon:coolify-read-token';
|
|
346
361
|
/**
|
|
347
362
|
* #169: `platform:<service>` for the platform's own mail server (Stalwart),
|
|
348
363
|
* mirroring {@link PLATFORM_AUTH_KEY} exactly.
|
|
@@ -777,7 +792,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
777
792
|
});
|
|
778
793
|
}
|
|
779
794
|
unmanaged.push(...backups.blocked);
|
|
780
|
-
return { plannedSteps, unmanaged, versionChanges };
|
|
795
|
+
return { plannedSteps: plannedSteps.map(withStepSystem), unmanaged, versionChanges };
|
|
781
796
|
}
|
|
782
797
|
/**
|
|
783
798
|
* The platform's own Cloudflare zone for its shared apex (`platform.ts`'s
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { DeclaredPlatform } from '../platform.js';
|
|
2
2
|
import type { AgoraHostValues } from './agora.js';
|
|
3
|
+
/**
|
|
4
|
+
* #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
|
|
5
|
+
* fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
|
|
6
|
+
* `bouleHostnameFor` and `hostnames.ts`'s own `<slug>.oikos.host` already
|
|
7
|
+
* use for a level-1 org's hostnames. `OrgPlatformReadiness.mailHost.apex`
|
|
8
|
+
* used to be `platform.apex` (the platform's own brand apex, e.g.
|
|
9
|
+
* `companyincluded.com`), which gave a domainless org `slug@companyincluded.com`
|
|
10
|
+
* instead of the settled `slug@oikos.host` contract
|
|
11
|
+
* (`@wtfalch/email`'s `org-mail.ts`'s `resolveDomain` is the only reader of
|
|
12
|
+
* this `apex`, for a level-1 org alone -- a with-domain org returns
|
|
13
|
+
* `org.domain` and never looks at it). `mailHost` itself (the MX server's own
|
|
14
|
+
* hostname, `platformMailMxHost(platform.apex)`) is unaffected: the shared
|
|
15
|
+
* mail server stays the platform's own, whichever apex a mailbox's address
|
|
16
|
+
* uses.
|
|
17
|
+
*/
|
|
18
|
+
export declare const NO_DOMAIN_MAIL_APEX = "oikos.host";
|
|
3
19
|
/**
|
|
4
20
|
* https://github.com/wtfalch/foundry/issues/177: what an org step needs to
|
|
5
21
|
* know about the platform it runs on -- whether each shared service it
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { JOBS_SCHEDULER_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
|
|
2
2
|
import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './platform-providers.js';
|
|
3
|
+
/**
|
|
4
|
+
* #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
|
|
5
|
+
* fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
|
|
6
|
+
* `bouleHostnameFor` and `hostnames.ts`'s own `<slug>.oikos.host` already
|
|
7
|
+
* use for a level-1 org's hostnames. `OrgPlatformReadiness.mailHost.apex`
|
|
8
|
+
* used to be `platform.apex` (the platform's own brand apex, e.g.
|
|
9
|
+
* `companyincluded.com`), which gave a domainless org `slug@companyincluded.com`
|
|
10
|
+
* instead of the settled `slug@oikos.host` contract
|
|
11
|
+
* (`@wtfalch/email`'s `org-mail.ts`'s `resolveDomain` is the only reader of
|
|
12
|
+
* this `apex`, for a level-1 org alone -- a with-domain org returns
|
|
13
|
+
* `org.domain` and never looks at it). `mailHost` itself (the MX server's own
|
|
14
|
+
* hostname, `platformMailMxHost(platform.apex)`) is unaffected: the shared
|
|
15
|
+
* mail server stays the platform's own, whichever apex a mailbox's address
|
|
16
|
+
* uses.
|
|
17
|
+
*/
|
|
18
|
+
export const NO_DOMAIN_MAIL_APEX = 'oikos.host';
|
|
3
19
|
/**
|
|
4
20
|
* What an org's plan reads when no platform is declared: nothing is ready, so
|
|
5
21
|
* every org step that depends on a platform service is blocked (#177). A host
|
|
@@ -73,7 +89,7 @@ noKeyReader = false) {
|
|
|
73
89
|
};
|
|
74
90
|
return {
|
|
75
91
|
mailHost: mailApplied
|
|
76
|
-
? { apex:
|
|
92
|
+
? { apex: NO_DOMAIN_MAIL_APEX, mailHost: platformMailMxHost(platform.apex) }
|
|
77
93
|
: null,
|
|
78
94
|
filesReady: filesApplied && missingKeys.files === null,
|
|
79
95
|
filesUrl: filesApplied
|
|
@@ -409,7 +409,7 @@ export interface PlatformMailRuntime {
|
|
|
409
409
|
* {@link PlatformStepExecutionContext}, bundled like {@link PlatformAuthDeps}.
|
|
410
410
|
*/
|
|
411
411
|
export interface PlatformArchonDeps {
|
|
412
|
-
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
|
|
412
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'deleteEnv' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
|
|
413
413
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord' | 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
|
|
414
414
|
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
415
415
|
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp'>;
|
|
@@ -7,7 +7,7 @@ import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
|
7
7
|
import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
|
|
8
8
|
import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
|
|
9
9
|
import { coolifyDomains, filesShareHostname } from './hostnames.js';
|
|
10
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, archonBootstrapOperator, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
|
|
10
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, archonBootstrapOperator, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
|
|
11
11
|
import { applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
12
12
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
13
13
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
@@ -2365,7 +2365,15 @@ export const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = 'zitadel_platform_archon_clie
|
|
|
2365
2365
|
* does not parse fails the step without printing it.
|
|
2366
2366
|
* - For each of files, ai, integrations and jobs that the platform declares
|
|
2367
2367
|
* enabled: its `https://<hostname>` URL, and its management key when
|
|
2368
|
-
* {@link PlatformArchonDeps.heldKeys} holds one.
|
|
2368
|
+
* {@link PlatformArchonDeps.heldKeys} holds one. Files also gets
|
|
2369
|
+
* `FILES_ORGANISATION_ID` (`platform.ownerOrgId`, Company Included's own
|
|
2370
|
+
* Files organisation -- `executor.ts`'s `filesOrganisation` provisions it
|
|
2371
|
+
* under that same id) and its signed-assertion identity
|
|
2372
|
+
* (`FILES_MANAGEMENT_ISSUER`, `_KEY_ID`, `_PRIVATE_KEY`; #436).
|
|
2373
|
+
* - `COOLIFY_URL`/`COOLIFY_READ_TOKEN` for `/backups`, once a human has
|
|
2374
|
+
* created a Coolify token scoped to read only and held it under
|
|
2375
|
+
* {@link PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF} -- that ref's own doc
|
|
2376
|
+
* comment says why foundry cannot mint one itself (#436).
|
|
2369
2377
|
*
|
|
2370
2378
|
* The OIDC client is ZITADEL's public web client (authorization code with
|
|
2371
2379
|
* PKCE, no client secret: `createOidcApp` never returns one, see
|
|
@@ -2397,10 +2405,7 @@ export const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = 'zitadel_platform_archon_clie
|
|
|
2397
2405
|
* steps are exported by those steps' own modules. A
|
|
2398
2406
|
* missing one blocks the plan (`blockedSteps`); it is never skipped. The
|
|
2399
2407
|
* env names `AI_*`, `INTEGRATIONS_*`, `JOBS_URL` and `JOBS_ADMIN_TOKEN` are
|
|
2400
|
-
* this step's proposal
|
|
2401
|
-
* `FILES_MANAGEMENT_KEY` of these today. Files' signed-assertion identity
|
|
2402
|
-
* (`FILES_MANAGEMENT_ISSUER`, `_KEY_ID`, `_PRIVATE_KEY`, `_SCOPE`) and
|
|
2403
|
-
* `FILES_ORGANISATION_ID` are not wired.
|
|
2408
|
+
* this step's proposal.
|
|
2404
2409
|
* - The step plans again when the hostname, `gitRef` or the Access policy's
|
|
2405
2410
|
* include list changes. Env for a service enabled later is not pushed until
|
|
2406
2411
|
* one of those changes.
|
|
@@ -2630,8 +2635,44 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
2630
2635
|
// unflagged, Coolify writes it into `.env` unquoted and the container never
|
|
2631
2636
|
// starts.
|
|
2632
2637
|
envs.push(...inputs.envs.map((e) => (e.value.includes('\n') ? { ...e, multiline: true } : e)));
|
|
2638
|
+
// #436: /backups needs a Coolify token scoped to read only, never the
|
|
2639
|
+
// root token `putArchonMasterKeys` above already held. Nothing mints that
|
|
2640
|
+
// token over Coolify's API (this ref's own doc comment), so until a human
|
|
2641
|
+
// creates one and holds it under PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF,
|
|
2642
|
+
// this pushes nothing and /backups keeps showing its own "set COOLIFY_URL
|
|
2643
|
+
// and COOLIFY_READ_TOKEN" message -- never a reason to block the rest of
|
|
2644
|
+
// this step.
|
|
2645
|
+
const coolifyReadToken = await deps.heldKeys.resolve(PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF);
|
|
2646
|
+
if (coolifyReadToken) {
|
|
2647
|
+
const coolifyUrl = await deps.heldKeys.resolve(PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF);
|
|
2648
|
+
if (coolifyUrl) {
|
|
2649
|
+
envs.push({ key: 'COOLIFY_URL', value: coolifyUrl }, { key: 'COOLIFY_READ_TOKEN', value: coolifyReadToken });
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2633
2652
|
const changed = await envsDiffer(deps.coolify, appId, envs);
|
|
2634
2653
|
await deps.coolify.bulkUpsertEnvs(appId, envs);
|
|
2654
|
+
// #436 round 2: `bulkUpsertEnvs` only adds or updates, so the
|
|
2655
|
+
// `FILES_MANAGEMENT_KEY` this step used to write before the fix above
|
|
2656
|
+
// would otherwise survive on an Archon already deployed with it --
|
|
2657
|
+
// `filesManagementFetch` checks that var first, so a stale copy kept
|
|
2658
|
+
// steering it into the issued-key path this issue's wiring fixes, and it
|
|
2659
|
+
// left a second copy of the private key sitting in Coolify. Every row
|
|
2660
|
+
// under this key, including `listEnvs`'s own preview duplicate, is
|
|
2661
|
+
// deleted outright -- never overwritten, which would still leave a row.
|
|
2662
|
+
//
|
|
2663
|
+
// #462: the deletes happen after `changed` is already computed above, so
|
|
2664
|
+
// a retry that finds the envs already matching (`changed` false) but a
|
|
2665
|
+
// stale `FILES_MANAGEMENT_KEY` row still present must still redeploy --
|
|
2666
|
+
// the running container otherwise keeps reading the stale key out of its
|
|
2667
|
+
// own process env until someone redeploys by hand. `staleKeyDeleted`
|
|
2668
|
+
// feeds `archonDeployedNow` below for exactly that case.
|
|
2669
|
+
let staleKeyDeleted = false;
|
|
2670
|
+
for (const stale of await deps.coolify.listEnvs(appId)) {
|
|
2671
|
+
if (stale.key === 'FILES_MANAGEMENT_KEY' && stale.uuid) {
|
|
2672
|
+
await deps.coolify.deleteEnv(appId, stale.uuid);
|
|
2673
|
+
staleKeyDeleted = true;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2635
2676
|
// ---- register with the broker (#204) ---------------------------------------
|
|
2636
2677
|
// After Archon's own `AUTH_APP_KEY` is durably written above, so a throw
|
|
2637
2678
|
// earlier in this function (a bad database URL, a Cloudflare failure) never
|
|
@@ -2756,6 +2797,7 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
2756
2797
|
app.justCreated ||
|
|
2757
2798
|
!app.running ||
|
|
2758
2799
|
changed ||
|
|
2800
|
+
staleKeyDeleted ||
|
|
2759
2801
|
appliedGitRef !== gitRef ||
|
|
2760
2802
|
recorded[PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY] !== hostname;
|
|
2761
2803
|
if (archonDeployedNow) {
|
|
@@ -3309,11 +3351,31 @@ async function collectArchonServiceInputs(platform, deps, recorded) {
|
|
|
3309
3351
|
const held = (ref) => heldByRef.get(ref);
|
|
3310
3352
|
for (const service of services) {
|
|
3311
3353
|
if (service === 'files') {
|
|
3312
|
-
envs.push(
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3354
|
+
envs.push(
|
|
3355
|
+
// #436 round 2: not `PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY`'s
|
|
3356
|
+
// internal `http://<name>:3000` -- Archon's signed path needs an
|
|
3357
|
+
// exact HTTPS origin (`filesOrigin()`/`filesBaseUrl()`,
|
|
3358
|
+
// `wtfalch/archon`'s `src/lib/authz/files-management.ts` and
|
|
3359
|
+
// `src/lib/files-admin/client.ts`), and that origin is also the
|
|
3360
|
+
// signing audience, so it must equal Files' own `APP_URL` exactly
|
|
3361
|
+
// (this file, the files runner env above: `https://${platform.
|
|
3362
|
+
// files.hostname}`).
|
|
3363
|
+
{ key: 'FILES_URL', value: `https://${platform.files.hostname}` },
|
|
3364
|
+
// #436: Company Included's own Files organisation, provisioned by
|
|
3365
|
+
// the ordinary per-org `planFilesOrganisation` step under its own
|
|
3366
|
+
// org id -- `executor.ts`'s `filesOrganisation` calls
|
|
3367
|
+
// `provisionOrganisation({ organisationId: org.id, ... })`, so
|
|
3368
|
+
// `platform.ownerOrgId` is already that organisation's id, with no
|
|
3369
|
+
// separate platform-level mint needed.
|
|
3370
|
+
{ key: 'FILES_ORGANISATION_ID', value: platform.ownerOrgId },
|
|
3371
|
+
// #436: the signed-assertion identity `filesSigner()` reads
|
|
3372
|
+
// (`archon`'s `src/lib/authz/files-management.ts`), not the bearer
|
|
3373
|
+
// token `FILES_MANAGEMENT_KEY` used to name here -- that name made
|
|
3374
|
+
// Archon take the issued-key branch of `filesManagementFetch`,
|
|
3375
|
+
// which then failed on the `FILES_MANAGEMENT_SCOPE` this step never
|
|
3376
|
+
// set, surfacing as "Files is not configured" (foundry#436).
|
|
3377
|
+
{ key: 'FILES_MANAGEMENT_ISSUER', value: FILES_MANAGEMENT_ISSUER }, { key: 'FILES_MANAGEMENT_KEY_ID', value: FILES_MANAGEMENT_KEY_ID }, {
|
|
3378
|
+
key: 'FILES_MANAGEMENT_PRIVATE_KEY',
|
|
3317
3379
|
value: held(ARCHON_SERVICE_INPUTS.files.refs[0]),
|
|
3318
3380
|
});
|
|
3319
3381
|
}
|
|
@@ -94,6 +94,8 @@ export declare class ProvidersCoolifyReader implements CoolifyReader {
|
|
|
94
94
|
private readonly resolveRecordedIds;
|
|
95
95
|
constructor(client: CoolifyClient, resolveScope: ScopeResolver, resolveRecordedIds: RecordedIds);
|
|
96
96
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
97
|
+
/** The project's resources as Coolify reports them, unmapped: what adopt checks against a declaration. */
|
|
98
|
+
listProject(projectUuid: string): Promise<LiveResource[]>;
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
99
101
|
* Backed by `@wtfalch/providers/cloudflare`'s `CloudflareClient.listResources`:
|
|
@@ -243,6 +243,10 @@ export class ProvidersCoolifyReader {
|
|
|
243
243
|
const withBouleConfig = await Promise.all(withAgoraConfig.map((r) => withBouleEnvConfig(this.client, r, recorded)));
|
|
244
244
|
return applyRecordedIds(withBouleConfig, recorded);
|
|
245
245
|
}
|
|
246
|
+
/** The project's resources as Coolify reports them, unmapped: what adopt checks against a declaration. */
|
|
247
|
+
async listProject(projectUuid) {
|
|
248
|
+
return toFoundryResources('coolify', await this.client.listResources(projectUuid));
|
|
249
|
+
}
|
|
246
250
|
}
|
|
247
251
|
/**
|
|
248
252
|
* Backed by `@wtfalch/providers/cloudflare`'s `CloudflareClient.listResources`:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { OrgId } from '../org.js';
|
|
2
|
+
/**
|
|
3
|
+
* #259: ordered, stop-on-failure rollout across several orgs' own applies.
|
|
4
|
+
*
|
|
5
|
+
* Where the order itself comes from is still open (#259 "Decide": the
|
|
6
|
+
* platform declaration, one place, vs. spread across each org) -- this
|
|
7
|
+
* takes the order as a plain, caller-supplied list, so it works unchanged
|
|
8
|
+
* whichever way that is answered. `apply` is the caller's own one-org apply
|
|
9
|
+
* (typically `plan` then `PlanApply.apply`, confirmation and all) -- this
|
|
10
|
+
* file knows nothing about that shape, only whether it threw.
|
|
11
|
+
*
|
|
12
|
+
* "Stop at the first failed health check" (the issue's own words) is, today,
|
|
13
|
+
* "stop at the first `apply` that throws" -- the shared health helper
|
|
14
|
+
* (`deploy-health.ts`'s `waitForNewDeployment`) is not yet wired into the
|
|
15
|
+
* org-level calls stack deploy (`executor.ts`'s `coolifyCalls`/
|
|
16
|
+
* `coolifyCallsService` only ever fire-and-forget deploy, and never patch a
|
|
17
|
+
* running app's image tag at all -- see `org.ts`'s `CallsConfig.versionPin`
|
|
18
|
+
* doc comment). Once that step itself throws on a failed health check, a
|
|
19
|
+
* caller's `apply` naturally does too, and this orchestration needs no
|
|
20
|
+
* change.
|
|
21
|
+
*/
|
|
22
|
+
/** One org's outcome from {@link applyOrgsInOrder}. */
|
|
23
|
+
export interface RolloutOrgResult {
|
|
24
|
+
readonly orgId: OrgId;
|
|
25
|
+
readonly outcome: 'applied' | 'skipped' | 'failed';
|
|
26
|
+
/** Set only for `'failed'` -- the thrown error's own message. */
|
|
27
|
+
readonly error?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Set only for `'applied'`, when `soakMs` names one for this org: the ISO
|
|
30
|
+
* time the caller should wait until before applying the next org. Never
|
|
31
|
+
* slept here (CLAUDE.md "Platform steps" rule 5, and the issue's own "Make
|
|
32
|
+
* the soak a declared value the caller supplies, not a sleep inside
|
|
33
|
+
* foundry") -- a caller that wants to honour it re-invokes later, from
|
|
34
|
+
* `orgIds` sliced after this result's own org.
|
|
35
|
+
*/
|
|
36
|
+
readonly soakUntil?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface RolloutOptions {
|
|
39
|
+
/** This org's soak window in milliseconds, or `undefined`/`0` for none (a stateless service). */
|
|
40
|
+
soakMs?: (orgId: OrgId) => number | undefined;
|
|
41
|
+
/** Overridable for tests; defaults to `Date.now`. */
|
|
42
|
+
now?: () => number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Applies every org in `orgIds`, in order, stopping at the first `apply`
|
|
46
|
+
* that throws: every org after it is reported `'skipped'` without `apply`
|
|
47
|
+
* ever being called for it, so it is left exactly as it was. An org whose
|
|
48
|
+
* own `apply` throws is reported `'failed'` with that error's message --
|
|
49
|
+
* `apply`'s own implementation decides what, if anything, it recorded
|
|
50
|
+
* before throwing (this function never inspects or rolls back that state).
|
|
51
|
+
*/
|
|
52
|
+
export declare function applyOrgsInOrder(orgIds: readonly OrgId[], apply: (orgId: OrgId) => Promise<unknown>, options?: RolloutOptions): Promise<RolloutOrgResult[]>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies every org in `orgIds`, in order, stopping at the first `apply`
|
|
3
|
+
* that throws: every org after it is reported `'skipped'` without `apply`
|
|
4
|
+
* ever being called for it, so it is left exactly as it was. An org whose
|
|
5
|
+
* own `apply` throws is reported `'failed'` with that error's message --
|
|
6
|
+
* `apply`'s own implementation decides what, if anything, it recorded
|
|
7
|
+
* before throwing (this function never inspects or rolls back that state).
|
|
8
|
+
*/
|
|
9
|
+
export async function applyOrgsInOrder(orgIds, apply, options = {}) {
|
|
10
|
+
const now = options.now ?? Date.now;
|
|
11
|
+
const results = [];
|
|
12
|
+
let stopped = false;
|
|
13
|
+
for (const orgId of orgIds) {
|
|
14
|
+
if (stopped) {
|
|
15
|
+
results.push({ orgId, outcome: 'skipped' });
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await apply(orgId);
|
|
20
|
+
const soak = options.soakMs?.(orgId);
|
|
21
|
+
results.push({
|
|
22
|
+
orgId,
|
|
23
|
+
outcome: 'applied',
|
|
24
|
+
...(soak ? { soakUntil: new Date(now() + soak).toISOString() } : {}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
results.push({
|
|
29
|
+
orgId,
|
|
30
|
+
outcome: 'failed',
|
|
31
|
+
error: error instanceof Error ? error.message : String(error),
|
|
32
|
+
});
|
|
33
|
+
stopped = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PlanStep } from '../plan.js';
|
|
2
|
+
import type { StepAction } from './diff.js';
|
|
3
|
+
import type { LiveResource } from './live.js';
|
|
4
|
+
/**
|
|
5
|
+
* The system a plan step or progress event touches, for Archon to draw as a
|
|
6
|
+
* tag. A superset of {@link LiveResource}'s `system` (where a resource
|
|
7
|
+
* lives): a step that deploys Stalwart, Synapse or a Postgres database
|
|
8
|
+
* through Coolify names that system, not the deployer.
|
|
9
|
+
*
|
|
10
|
+
* `foundry` is for a progress event that covers more than one system, or
|
|
11
|
+
* none (a round of mixed steps, the closing event). No plan step carries it.
|
|
12
|
+
*/
|
|
13
|
+
export type StepSystem = LiveResource['system'] | 'stalwart' | 'matrix' | 'postgres' | 'foundry';
|
|
14
|
+
/** The system `action` touches. The one place that decides it, so no planner sets it by hand. */
|
|
15
|
+
export declare function stepSystemOf(action: StepAction): StepSystem;
|
|
16
|
+
/** The step with its `system` set from `action`. */
|
|
17
|
+
export declare function withStepSystem<T extends {
|
|
18
|
+
step: Omit<PlanStep, 'system'>;
|
|
19
|
+
action: StepAction;
|
|
20
|
+
}>(planned: T): T & {
|
|
21
|
+
step: PlanStep;
|
|
22
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Which system a platform `app` step (keyed `platform:<service>`) runs. */
|
|
2
|
+
const PLATFORM_APP_SYSTEMS = {
|
|
3
|
+
'platform:auth': 'zitadel',
|
|
4
|
+
'platform:mail': 'stalwart',
|
|
5
|
+
'platform:files': 'files',
|
|
6
|
+
'platform:ai': 'ai',
|
|
7
|
+
};
|
|
8
|
+
/** The system `action` touches. The one place that decides it, so no planner sets it by hand. */
|
|
9
|
+
export function stepSystemOf(action) {
|
|
10
|
+
switch (action.kind) {
|
|
11
|
+
case 'mail':
|
|
12
|
+
case 'mail-copy':
|
|
13
|
+
return 'stalwart';
|
|
14
|
+
case 'chat':
|
|
15
|
+
case 'calls':
|
|
16
|
+
case 'calls-service':
|
|
17
|
+
return 'matrix';
|
|
18
|
+
case 'database':
|
|
19
|
+
case 'adopt-database':
|
|
20
|
+
case 'backup':
|
|
21
|
+
return 'postgres';
|
|
22
|
+
// Re-registers the R2 bucket's credentials with Coolify: the bucket is what it writes to.
|
|
23
|
+
case 'backup-storage':
|
|
24
|
+
return 'cloudflare';
|
|
25
|
+
// The platform's jobs scheduler app, like the org's own `jobs` registration.
|
|
26
|
+
case 'jobs':
|
|
27
|
+
return 'jobs';
|
|
28
|
+
case 'app':
|
|
29
|
+
return PLATFORM_APP_SYSTEMS[action.key] ?? action.system;
|
|
30
|
+
default:
|
|
31
|
+
return action.system;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** The step with its `system` set from `action`. */
|
|
35
|
+
export function withStepSystem(planned) {
|
|
36
|
+
return { ...planned, step: { ...planned.step, system: stepSystemOf(planned.action) } };
|
|
37
|
+
}
|
package/dist/plan.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OrgId } from './org.js';
|
|
2
|
+
import type { StepSystem } from './plan/step-system.js';
|
|
2
3
|
/**
|
|
3
4
|
* One step of a {@link Plan}.
|
|
4
5
|
*
|
|
@@ -9,6 +10,8 @@ import type { OrgId } from './org.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export interface PlanStep {
|
|
11
12
|
description: string;
|
|
13
|
+
/** The system this step touches, for Archon to tag it with. Set from the step's action, never by hand (`plan/step-system.ts`). */
|
|
14
|
+
system: StepSystem;
|
|
12
15
|
/**
|
|
13
16
|
* True when this step deletes or replaces data the org cannot get back
|
|
14
17
|
* without foundry's own backup: a chat re-key, an issuer move, a mail
|
package/dist/state.d.ts
CHANGED
|
@@ -12,6 +12,17 @@ export interface Checkpoint {
|
|
|
12
12
|
step: string;
|
|
13
13
|
completedAt: string;
|
|
14
14
|
}
|
|
15
|
+
/** One step of a destroy: `finished`, `failed` (with `error`), `started` (in flight; a crash leaves it so), or `planned` (not reached yet). */
|
|
16
|
+
export interface DestroyStepRecord {
|
|
17
|
+
step: string;
|
|
18
|
+
status: 'planned' | 'started' | 'finished' | 'failed';
|
|
19
|
+
completedAt?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Foundry's record of the current or most recent destroy of an org, in step order. */
|
|
23
|
+
export interface DestroyRecord {
|
|
24
|
+
steps: DestroyStepRecord[];
|
|
25
|
+
}
|
|
15
26
|
/**
|
|
16
27
|
* When an apply last completed, and against which declared org version.
|
|
17
28
|
*
|
|
@@ -81,4 +92,6 @@ export interface AppliedState {
|
|
|
81
92
|
failedFields: string[];
|
|
82
93
|
/** `null` before this org has ever had a completed apply. */
|
|
83
94
|
lastApplied: LastApplied | null;
|
|
95
|
+
/** The destroy that started for this org, if any. Absent until one has. */
|
|
96
|
+
destroy?: DestroyRecord | null;
|
|
84
97
|
}
|
package/dist/store/audit.js
CHANGED
|
@@ -14,10 +14,11 @@ const FOUNDRY_LEDGER_VOCABULARY = ledgerVocabulary({
|
|
|
14
14
|
'plan.computed': { tenantVisible: false },
|
|
15
15
|
'apply.recorded': { tenantVisible: false },
|
|
16
16
|
'destroy.recorded': { tenantVisible: false },
|
|
17
|
+
'adoption.recorded': { tenantVisible: false },
|
|
17
18
|
},
|
|
18
19
|
actorClasses: ['operator'],
|
|
19
20
|
contexts: ['operator'],
|
|
20
|
-
outcomes: ['recorded', 'applied', 'destroyed', 'refused', 'failed'],
|
|
21
|
+
outcomes: ['recorded', 'applied', 'destroyed', 'refused', 'failed', 'adopted', 'released'],
|
|
21
22
|
});
|
|
22
23
|
/**
|
|
23
24
|
* The ledger table's CHECKs (migration 0024): reason <= 512 chars, `after`
|
|
@@ -110,6 +111,19 @@ export function createPostgresAuditLog(db, options = {}) {
|
|
|
110
111
|
occurredAt: at,
|
|
111
112
|
});
|
|
112
113
|
},
|
|
114
|
+
async recordAdoption({ actor, orgId, action, entryKey, outcome, reason, at }) {
|
|
115
|
+
await ledger.sign(db, {
|
|
116
|
+
action: 'adoption.recorded',
|
|
117
|
+
tenantId: null,
|
|
118
|
+
actor: { class: 'operator', id: actor.id, display: boundDisplay(actor) },
|
|
119
|
+
context: 'operator',
|
|
120
|
+
target: targetFor(orgId),
|
|
121
|
+
outcome,
|
|
122
|
+
reason: boundReason(reason),
|
|
123
|
+
after: { action, entryKey },
|
|
124
|
+
occurredAt: at,
|
|
125
|
+
});
|
|
126
|
+
},
|
|
113
127
|
async recordDestroy({ actor, orgId, outcome, reason, at }) {
|
|
114
128
|
await ledger.sign(db, {
|
|
115
129
|
action: 'destroy.recorded',
|
package/dist/store/postgres.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
|
|
2
|
-
import type
|
|
2
|
+
import { type AppliedVersionsStore, type DeclaredOrgStore, type PlatformStore, type RequestedChangeStore } from '../store.js';
|
|
3
3
|
/**
|
|
4
4
|
* The host's own drizzle handle or a transaction open on it, same shape
|
|
5
5
|
* `@wtfalch/keys`'s `HeldKeysDb` uses: postgres-js in a real host, PGlite in
|