@wtfalch/foundry 0.16.15 → 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 +82 -5
- package/dist/org.js +62 -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/deploy-health.d.ts +136 -0
- package/dist/plan/deploy-health.js +153 -0
- package/dist/plan/diff.d.ts +11 -3
- package/dist/plan/diff.js +77 -71
- package/dist/plan/entry-ai-key.d.ts +3 -3
- package/dist/plan/entry-files-key.d.ts +6 -4
- package/dist/plan/entry-files-key.js +7 -10
- package/dist/plan/executor.d.ts +109 -28
- package/dist/plan/executor.js +213 -56
- package/dist/plan/hostnames.d.ts +6 -0
- package/dist/plan/hostnames.js +13 -0
- package/dist/plan/index.d.ts +5 -1
- package/dist/plan/index.js +3 -1
- 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 +153 -3
- package/dist/plan/plan-apply.js +584 -69
- package/dist/plan/platform-diff.d.ts +103 -9
- package/dist/plan/platform-diff.js +192 -14
- package/dist/plan/platform-org.d.ts +25 -0
- package/dist/plan/platform-org.js +17 -1
- package/dist/plan/platform-providers.d.ts +38 -5
- package/dist/plan/platform-providers.js +592 -109
- 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 +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
|
|
1
|
+
import type { DeclaredOrg, EnvSharedSecret, HostingEntry, OrgId } from '../org.js';
|
|
2
2
|
import { type Plan, type UnmanagedItem, type VersionChangeItem } from '../plan.js';
|
|
3
3
|
import type { DeclaredPlatform } from '../platform.js';
|
|
4
4
|
import type { PlannedStep } from './diff.js';
|
|
@@ -107,6 +107,17 @@ export declare const PLATFORM_ARCHON_KEY = "platform:archon";
|
|
|
107
107
|
*/
|
|
108
108
|
export declare const PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY = "archon_applied_hostname";
|
|
109
109
|
export declare const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = "archon_applied_git_ref";
|
|
110
|
+
/**
|
|
111
|
+
* #398: the integrations management key id Archon runs with, recorded by
|
|
112
|
+
* `provisionPlatformArchon` only after its deployment finished and its health
|
|
113
|
+
* check passed with that id in its env. `provisionPlatformIntegrations`'s
|
|
114
|
+
* rotation step reads this before dropping a previous key id from the
|
|
115
|
+
* runner's `MANAGEMENT_PUBLIC_KEYS`: the old key stays pinned until this
|
|
116
|
+
* matches the new id, so a request signed by an Archon container still
|
|
117
|
+
* running the old key is never refused. If Archon's deploy fails this stays
|
|
118
|
+
* unrecorded and the old key stays pinned.
|
|
119
|
+
*/
|
|
120
|
+
export declare const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = "archon_integrations_key_synced";
|
|
110
121
|
/**
|
|
111
122
|
* #211 round 4: the broker app id Archon last registered its entry with.
|
|
112
123
|
* `provisionPlatformArchon` reads the broker to register with from
|
|
@@ -118,6 +129,19 @@ export declare const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = "archon_applied
|
|
|
118
129
|
* changed hostname or `gitRef` does.
|
|
119
130
|
*/
|
|
120
131
|
export declare const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = "archon_applied_broker_app";
|
|
132
|
+
/**
|
|
133
|
+
* The `AUTHZ_BOOTSTRAP_OPERATOR` the last Archon apply set (`none` when it set
|
|
134
|
+
* none). `planArchon` plans the step again when the owner's id recorded by
|
|
135
|
+
* `platform:auth` differs from it, so Archon picks the owner up once auth
|
|
136
|
+
* has recorded it.
|
|
137
|
+
*/
|
|
138
|
+
export declare const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = "archon_applied_bootstrap_operator";
|
|
139
|
+
/**
|
|
140
|
+
* The owner's ZITADEL user id Archon takes as its first operator, from
|
|
141
|
+
* {@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY}. Undefined when none is
|
|
142
|
+
* recorded or the recorded value is `none` (no owner declared).
|
|
143
|
+
*/
|
|
144
|
+
export declare function archonBootstrapOperator(recorded: Readonly<Record<string, string>>): string | undefined;
|
|
121
145
|
/**
|
|
122
146
|
* #175: set to '1' before the step points the tunnel at `http_status:404`
|
|
123
147
|
* (Access app rebuild), cleared to '' once the real ingress is written back.
|
|
@@ -285,6 +309,20 @@ export declare const PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY = "coolify_platform
|
|
|
285
309
|
export declare const PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF = "platform:master:coolify-url";
|
|
286
310
|
export declare const PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF = "platform:master:coolify-token";
|
|
287
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";
|
|
288
326
|
/**
|
|
289
327
|
* #169: `platform:<service>` for the platform's own mail server (Stalwart),
|
|
290
328
|
* mirroring {@link PLATFORM_AUTH_KEY} exactly.
|
|
@@ -400,14 +438,31 @@ export declare function appDomainsAppliedScopeKey(entryKey: string): string;
|
|
|
400
438
|
* entry), so an app that runs today plans no change.
|
|
401
439
|
*/
|
|
402
440
|
export declare function appliedAppDomains(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, entryKey: string): string[];
|
|
441
|
+
/** Whether the secret sources can be filled right now: the mail key is held, and a new shared secret can be held. */
|
|
442
|
+
export interface AppSecretAvailability {
|
|
443
|
+
platformMailKey: boolean;
|
|
444
|
+
sharedSecret: boolean;
|
|
445
|
+
}
|
|
403
446
|
/**
|
|
404
447
|
* The entry's `env` values that have a value, in env-name order, and the env
|
|
405
448
|
* names whose source has none yet. Shared by `planApp` and the executor's
|
|
406
449
|
* `pushAppEnv` so both read the same recorded ids.
|
|
450
|
+
*
|
|
451
|
+
* A secret source is never a value here. `secrets` names each one that can be
|
|
452
|
+
* filled (`available`), `unresolvedSecrets` each that cannot, and `marker`
|
|
453
|
+
* is `values` plus a `secret:<source>` label per available secret: the JSON
|
|
454
|
+
* `coolifyApp` records as applied, so a declared change replans and no secret
|
|
455
|
+
* is written into it.
|
|
407
456
|
*/
|
|
408
|
-
export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string
|
|
457
|
+
export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>, available?: AppSecretAvailability): {
|
|
409
458
|
values: Record<string, string>;
|
|
410
459
|
unresolved: Array<[string, string]>;
|
|
460
|
+
secrets: Array<{
|
|
461
|
+
name: string;
|
|
462
|
+
source: 'platformMailKey' | EnvSharedSecret;
|
|
463
|
+
}>;
|
|
464
|
+
unresolvedSecrets: Array<[string, string]>;
|
|
465
|
+
marker: Record<string, string>;
|
|
411
466
|
};
|
|
412
467
|
/**
|
|
413
468
|
* #172: the resource key `planAi` plans under and `FoundryPlatformStepExecutor`
|
|
@@ -478,13 +533,6 @@ export declare const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = "ai_platform_migrat
|
|
|
478
533
|
* that throws) is repaired by the next one.
|
|
479
534
|
*/
|
|
480
535
|
export declare const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = "ai_platform_deploy_pending";
|
|
481
|
-
/**
|
|
482
|
-
* The Coolify deployment queued for the host (its `deployment_uuid`, or
|
|
483
|
-
* `untracked` when the deploy answered none), set when that deploy is queued
|
|
484
|
-
* and cleared once it finished. The mint waits for it, so a retry after a
|
|
485
|
-
* failed wait still waits instead of minting against the old container.
|
|
486
|
-
*/
|
|
487
|
-
export declare const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = "ai_platform_runner_deployment";
|
|
488
536
|
/**
|
|
489
537
|
* #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
|
|
490
538
|
* dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
|
|
@@ -684,3 +732,49 @@ export declare const PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY = "integrations_man
|
|
|
684
732
|
/** The hostname and gitRef the last successful apply put live; a difference from the declared ones plans the step again. */
|
|
685
733
|
export declare const PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY = "integrations_applied_hostname";
|
|
686
734
|
export declare const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = "integrations_applied_gitref";
|
|
735
|
+
/**
|
|
736
|
+
* #398: the key id currently pinned on the runner as the active entry of
|
|
737
|
+
* `MANAGEMENT_PUBLIC_KEYS` -- `foundry-1` on the first mint, and whatever a
|
|
738
|
+
* rotation minted next after that. `collectArchonServiceInputs` reads this
|
|
739
|
+
* (falling back to the fixed `INTEGRATIONS_MANAGEMENT_KEY_ID` constant for a
|
|
740
|
+
* platform applied before this key existed) for Archon's own
|
|
741
|
+
* `INTEGRATIONS_MANAGEMENT_KEY_ID` env, so a rotated id reaches it without
|
|
742
|
+
* Archon's step needing to know anything happened on the integrations side.
|
|
743
|
+
*/
|
|
744
|
+
export declare const PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY = "integrations_management_key_id";
|
|
745
|
+
/**
|
|
746
|
+
* #398: the key id the runner was last deployed with in its
|
|
747
|
+
* `MANAGEMENT_PUBLIC_KEYS`, recorded by `provisionPlatformIntegrations` only
|
|
748
|
+
* after the runner's deployment finished. Archon is not moved to a new key id
|
|
749
|
+
* until this equals the active id, so Archon never signs with a key the
|
|
750
|
+
* running runner does not yet hold.
|
|
751
|
+
*/
|
|
752
|
+
export declare const PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY = "integrations_runner_keyring_deployed";
|
|
753
|
+
/**
|
|
754
|
+
* #398: the same two-key discipline as
|
|
755
|
+
* {@link PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY}, for a rotation's
|
|
756
|
+
* new pair: recorded the moment it is minted, before delivery, and cleared
|
|
757
|
+
* by nothing -- {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} moving past it
|
|
758
|
+
* is what later runs read as "delivered and pinned", the same way the
|
|
759
|
+
* once-only mint's guard is a second key rather than removing the first.
|
|
760
|
+
* An id here that {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} has not
|
|
761
|
+
* caught up to means the pair may be lost or half-applied; the step reports
|
|
762
|
+
* it and mints no further pair.
|
|
763
|
+
*/
|
|
764
|
+
export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY = "integrations_management_key_rotation_undelivered";
|
|
765
|
+
/**
|
|
766
|
+
* #398: the previous key id, still pinned in `MANAGEMENT_PUBLIC_KEYS` beside
|
|
767
|
+
* the new one, pending removal -- set the moment the new pair is pinned
|
|
768
|
+
* beside it, and cleared (to `''`, the {@link PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY}
|
|
769
|
+
* convention for "was set, now isn't") once the previous id is actually
|
|
770
|
+
* removed from the keyring. Its presence is the overlap window: both keys
|
|
771
|
+
* verify a request while it holds.
|
|
772
|
+
*/
|
|
773
|
+
export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = "integrations_management_key_rotation_previous";
|
|
774
|
+
/**
|
|
775
|
+
* #398: how a rotation is requested. A person records any non-empty value
|
|
776
|
+
* under this scope key; `planIntegrations` then plans the step again and
|
|
777
|
+
* `provisionPlatformIntegrations` mints the new pair, clearing the key (to
|
|
778
|
+
* `''`) as soon as the mint is recorded, so one request is one rotation.
|
|
779
|
+
*/
|
|
780
|
+
export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = "integrations_management_key_rotation_requested";
|
|
@@ -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`
|
|
@@ -127,6 +128,17 @@ export const PLATFORM_ARCHON_KEY = 'platform:archon';
|
|
|
127
128
|
*/
|
|
128
129
|
export const PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY = 'archon_applied_hostname';
|
|
129
130
|
export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref';
|
|
131
|
+
/**
|
|
132
|
+
* #398: the integrations management key id Archon runs with, recorded by
|
|
133
|
+
* `provisionPlatformArchon` only after its deployment finished and its health
|
|
134
|
+
* check passed with that id in its env. `provisionPlatformIntegrations`'s
|
|
135
|
+
* rotation step reads this before dropping a previous key id from the
|
|
136
|
+
* runner's `MANAGEMENT_PUBLIC_KEYS`: the old key stays pinned until this
|
|
137
|
+
* matches the new id, so a request signed by an Archon container still
|
|
138
|
+
* running the old key is never refused. If Archon's deploy fails this stays
|
|
139
|
+
* unrecorded and the old key stays pinned.
|
|
140
|
+
*/
|
|
141
|
+
export const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = 'archon_integrations_key_synced';
|
|
130
142
|
/**
|
|
131
143
|
* #211 round 4: the broker app id Archon last registered its entry with.
|
|
132
144
|
* `provisionPlatformArchon` reads the broker to register with from
|
|
@@ -138,6 +150,22 @@ export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref
|
|
|
138
150
|
* changed hostname or `gitRef` does.
|
|
139
151
|
*/
|
|
140
152
|
export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_app';
|
|
153
|
+
/**
|
|
154
|
+
* The `AUTHZ_BOOTSTRAP_OPERATOR` the last Archon apply set (`none` when it set
|
|
155
|
+
* none). `planArchon` plans the step again when the owner's id recorded by
|
|
156
|
+
* `platform:auth` differs from it, so Archon picks the owner up once auth
|
|
157
|
+
* has recorded it.
|
|
158
|
+
*/
|
|
159
|
+
export const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = 'archon_applied_bootstrap_operator';
|
|
160
|
+
/**
|
|
161
|
+
* The owner's ZITADEL user id Archon takes as its first operator, from
|
|
162
|
+
* {@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY}. Undefined when none is
|
|
163
|
+
* recorded or the recorded value is `none` (no owner declared).
|
|
164
|
+
*/
|
|
165
|
+
export function archonBootstrapOperator(recorded) {
|
|
166
|
+
const id = recorded[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
|
|
167
|
+
return id && id !== 'none' ? id : undefined;
|
|
168
|
+
}
|
|
141
169
|
/**
|
|
142
170
|
* #175: set to '1' before the step points the tunnel at `http_status:404`
|
|
143
171
|
* (Access app rebuild), cleared to '' once the real ingress is written back.
|
|
@@ -316,6 +344,20 @@ export const PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY = 'coolify_platform_archon_
|
|
|
316
344
|
export const PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF = 'platform:master:coolify-url';
|
|
317
345
|
export const PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF = 'platform:master:coolify-token';
|
|
318
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';
|
|
319
361
|
/**
|
|
320
362
|
* #169: `platform:<service>` for the platform's own mail server (Stalwart),
|
|
321
363
|
* mirroring {@link PLATFORM_AUTH_KEY} exactly.
|
|
@@ -448,8 +490,14 @@ export function appliedAppDomains(org, recorded, entryKey) {
|
|
|
448
490
|
* The entry's `env` values that have a value, in env-name order, and the env
|
|
449
491
|
* names whose source has none yet. Shared by `planApp` and the executor's
|
|
450
492
|
* `pushAppEnv` so both read the same recorded ids.
|
|
493
|
+
*
|
|
494
|
+
* A secret source is never a value here. `secrets` names each one that can be
|
|
495
|
+
* filled (`available`), `unresolvedSecrets` each that cannot, and `marker`
|
|
496
|
+
* is `values` plus a `secret:<source>` label per available secret: the JSON
|
|
497
|
+
* `coolifyApp` records as applied, so a declared change replans and no secret
|
|
498
|
+
* is written into it.
|
|
451
499
|
*/
|
|
452
|
-
export function resolveAppEnv(org, entry, recorded) {
|
|
500
|
+
export function resolveAppEnv(org, entry, recorded, available = { platformMailKey: false, sharedSecret: false }) {
|
|
453
501
|
const sources = {
|
|
454
502
|
filesOrganisationId: recorded[`files:organisation:${org.id}`],
|
|
455
503
|
aiOrganisationId: recorded[`ai:organisation:${org.id}`],
|
|
@@ -457,14 +505,38 @@ export function resolveAppEnv(org, entry, recorded) {
|
|
|
457
505
|
};
|
|
458
506
|
const values = {};
|
|
459
507
|
const unresolved = [];
|
|
508
|
+
const secrets = [];
|
|
509
|
+
const unresolvedSecrets = [];
|
|
510
|
+
const marker = {};
|
|
460
511
|
for (const [name, declared] of Object.entries(entry.env).sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
512
|
+
if (typeof declared !== 'string' && declared.from === 'platformMailKey') {
|
|
513
|
+
if (available.platformMailKey) {
|
|
514
|
+
secrets.push({ name, source: 'platformMailKey' });
|
|
515
|
+
marker[name] = 'secret:platformMailKey';
|
|
516
|
+
}
|
|
517
|
+
else
|
|
518
|
+
unresolvedSecrets.push([name, 'platformMailKey']);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (typeof declared !== 'string' && declared.from === 'sharedSecret') {
|
|
522
|
+
const label = `sharedSecret ${declared.name}`;
|
|
523
|
+
if (available.sharedSecret) {
|
|
524
|
+
secrets.push({ name, source: declared });
|
|
525
|
+
marker[name] = `secret:sharedSecret:${declared.name}`;
|
|
526
|
+
}
|
|
527
|
+
else
|
|
528
|
+
unresolvedSecrets.push([name, label]);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
461
531
|
const value = typeof declared === 'string' ? declared : sources[declared.from];
|
|
462
|
-
if (value)
|
|
532
|
+
if (value) {
|
|
463
533
|
values[name] = value;
|
|
534
|
+
marker[name] = value;
|
|
535
|
+
}
|
|
464
536
|
else if (typeof declared !== 'string')
|
|
465
537
|
unresolved.push([name, declared.from]);
|
|
466
538
|
}
|
|
467
|
-
return { values, unresolved };
|
|
539
|
+
return { values, unresolved, secrets, unresolvedSecrets, marker };
|
|
468
540
|
}
|
|
469
541
|
/**
|
|
470
542
|
* #172: the resource key `planAi` plans under and `FoundryPlatformStepExecutor`
|
|
@@ -535,13 +607,6 @@ export const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = 'ai_platform_migrate_blocke
|
|
|
535
607
|
* that throws) is repaired by the next one.
|
|
536
608
|
*/
|
|
537
609
|
export const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = 'ai_platform_deploy_pending';
|
|
538
|
-
/**
|
|
539
|
-
* The Coolify deployment queued for the host (its `deployment_uuid`, or
|
|
540
|
-
* `untracked` when the deploy answered none), set when that deploy is queued
|
|
541
|
-
* and cleared once it finished. The mint waits for it, so a retry after a
|
|
542
|
-
* failed wait still waits instead of minting against the old container.
|
|
543
|
-
*/
|
|
544
|
-
export const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = 'ai_platform_runner_deployment';
|
|
545
610
|
/**
|
|
546
611
|
* #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
|
|
547
612
|
* dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
|
|
@@ -715,8 +780,19 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
715
780
|
unmanaged.push({ description: ai.blocked, key: 'ai' });
|
|
716
781
|
if (archon.blocked)
|
|
717
782
|
unmanaged.push({ description: archon.blocked, key: 'archon' });
|
|
783
|
+
// #398: the old integrations key stays pinned until Archon holds the new
|
|
784
|
+
// id; name that when Archon is what is stopping it.
|
|
785
|
+
if (archon.blocked &&
|
|
786
|
+
recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY] &&
|
|
787
|
+
recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] !==
|
|
788
|
+
recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY]) {
|
|
789
|
+
unmanaged.push({
|
|
790
|
+
description: `the previous integrations management key ${recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY]} stays pinned on the runner until Archon is redeployed with the new key, and Archon is blocked (see above)`,
|
|
791
|
+
key: 'integrations',
|
|
792
|
+
});
|
|
793
|
+
}
|
|
718
794
|
unmanaged.push(...backups.blocked);
|
|
719
|
-
return { plannedSteps, unmanaged, versionChanges };
|
|
795
|
+
return { plannedSteps: plannedSteps.map(withStepSystem), unmanaged, versionChanges };
|
|
720
796
|
}
|
|
721
797
|
/**
|
|
722
798
|
* The platform's own Cloudflare zone for its shared apex (`platform.ts`'s
|
|
@@ -1344,6 +1420,52 @@ export const PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY = 'integrations_management_
|
|
|
1344
1420
|
/** The hostname and gitRef the last successful apply put live; a difference from the declared ones plans the step again. */
|
|
1345
1421
|
export const PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY = 'integrations_applied_hostname';
|
|
1346
1422
|
export const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = 'integrations_applied_gitref';
|
|
1423
|
+
/**
|
|
1424
|
+
* #398: the key id currently pinned on the runner as the active entry of
|
|
1425
|
+
* `MANAGEMENT_PUBLIC_KEYS` -- `foundry-1` on the first mint, and whatever a
|
|
1426
|
+
* rotation minted next after that. `collectArchonServiceInputs` reads this
|
|
1427
|
+
* (falling back to the fixed `INTEGRATIONS_MANAGEMENT_KEY_ID` constant for a
|
|
1428
|
+
* platform applied before this key existed) for Archon's own
|
|
1429
|
+
* `INTEGRATIONS_MANAGEMENT_KEY_ID` env, so a rotated id reaches it without
|
|
1430
|
+
* Archon's step needing to know anything happened on the integrations side.
|
|
1431
|
+
*/
|
|
1432
|
+
export const PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY = 'integrations_management_key_id';
|
|
1433
|
+
/**
|
|
1434
|
+
* #398: the key id the runner was last deployed with in its
|
|
1435
|
+
* `MANAGEMENT_PUBLIC_KEYS`, recorded by `provisionPlatformIntegrations` only
|
|
1436
|
+
* after the runner's deployment finished. Archon is not moved to a new key id
|
|
1437
|
+
* until this equals the active id, so Archon never signs with a key the
|
|
1438
|
+
* running runner does not yet hold.
|
|
1439
|
+
*/
|
|
1440
|
+
export const PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY = 'integrations_runner_keyring_deployed';
|
|
1441
|
+
/**
|
|
1442
|
+
* #398: the same two-key discipline as
|
|
1443
|
+
* {@link PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY}, for a rotation's
|
|
1444
|
+
* new pair: recorded the moment it is minted, before delivery, and cleared
|
|
1445
|
+
* by nothing -- {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} moving past it
|
|
1446
|
+
* is what later runs read as "delivered and pinned", the same way the
|
|
1447
|
+
* once-only mint's guard is a second key rather than removing the first.
|
|
1448
|
+
* An id here that {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} has not
|
|
1449
|
+
* caught up to means the pair may be lost or half-applied; the step reports
|
|
1450
|
+
* it and mints no further pair.
|
|
1451
|
+
*/
|
|
1452
|
+
export const PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY = 'integrations_management_key_rotation_undelivered';
|
|
1453
|
+
/**
|
|
1454
|
+
* #398: the previous key id, still pinned in `MANAGEMENT_PUBLIC_KEYS` beside
|
|
1455
|
+
* the new one, pending removal -- set the moment the new pair is pinned
|
|
1456
|
+
* beside it, and cleared (to `''`, the {@link PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY}
|
|
1457
|
+
* convention for "was set, now isn't") once the previous id is actually
|
|
1458
|
+
* removed from the keyring. Its presence is the overlap window: both keys
|
|
1459
|
+
* verify a request while it holds.
|
|
1460
|
+
*/
|
|
1461
|
+
export const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = 'integrations_management_key_rotation_previous';
|
|
1462
|
+
/**
|
|
1463
|
+
* #398: how a rotation is requested. A person records any non-empty value
|
|
1464
|
+
* under this scope key; `planIntegrations` then plans the step again and
|
|
1465
|
+
* `provisionPlatformIntegrations` mints the new pair, clearing the key (to
|
|
1466
|
+
* `''`) as soon as the mint is recorded, so one request is one rotation.
|
|
1467
|
+
*/
|
|
1468
|
+
export const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = 'integrations_management_key_rotation_requested';
|
|
1347
1469
|
/**
|
|
1348
1470
|
* #174: the integrations host (https://github.com/wtfalch/integrations) -- its
|
|
1349
1471
|
* migrate job, web app and queue worker, their shared database, and its
|
|
@@ -1360,6 +1482,13 @@ export const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = 'integrations_appl
|
|
|
1360
1482
|
* Blocked, not planned: an undelivered key, and an executor that cannot run
|
|
1361
1483
|
* the step (no integrations dependencies, or no `@wtfalch/keys` values for
|
|
1362
1484
|
* the host).
|
|
1485
|
+
*
|
|
1486
|
+
* #398: a rotation (see {@link PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY})
|
|
1487
|
+
* or one still pending removal of the previous key
|
|
1488
|
+
* ({@link PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY}) plans the
|
|
1489
|
+
* step again even when `applied`. A rotation whose new pair is minted but
|
|
1490
|
+
* not confirmed delivered is blocked, never planned: the step would throw
|
|
1491
|
+
* on every apply.
|
|
1363
1492
|
*/
|
|
1364
1493
|
function planIntegrations(platform, recordedIds, executorBlocked) {
|
|
1365
1494
|
if (!platform.integrations.enabled)
|
|
@@ -1368,7 +1497,41 @@ function planIntegrations(platform, recordedIds, executorBlocked) {
|
|
|
1368
1497
|
const applied = Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_INTEGRATIONS_KEY)]) &&
|
|
1369
1498
|
recordedIds[PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY] === hostname &&
|
|
1370
1499
|
recordedIds[PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY] === gitRef;
|
|
1371
|
-
|
|
1500
|
+
const keyId = recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY];
|
|
1501
|
+
const previous = recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY];
|
|
1502
|
+
const keySet = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY]);
|
|
1503
|
+
// #398: a rotation key minted but not confirmed delivered and pinned blocks
|
|
1504
|
+
// the step whatever else is recorded: the executor throws on it, and
|
|
1505
|
+
// Archon's env refuses to pair the held key with an older id.
|
|
1506
|
+
const rotationUndelivered = recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY];
|
|
1507
|
+
if (rotationUndelivered && rotationUndelivered !== keyId) {
|
|
1508
|
+
return {
|
|
1509
|
+
step: null,
|
|
1510
|
+
blocked: `the integrations host is blocked, not planned: rotation key ${rotationUndelivered} was minted but not confirmed delivered and pinned; check the held key and the runner's MANAGEMENT_PUBLIC_KEYS, then remove the recorded key ${PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY} and set ${PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY} again to mint a new one`,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
// A request counts only once the first key is minted and no earlier
|
|
1514
|
+
// rotation is waiting to drop its previous key.
|
|
1515
|
+
const requested = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY]) &&
|
|
1516
|
+
keySet &&
|
|
1517
|
+
!previous;
|
|
1518
|
+
const requestIgnored = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY]) && !requested;
|
|
1519
|
+
// The previous key is dropped once Archon holds the new id, or at once when
|
|
1520
|
+
// Archon is not enabled (nothing else holds the old key). While Archon has
|
|
1521
|
+
// not synced there is nothing for this step to do.
|
|
1522
|
+
// Mirrors the executor's drop guards: the previous id is not the active
|
|
1523
|
+
// one, and the last delivered mint is the active one.
|
|
1524
|
+
const dropReady = Boolean(previous) &&
|
|
1525
|
+
previous !== keyId &&
|
|
1526
|
+
rotationUndelivered === keyId &&
|
|
1527
|
+
(!platform.archon.enabled ||
|
|
1528
|
+
recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY] === keyId);
|
|
1529
|
+
// The runner has not been deployed with the active key id yet (a deploy
|
|
1530
|
+
// failed after the key was pinned): plan the step to deploy it.
|
|
1531
|
+
const runnerStale = Boolean(keyId) &&
|
|
1532
|
+
recordedIds[PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY] !== keyId;
|
|
1533
|
+
const rotating = requested || dropReady || runnerStale;
|
|
1534
|
+
if (applied && !rotating)
|
|
1372
1535
|
return { step: null, blocked: null };
|
|
1373
1536
|
const undelivered = recordedIds[PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY];
|
|
1374
1537
|
if (undelivered && !recordedIds[PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY]) {
|
|
@@ -1388,7 +1551,9 @@ function planIntegrations(platform, recordedIds, executorBlocked) {
|
|
|
1388
1551
|
desired: { hostname, gitRef },
|
|
1389
1552
|
},
|
|
1390
1553
|
step: {
|
|
1391
|
-
description: `provision the integrations host at ${hostname}
|
|
1554
|
+
description: `provision the integrations host at ${hostname}${requestIgnored
|
|
1555
|
+
? `; the recorded rotation request ${PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY} is ignored and cleared (no key minted yet, or the previous key ${previous ?? ''} is still pending removal); set it again afterwards`
|
|
1556
|
+
: ''}`,
|
|
1392
1557
|
destroys: false,
|
|
1393
1558
|
migrates: false,
|
|
1394
1559
|
},
|
|
@@ -1613,7 +1778,20 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted)
|
|
|
1613
1778
|
Boolean(recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY]) &&
|
|
1614
1779
|
recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ===
|
|
1615
1780
|
recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY] &&
|
|
1616
|
-
|
|
1781
|
+
(recordedIds[PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY] ?? 'none') ===
|
|
1782
|
+
(archonBootstrapOperator(recordedIds) ?? 'none') &&
|
|
1783
|
+
!recordedIds[PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY] &&
|
|
1784
|
+
// #398: a rotated integrations key id Archon's env was not yet written
|
|
1785
|
+
// with (see PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY). Absent
|
|
1786
|
+
// before the first rotation on a platform applied before #398.
|
|
1787
|
+
// Only while integrations is enabled, and only once the runner is deployed
|
|
1788
|
+
// with that id (PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY).
|
|
1789
|
+
(!platform.integrations.enabled ||
|
|
1790
|
+
!recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ||
|
|
1791
|
+
recordedIds[PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY] !==
|
|
1792
|
+
recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ||
|
|
1793
|
+
recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ===
|
|
1794
|
+
recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY]);
|
|
1617
1795
|
if (applied && !drifted)
|
|
1618
1796
|
return { step: null, blocked: null };
|
|
1619
1797
|
if (!platform.auth.enabled) {
|
|
@@ -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
|
|
@@ -68,6 +84,13 @@ export interface OrgPlatformReadiness {
|
|
|
68
84
|
ai: string | null;
|
|
69
85
|
jobs: string | null;
|
|
70
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* The platform's Lettermint key (`PLATFORM_AUTH_BROKER_MAIL_KEY_REF`) is held,
|
|
89
|
+
* so an entry env from `platformMailKey` can be filled. Absent reads as not held.
|
|
90
|
+
*/
|
|
91
|
+
mailKeyHeld?: boolean;
|
|
92
|
+
/** The host's held keys can `put`, so an entry env from `sharedSecret` can be filled. Absent reads as false. */
|
|
93
|
+
canHoldSecrets?: boolean;
|
|
71
94
|
/**
|
|
72
95
|
* True when the host wired no held-keys reader, so no key could be checked.
|
|
73
96
|
* A missing key is then reported as "no reader given" instead of "not held".
|
|
@@ -91,6 +114,8 @@ export declare const ORG_PLATFORM_KEY_REFS: {
|
|
|
91
114
|
/** The one method of Archon's held keys this package reads. A value is returned to the caller and never logged, recorded or put in a plan. */
|
|
92
115
|
export interface HeldKeyResolver {
|
|
93
116
|
resolve(ref: string): Promise<string | null | undefined>;
|
|
117
|
+
/** Holds a value under `ref`; only the org steps' shared secrets need it. */
|
|
118
|
+
put?(ref: string, value: string): Promise<void>;
|
|
94
119
|
}
|
|
95
120
|
/**
|
|
96
121
|
* Which of {@link ORG_PLATFORM_KEY_REFS} `heldKeys` can resolve right now. A
|
|
@@ -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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CloudflareClient } from '@wtfalch/providers/cloudflare';
|
|
2
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
3
|
import type { DeclaredPlatform } from '../platform.js';
|
|
3
4
|
import type { DeclaredOrgStore, PlatformStore } from '../store.js';
|
|
4
5
|
import type { StepAction } from './diff.js';
|
|
@@ -243,6 +244,13 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
243
244
|
backupStorage?: boolean;
|
|
244
245
|
backupStorageBlocked?: string;
|
|
245
246
|
}>;
|
|
247
|
+
/**
|
|
248
|
+
* Gives the files runner a changed share-domain set and nothing else: its
|
|
249
|
+
* Coolify `domains`, `FILES_PUBLIC_HOSTS` and one redeploy of the runner.
|
|
250
|
+
* Returns the scope ids to record, or `null` when nothing changed
|
|
251
|
+
* (or the runner was never applied).
|
|
252
|
+
*/
|
|
253
|
+
refreshFilesShare(_platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<Record<string, string> | null>;
|
|
246
254
|
execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
|
|
247
255
|
resourceId?: string;
|
|
248
256
|
} | undefined>;
|
|
@@ -357,7 +365,7 @@ export declare function defaultStalwartForInstance(input: {
|
|
|
357
365
|
* own shape.
|
|
358
366
|
*/
|
|
359
367
|
export interface PlatformMailDeps {
|
|
360
|
-
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
|
|
368
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
|
|
361
369
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'>;
|
|
362
370
|
/**
|
|
363
371
|
* Must target the platform's own ZITADEL instance, or `zitadelForInstance`
|
|
@@ -401,7 +409,7 @@ export interface PlatformMailRuntime {
|
|
|
401
409
|
* {@link PlatformStepExecutionContext}, bundled like {@link PlatformAuthDeps}.
|
|
402
410
|
*/
|
|
403
411
|
export interface PlatformArchonDeps {
|
|
404
|
-
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
|
|
412
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'deleteEnv' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
|
|
405
413
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord' | 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
|
|
406
414
|
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
407
415
|
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp'>;
|
|
@@ -447,6 +455,17 @@ export interface PlatformArchonDeps {
|
|
|
447
455
|
coolifyToken: string;
|
|
448
456
|
cloudflareParentToken: string;
|
|
449
457
|
};
|
|
458
|
+
/** Clock, sleep and fetch for the post-deploy health wait (#252); real ones by default. */
|
|
459
|
+
runtime?: PlatformArchonRuntime;
|
|
460
|
+
}
|
|
461
|
+
export interface PlatformArchonRuntime {
|
|
462
|
+
fetch?: typeof fetch;
|
|
463
|
+
sleep?: (ms: number) => Promise<void>;
|
|
464
|
+
now?: () => number;
|
|
465
|
+
/** How long to wait for `/api/health` to answer the new deploy (default 5 minutes). */
|
|
466
|
+
readyTimeoutMs?: number;
|
|
467
|
+
/** Pause between readiness polls (default 5 seconds). */
|
|
468
|
+
readyIntervalMs?: number;
|
|
450
469
|
}
|
|
451
470
|
/**
|
|
452
471
|
* The name of foundry's own Cloudflare Access app in front of Archon. Same
|
|
@@ -562,7 +581,7 @@ export declare class AiManagementKeyUntrustedError extends Error {
|
|
|
562
581
|
}
|
|
563
582
|
/** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
|
|
564
583
|
export interface PlatformAiDeps {
|
|
565
|
-
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
|
|
584
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
|
|
566
585
|
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
567
586
|
/**
|
|
568
587
|
* Reads back a secret this step delivered on an earlier apply -- needed
|
|
@@ -690,7 +709,7 @@ export declare function brokerDeployRecordValue(brokerAppId: string, authAppsRaw
|
|
|
690
709
|
* {@link PlatformAuthDeps}.
|
|
691
710
|
*/
|
|
692
711
|
export interface PlatformFilesDeps {
|
|
693
|
-
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
|
|
712
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
|
|
694
713
|
/**
|
|
695
714
|
* Must target the platform's own sign-in (the instance `platform:auth`
|
|
696
715
|
* built), or {@link zitadelForInstance} must be set.
|
|
@@ -799,7 +818,7 @@ export declare const FILES_INTROSPECTION_SECRET_REF = "zitadel:platform:files:in
|
|
|
799
818
|
* {@link FoundryPlatformStepExecutor} keeps compiling with it omitted.
|
|
800
819
|
*/
|
|
801
820
|
export interface PlatformIntegrationsDeps {
|
|
802
|
-
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'raw'>;
|
|
821
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'deployAndWait' | 'raw'>;
|
|
803
822
|
/** Hands the management private key to the host, once (`FoundryStepExecutorPorts.deliverSecret`'s contract). */
|
|
804
823
|
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
805
824
|
/**
|
|
@@ -898,4 +917,18 @@ export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyRe
|
|
|
898
917
|
constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
|
|
899
918
|
listResources(): Promise<LiveResource[]>;
|
|
900
919
|
}
|
|
920
|
+
/** Scope id (no `:`): the share domains the runner's Coolify `domains` was last set to, comma-joined. */
|
|
921
|
+
export declare const PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY = "platform_files_share_domains";
|
|
922
|
+
/**
|
|
923
|
+
* The runner's `FILES_PUBLIC_HOSTS`: the host-supplied `publicHosts` first, in
|
|
924
|
+
* their given order, then the share domains sorted, with no host twice.
|
|
925
|
+
* Empty when there is neither.
|
|
926
|
+
*/
|
|
927
|
+
export declare function filesPublicHostsValue(publicHosts: string | undefined, shareDomains: readonly string[] | undefined): string;
|
|
928
|
+
/**
|
|
929
|
+
* The share domain of every org that has a domain and an entry declaring
|
|
930
|
+
* `uses.files.shareHost`: `share.<domain>`. The platform files step derives it
|
|
931
|
+
* from the declared orgs, for its apply and its drift check alike.
|
|
932
|
+
*/
|
|
933
|
+
export declare function filesShareDomains(orgs: readonly DeclaredOrg[]): string[];
|
|
901
934
|
export {};
|