@wtfalch/foundry 0.30.0 → 0.32.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 +6 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/org.d.ts +6 -1
- package/dist/org.js +3 -3
- package/dist/plan/confirm-upgrade.d.ts +17 -0
- package/dist/plan/confirm-upgrade.js +47 -0
- package/dist/plan/diff.js +49 -3
- package/dist/plan/executor.js +20 -27
- package/dist/plan/index.d.ts +3 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/plan-apply.d.ts +17 -2
- package/dist/plan/plan-apply.js +32 -2
- package/dist/plan/platform-diff.d.ts +17 -0
- package/dist/plan/platform-diff.js +62 -6
- package/dist/plan/platform-providers.d.ts +3 -4
- package/dist/plan/platform-providers.js +63 -23
- package/dist/plan/version-status.d.ts +1 -2
- package/dist/plan.d.ts +12 -0
- package/package.json +2 -2
package/dist/apply.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OrgId } from './org.js';
|
|
2
2
|
import type { Plan } from './plan.js';
|
|
3
|
+
import type { FoundryApplyOptions } from './plan/plan-apply.js';
|
|
3
4
|
import type { AppliedState } from './state.js';
|
|
4
5
|
/** One destroy step starting, finishing or failing; `index` counts from 1 up to `total`. */
|
|
5
6
|
export interface DestroyProgress {
|
|
@@ -30,8 +31,12 @@ export interface PlanApply {
|
|
|
30
31
|
* required whenever `plan` contains a step with `destroys` or `migrates`
|
|
31
32
|
* true (#9). Implementations must refuse to apply when the live state a
|
|
32
33
|
* fresh plan would compute no longer matches this `plan` (#9, #10).
|
|
34
|
+
* @param options `confirmUpgrade` names the services whose version change
|
|
35
|
+
* is confirmed. Implementations must refuse, before any step runs, a plan
|
|
36
|
+
* whose `versionChanges` holds a `needsConfirmation` line for a service
|
|
37
|
+
* `confirmUpgrade` does not name exactly.
|
|
33
38
|
*/
|
|
34
|
-
apply(plan: Plan, confirmedOrgName: string): Promise<AppliedState>;
|
|
39
|
+
apply(plan: Plan, confirmedOrgName: string, options?: FoundryApplyOptions): Promise<AppliedState>;
|
|
35
40
|
/**
|
|
36
41
|
* Deletes every resource foundry created and recorded for `id`, then the
|
|
37
42
|
* declared org itself.
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requ
|
|
|
10
10
|
export type { SlugRejectionReason, SlugRejection } from './slug.js';
|
|
11
11
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
12
12
|
export type { Checkpoint, LastApplied, AppliedState, DestroyRecord, DestroyStepRecord, } from './state.js';
|
|
13
|
-
export type { PlanStep, UnmanagedItem, Plan } from './plan.js';
|
|
13
|
+
export type { PlanStep, UnmanagedItem, Plan, VersionChangeItem } from './plan.js';
|
|
14
14
|
export type { AppliedVersion, AppliedVersionsStore, DeclaredOrgStore, HeldLease, MutationLease, PlatformStore, RequestedChangeStore, StaleUpdate, UpdateOptions, } from './store.js';
|
|
15
15
|
export { StaleLeaseError } from './store.js';
|
|
16
16
|
export type { PlanApply, DestroyProgress } from './apply.js';
|
|
@@ -18,11 +18,11 @@ export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, cre
|
|
|
18
18
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
19
19
|
export type { FoundryStoreDb } from './store/postgres.js';
|
|
20
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
21
|
-
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, OrgPostgresPlan, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
21
|
+
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, OrgPostgresPlan, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, UnconfirmedUpgrade, } from './plan/index.js';
|
|
22
22
|
export { AdoptRefusedError } from './plan/index.js';
|
|
23
23
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
24
24
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, DnsPort, EdgePlatformAccess, EdgePlatformDns, EdgePlatformRateLimits, EdgePlatformWorkers, EdgePlatformZones, EdgeZonesOptions, ZonePort, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, EdgeWorkersOptions, EdgePlatformTunnels, TunnelPort, } from './plan/index.js';
|
|
25
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
25
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
26
26
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
27
27
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
28
28
|
export type { PlatformOrgClientFactories } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { createPostgresAuditLog } from './store/audit.js';
|
|
|
13
13
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
14
14
|
export { AdoptRefusedError } from './plan/index.js';
|
|
15
15
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
16
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
16
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_SFU_APPLIED_SCOPE_KEY, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
17
17
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
18
18
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
19
19
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
package/dist/org.d.ts
CHANGED
|
@@ -288,7 +288,12 @@ export declare const APP_ENTRY_KEY = "app";
|
|
|
288
288
|
export interface HostingEntry {
|
|
289
289
|
/** A DNS label, unique per org. It goes into step ids and scope keys, so it never holds `:` or `.`. */
|
|
290
290
|
key: string;
|
|
291
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* The repo under `wtfalch` the app builds from. `null` is the shape
|
|
293
|
+
* migration 0029 wrote for every org with no `bespokeAppRepo`; there is no
|
|
294
|
+
* shared default app any more (#447), so `plan/diff.ts`'s `planApp` blocks
|
|
295
|
+
* it at plan time instead of provisioning it.
|
|
296
|
+
*/
|
|
292
297
|
repo: string | null;
|
|
293
298
|
/** The app's folder in `repo`. `null` is the repo root. */
|
|
294
299
|
folder: string | null;
|
package/dist/org.js
CHANGED
|
@@ -502,8 +502,8 @@ const WORKER_SHA256 = /^[0-9a-f]{64}$/;
|
|
|
502
502
|
const WORKER_NAME_FIXED_LENGTH = 8 + 1 + '-edge'.length;
|
|
503
503
|
const WORKER_NAME_MAX_LENGTH = 63;
|
|
504
504
|
/**
|
|
505
|
-
* `worker`: the entry's own repo (
|
|
506
|
-
*
|
|
505
|
+
* `worker`: the entry's own repo (entries with `repo: null` have no Worker
|
|
506
|
+
* source of their own), an `https://`
|
|
507
507
|
* artifact URL with a lowercase-hex sha256 (never a bare path or relative
|
|
508
508
|
* file -- estate DECISIONS.md, 2026-10-04, rules out any checkout or
|
|
509
509
|
* bundling inside foundry), a route whose zone is the org's own domain
|
|
@@ -519,7 +519,7 @@ function validateEntryWorker(entry, hosting, domain) {
|
|
|
519
519
|
if (!hosting.enabled)
|
|
520
520
|
return `${at} requires hosting.enabled`;
|
|
521
521
|
if (entry.repo === null) {
|
|
522
|
-
return `${at} needs its own repo (repo: null is
|
|
522
|
+
return `${at} needs its own repo (repo: null is not a Worker source)`;
|
|
523
523
|
}
|
|
524
524
|
if (entry.key.length > WORKER_NAME_MAX_LENGTH - WORKER_NAME_FIXED_LENGTH) {
|
|
525
525
|
return `${at} entry key is too long: the deployed Worker's name (an 8-character org hash, the key, and "-edge") must fit Cloudflare's ${WORKER_NAME_MAX_LENGTH}-character limit`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { VersionChangeItem } from '../plan.js';
|
|
2
|
+
export interface UnconfirmedUpgrade {
|
|
3
|
+
service: string;
|
|
4
|
+
from?: string;
|
|
5
|
+
to?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Whether a change of `service` from `from` to `to` needs a confirmation:
|
|
9
|
+
* any change on a stateful service, otherwise a major or minor difference.
|
|
10
|
+
* A git ref or branch name has no major or minor, so it never counts as a jump.
|
|
11
|
+
*/
|
|
12
|
+
export declare function versionChangeNeedsConfirmation(service: string, from: string, to: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The lines that need a confirmation `confirmUpgrade` does not give. A name
|
|
15
|
+
* must match `VersionChangeItem.service` exactly; no value confirms all.
|
|
16
|
+
*/
|
|
17
|
+
export declare function unconfirmedUpgrades(versionChanges: readonly VersionChangeItem[] | undefined, confirmUpgrade: readonly string[] | undefined): UnconfirmedUpgrade[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `VersionChangeItem.service` names whose data the service migrates on
|
|
3
|
+
* first boot with a new image, so going back to the old image may not undo
|
|
4
|
+
* it. Stalwart is reported as `mail`, the name of its plan step. Postgres
|
|
5
|
+
* has no version-change line yet; its name is here for when it gets one.
|
|
6
|
+
*/
|
|
7
|
+
const STATEFUL_VERSION_CHANGE_SERVICES = new Set([
|
|
8
|
+
'zitadel',
|
|
9
|
+
'mail',
|
|
10
|
+
'synapse',
|
|
11
|
+
'mas',
|
|
12
|
+
'postgres',
|
|
13
|
+
]);
|
|
14
|
+
function majorMinor(version) {
|
|
15
|
+
const match = /^v?(\d+)\.(\d+)\.\d+/.exec(version.trim());
|
|
16
|
+
return match ? `${match[1]}.${match[2]}` : null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether a change of `service` from `from` to `to` needs a confirmation:
|
|
20
|
+
* any change on a stateful service, otherwise a major or minor difference.
|
|
21
|
+
* A git ref or branch name has no major or minor, so it never counts as a jump.
|
|
22
|
+
*/
|
|
23
|
+
export function versionChangeNeedsConfirmation(service, from, to) {
|
|
24
|
+
if (from === to)
|
|
25
|
+
return false;
|
|
26
|
+
if (STATEFUL_VERSION_CHANGE_SERVICES.has(service))
|
|
27
|
+
return true;
|
|
28
|
+
const a = majorMinor(from);
|
|
29
|
+
const b = majorMinor(to);
|
|
30
|
+
return a !== null && b !== null && a !== b;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The lines that need a confirmation `confirmUpgrade` does not give. A name
|
|
34
|
+
* must match `VersionChangeItem.service` exactly; no value confirms all.
|
|
35
|
+
*/
|
|
36
|
+
export function unconfirmedUpgrades(versionChanges, confirmUpgrade) {
|
|
37
|
+
const confirmed = new Set(confirmUpgrade ?? []);
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const result = [];
|
|
40
|
+
for (const line of versionChanges ?? []) {
|
|
41
|
+
if (!line.needsConfirmation || confirmed.has(line.service) || seen.has(line.service))
|
|
42
|
+
continue;
|
|
43
|
+
seen.add(line.service);
|
|
44
|
+
result.push({ service: line.service, from: line.from, to: line.to });
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
package/dist/plan/diff.js
CHANGED
|
@@ -2323,10 +2323,17 @@ function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed, de
|
|
|
2323
2323
|
step: { description: describeProvision(), destroys: false, migrates: false },
|
|
2324
2324
|
};
|
|
2325
2325
|
}
|
|
2326
|
+
/**
|
|
2327
|
+
* The git source of the retired shared default app (`apps/hestia`, deleted in
|
|
2328
|
+
* #447). {@link planApp} uses it only to recognise a live app still built
|
|
2329
|
+
* from it, which it deletes instead of parking: nothing can redeploy it.
|
|
2330
|
+
*/
|
|
2331
|
+
const RETIRED_DEFAULT_APP_REPO = 'wtfalch/foundry';
|
|
2326
2332
|
/**
|
|
2327
2333
|
* The org's own app on Coolify (`org.ts`'s `HostingConfig.enabled`, #71), a
|
|
2328
2334
|
* per-org singleton resource modeled on {@link planSite}: park, not delete,
|
|
2329
|
-
* on the way off (#7 corrections 1)
|
|
2335
|
+
* on the way off (#7 corrections 1) -- except a live app built from
|
|
2336
|
+
* {@link RETIRED_DEFAULT_APP_REPO}, which is deleted (#447).
|
|
2330
2337
|
*
|
|
2331
2338
|
* #75: every app foundry deploys also carries a durable log directory (a
|
|
2332
2339
|
* persistent volume mounted at `/app/logs`, `LOG_DIR` in its env) so a
|
|
@@ -2411,10 +2418,49 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2411
2418
|
});
|
|
2412
2419
|
}
|
|
2413
2420
|
}
|
|
2414
|
-
|
|
2421
|
+
// #447: no shared default app any more. Migration 0029 wrote `repo: null`
|
|
2422
|
+
// for every org without a bespoke app and nothing rewrites that data, so
|
|
2423
|
+
// the entry stays declared but is blocked here and never provisioned.
|
|
2424
|
+
const noRepo = entry !== undefined && entry.repo === null;
|
|
2425
|
+
if (noRepo) {
|
|
2426
|
+
blocked.push({
|
|
2427
|
+
description: `${org.slug}'s ${label} repo is blocked, not planned: there is no shared default app any more (#447)`,
|
|
2428
|
+
key: blockedKey('repo'),
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
const toggled = planHostingToggle('coolify', 'app', entry !== undefined && !noRepo, org, byKey, claimed, () => `provision ${org.slug}'s ${label}`, () => `park ${org.slug}'s ${label} (stops serving; app data is retained, not deleted -- #7 corrections 1)`, stepKey);
|
|
2432
|
+
// #447: only the live app's own `git_repository` decides, never the
|
|
2433
|
+
// declared entry (a `repo: null` entry can sit over a bespoke app, which is
|
|
2434
|
+
// parked as before). Only the `app` key ever held the default app, and an
|
|
2435
|
+
// entry that declares a repo is never a candidate. The reader does not
|
|
2436
|
+
// surface the Dockerfile location, so the repo is the whole match. Checked
|
|
2437
|
+
// whether or not the app is already parked, so a parked leftover is not
|
|
2438
|
+
// stranded.
|
|
2439
|
+
if (entryKey === APP_ENTRY_KEY && (entry === undefined || noRepo)) {
|
|
2440
|
+
const liveApp = byKey.get(resourceKey('coolify', 'app', stepKey));
|
|
2441
|
+
if (liveApp?.config.git_repository === RETIRED_DEFAULT_APP_REPO) {
|
|
2442
|
+
claimed.add(resourceKey('coolify', 'app', stepKey));
|
|
2443
|
+
return {
|
|
2444
|
+
step: {
|
|
2445
|
+
action: {
|
|
2446
|
+
system: 'coolify',
|
|
2447
|
+
kind: 'app',
|
|
2448
|
+
key: stepKey,
|
|
2449
|
+
desired: { enabled: false, delete: true },
|
|
2450
|
+
},
|
|
2451
|
+
step: {
|
|
2452
|
+
description: `delete ${org.slug}'s ${label}: it still builds from the retired shared default app, which no longer exists to redeploy from (#447)`,
|
|
2453
|
+
destroys: true,
|
|
2454
|
+
migrates: false,
|
|
2455
|
+
},
|
|
2456
|
+
},
|
|
2457
|
+
blocked,
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2415
2461
|
if (toggled)
|
|
2416
2462
|
return { step: toggled, blocked };
|
|
2417
|
-
if (!entry)
|
|
2463
|
+
if (!entry || noRepo)
|
|
2418
2464
|
return { step: null, blocked };
|
|
2419
2465
|
const live = byKey.get(resourceKey('coolify', 'app', stepKey));
|
|
2420
2466
|
if (!live)
|
package/dist/plan/executor.js
CHANGED
|
@@ -230,24 +230,6 @@ const RATE_LIMIT_RULE_DESCRIPTION = 'foundry: rate limit sign-in paths';
|
|
|
230
230
|
*/
|
|
231
231
|
const RATE_LIMIT_FREE_PLAN_PERIOD_SECONDS = 10;
|
|
232
232
|
const RATE_LIMIT_REQUESTS_PER_PERIOD = 10;
|
|
233
|
-
/**
|
|
234
|
-
* The shared standard app a level-1 org with no `bespokeAppRepo` gets
|
|
235
|
-
* deployed (gap P1, `.plans/2026-09-26-level-1-org-map.md`): one repo, the
|
|
236
|
-
* same for every such org, decided by William 2026-09-26. `coolifyApp` below
|
|
237
|
-
* names it as the git source in exactly the shape `bespokeAppRepo` itself
|
|
238
|
-
* uses (`org.ts`'s `DeclaredOrg.bespokeAppRepo: string | null`) whenever an
|
|
239
|
-
* org declares none of its own — the repo may not exist yet at decision
|
|
240
|
-
* time, and Coolify's own GitHub App create call is what will fail loudly
|
|
241
|
-
* against it, the same as any other repo name that does not exist.
|
|
242
|
-
*
|
|
243
|
-
* Hestia moved into this repo's own `apps/hestia` (estate DECISIONS.md,
|
|
244
|
-
* 2026-09-28; the hestia repo archives once this move lands), so the
|
|
245
|
-
* standard app now builds from foundry's own repo, told apart from
|
|
246
|
-
* `packages/foundry` by `dockerfile_location` — the same one-repo,
|
|
247
|
-
* multiple-apps shape `cmsSite`/`CMS_APP_DOCKERFILE` already use below.
|
|
248
|
-
*/
|
|
249
|
-
const DEFAULT_APP_REPO = 'wtfalch/foundry';
|
|
250
|
-
const DEFAULT_APP_DOCKERFILE = '/apps/hestia/Dockerfile';
|
|
251
233
|
/**
|
|
252
234
|
* The shared repo `cmsSite` builds its cms admin app from, always, and its
|
|
253
235
|
* site app from by default (https://github.com/wtfalch/cms) -- the
|
|
@@ -275,7 +257,7 @@ const CMS_APP_PORT = 3000;
|
|
|
275
257
|
* #164: the repo `coolifyAgora` builds the org's Agora from
|
|
276
258
|
* (https://github.com/wtfalch/agora) -- the issue's own "How" names this
|
|
277
259
|
* exact Dockerfile, and it is the only build strategy any app this file
|
|
278
|
-
* provisions ever uses ({@link
|
|
260
|
+
* provisions ever uses ({@link CMS_APP_DOCKERFILE}/
|
|
279
261
|
* `CALLS_SERVICE_DOCKERFILE` below are all `dockerfile_location` into a
|
|
280
262
|
* `createGithubAppApplication` build-from-repo call, never a registry
|
|
281
263
|
* image): "Coolify builds each org's Agora from the agora repo" is not a
|
|
@@ -1868,7 +1850,19 @@ export class FoundryStepExecutor {
|
|
|
1868
1850
|
const recordedAppId = context.recordedIds()[`coolify:app:${action.key}`];
|
|
1869
1851
|
if (action.desired.enabled === false) {
|
|
1870
1852
|
if (!recordedAppId) {
|
|
1871
|
-
throw new Error(`coolify:app for ${org.slug}: no live app recorded, nothing to park`);
|
|
1853
|
+
throw new Error(`coolify:app for ${org.slug}: no live app recorded, nothing to ${action.desired.delete === true ? 'delete' : 'park'}`);
|
|
1854
|
+
}
|
|
1855
|
+
// `planApp` sets `delete` only on the retired default app. A 404 counts
|
|
1856
|
+
// as deleted, so a retry after a crash past the Coolify delete succeeds.
|
|
1857
|
+
if (action.desired.delete === true) {
|
|
1858
|
+
try {
|
|
1859
|
+
await this.ports.coolify.deleteApplication(recordedAppId);
|
|
1860
|
+
}
|
|
1861
|
+
catch (error) {
|
|
1862
|
+
if (!isNotFoundError(error))
|
|
1863
|
+
throw error;
|
|
1864
|
+
}
|
|
1865
|
+
return {};
|
|
1872
1866
|
}
|
|
1873
1867
|
await this.ports.coolify.raw('POST', `/applications/${recordedAppId}/stop`);
|
|
1874
1868
|
return { resourceId: recordedAppId };
|
|
@@ -1876,10 +1870,12 @@ export class FoundryStepExecutor {
|
|
|
1876
1870
|
const entry = org.hosting.entries.find((e) => appStepKey(org, e.key) === action.key);
|
|
1877
1871
|
if (!entry)
|
|
1878
1872
|
throw new Error(`coolify:app for ${org.slug}: no hosting entry for ${action.key}`);
|
|
1879
|
-
//
|
|
1880
|
-
//
|
|
1881
|
-
|
|
1882
|
-
|
|
1873
|
+
// `planApp` blocks a `repo: null` entry at plan time; reaching here with
|
|
1874
|
+
// one means a stale or hand-built plan.
|
|
1875
|
+
if (entry.repo === null) {
|
|
1876
|
+
throw new Error(`coolify:app for ${org.slug}: entry '${entry.key}' has no repo`);
|
|
1877
|
+
}
|
|
1878
|
+
const appRepo = entry.repo;
|
|
1883
1879
|
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
1884
1880
|
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
1885
1881
|
// The recorded id first; the name when nothing is recorded, or when the
|
|
@@ -1953,9 +1949,6 @@ export class FoundryStepExecutor {
|
|
|
1953
1949
|
// copies `base_directory`, and reads `publish_directory` only for a
|
|
1954
1950
|
// built app.
|
|
1955
1951
|
base_directory: entry.folder === null ? undefined : `/${entry.folder}`,
|
|
1956
|
-
// Only the shared standard app builds from this repo; a bespoke
|
|
1957
|
-
// app's Dockerfile is at its own repo's root.
|
|
1958
|
-
dockerfile_location: entry.repo ? undefined : DEFAULT_APP_DOCKERFILE,
|
|
1959
1952
|
});
|
|
1960
1953
|
appId = created.uuid;
|
|
1961
1954
|
// Create sent every setting, so nothing is left to patch.
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -12,8 +12,9 @@ export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
|
|
12
12
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
13
13
|
export type { AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot } from './adopt.js';
|
|
14
14
|
export { AdoptRefusedError } from './adopt.js';
|
|
15
|
-
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan-apply.js';
|
|
16
|
-
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
15
|
+
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, } from './plan-apply.js';
|
|
16
|
+
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
17
|
+
export type { UnconfirmedUpgrade } from './confirm-upgrade.js';
|
|
17
18
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
18
19
|
export type { PlatformAuthDeps, PlatformAuthSmtp, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformDnsFactory, PlatformAccessFactory, PlatformAccessPort, PlatformTunnelsFactory, PlatformEdgeRuntime, } from './platform-providers.js';
|
|
19
20
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDUL
|
|
|
5
5
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
6
6
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
7
7
|
export { AdoptRefusedError } from './adopt.js';
|
|
8
|
-
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
8
|
+
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, UpgradeConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
9
9
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
10
10
|
// #171: how a host signs files management requests with the delivered key.
|
|
11
11
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
@@ -7,6 +7,7 @@ import type { AppliedState } from '../state.js';
|
|
|
7
7
|
import { type AppliedVersionsStore, type DeclaredOrgStore, type PlatformStore } from '../store.js';
|
|
8
8
|
import { type AdoptCandidate, type AdoptInput, type AdoptPlan } from './adopt.js';
|
|
9
9
|
import { type AgoraHostPort } from './agora.js';
|
|
10
|
+
import { type UnconfirmedUpgrade } from './confirm-upgrade.js';
|
|
10
11
|
import { type StepAction } from './diff.js';
|
|
11
12
|
import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
|
|
12
13
|
import { type EdgePortName, type HeldKeyResolver } from './platform-org.js';
|
|
@@ -313,6 +314,15 @@ export declare class StalePlanError extends Error {
|
|
|
313
314
|
export declare class ConfirmationRequiredError extends Error {
|
|
314
315
|
constructor(orgId: OrgId);
|
|
315
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* A plan holds a version change that needs confirmation
|
|
319
|
+
* (`VersionChangeItem.needsConfirmation`) and `confirmUpgrade` does not name
|
|
320
|
+
* its service exactly. `upgrades` lists every such service with its versions.
|
|
321
|
+
*/
|
|
322
|
+
export declare class UpgradeConfirmationRequiredError extends Error {
|
|
323
|
+
readonly upgrades: readonly UnconfirmedUpgrade[];
|
|
324
|
+
constructor(upgrades: readonly UnconfirmedUpgrade[]);
|
|
325
|
+
}
|
|
316
326
|
export declare class NotAuthorizedError extends Error {
|
|
317
327
|
constructor(actor: AuditActor, orgId: OrgId);
|
|
318
328
|
}
|
|
@@ -336,6 +346,11 @@ export declare class PlatformStalePlanError extends Error {
|
|
|
336
346
|
export declare class PlatformConfirmationRequiredError extends Error {
|
|
337
347
|
constructor();
|
|
338
348
|
}
|
|
349
|
+
/** The third argument of `apply` and `applyPlatform`. */
|
|
350
|
+
export interface FoundryApplyOptions {
|
|
351
|
+
/** Service names (`VersionChangeItem.service`), matched exactly, whose version change the operator confirmed. */
|
|
352
|
+
confirmUpgrade?: readonly string[];
|
|
353
|
+
}
|
|
339
354
|
export interface FoundryPlanApplyOptions {
|
|
340
355
|
store: DeclaredOrgStore;
|
|
341
356
|
readers: LiveReaders;
|
|
@@ -518,7 +533,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
518
533
|
*/
|
|
519
534
|
release(id: OrgId, entryKey: string): Promise<AppliedState>;
|
|
520
535
|
private computeAdopt;
|
|
521
|
-
apply(plan: Plan, confirmedOrgName: string): Promise<AppliedState>;
|
|
536
|
+
apply(plan: Plan, confirmedOrgName: string, options?: FoundryApplyOptions): Promise<AppliedState>;
|
|
522
537
|
/**
|
|
523
538
|
* After an org apply: gives the platform files runner a changed share-domain
|
|
524
539
|
* set (its Coolify `domains` and `FILES_PUBLIC_HOSTS`, derived from the
|
|
@@ -543,7 +558,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
543
558
|
* shape of their own. `confirmedName` must be the literal word `platform`
|
|
544
559
|
* rather than a declared name -- `DeclaredPlatform` has none.
|
|
545
560
|
*/
|
|
546
|
-
applyPlatform(plan: Plan, confirmedName: string): Promise<{
|
|
561
|
+
applyPlatform(plan: Plan, confirmedName: string, options?: FoundryApplyOptions): Promise<{
|
|
547
562
|
serviceIds: Record<string, string>;
|
|
548
563
|
}>;
|
|
549
564
|
/**
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -6,6 +6,7 @@ import { AlreadyRunningError, StepFailedError } from '../runner/errors.js';
|
|
|
6
6
|
import { StaleLeaseError, } from '../store.js';
|
|
7
7
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
8
8
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
9
|
+
import { unconfirmedUpgrades } from './confirm-upgrade.js';
|
|
9
10
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
10
11
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
11
12
|
import { BOULE_EDGE_ENTRY_KEY, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
@@ -53,6 +54,21 @@ export class ConfirmationRequiredError extends Error {
|
|
|
53
54
|
this.name = 'ConfirmationRequiredError';
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* A plan holds a version change that needs confirmation
|
|
59
|
+
* (`VersionChangeItem.needsConfirmation`) and `confirmUpgrade` does not name
|
|
60
|
+
* its service exactly. `upgrades` lists every such service with its versions.
|
|
61
|
+
*/
|
|
62
|
+
export class UpgradeConfirmationRequiredError extends Error {
|
|
63
|
+
upgrades;
|
|
64
|
+
constructor(upgrades) {
|
|
65
|
+
super(`the plan changes versions that must be confirmed by service name: ${upgrades
|
|
66
|
+
.map((u) => `${u.service} ${u.from ?? 'unknown'} -> ${u.to ?? 'unknown'}`)
|
|
67
|
+
.join(', ')}; pass confirmUpgrade naming each one`);
|
|
68
|
+
this.upgrades = upgrades;
|
|
69
|
+
this.name = 'UpgradeConfirmationRequiredError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
56
72
|
export class NotAuthorizedError extends Error {
|
|
57
73
|
constructor(actor, orgId) {
|
|
58
74
|
super(`${actor.display} does not hold the permission to apply org ${orgId} (#9, #19)`);
|
|
@@ -341,7 +357,7 @@ export class FoundryPlanApply {
|
|
|
341
357
|
previous,
|
|
342
358
|
};
|
|
343
359
|
}
|
|
344
|
-
async apply(plan, confirmedOrgName) {
|
|
360
|
+
async apply(plan, confirmedOrgName, options = {}) {
|
|
345
361
|
const orgId = plan.orgId;
|
|
346
362
|
const authorized = await this.authz.canApply(this.actor, orgId);
|
|
347
363
|
if (!authorized) {
|
|
@@ -354,6 +370,11 @@ export class FoundryPlanApply {
|
|
|
354
370
|
await this.refuse(plan, 'stale: live state has moved since this plan was computed');
|
|
355
371
|
throw new StalePlanError(orgId);
|
|
356
372
|
}
|
|
373
|
+
const unconfirmed = unconfirmedUpgrades(freshPlan.versionChanges, options.confirmUpgrade);
|
|
374
|
+
if (unconfirmed.length > 0) {
|
|
375
|
+
await this.refuse(plan, `version change without confirmUpgrade naming: ${unconfirmed.map((u) => u.service).join(', ')}`);
|
|
376
|
+
throw new UpgradeConfirmationRequiredError(unconfirmed);
|
|
377
|
+
}
|
|
357
378
|
if (planNeedsConfirmation(plan.steps) &&
|
|
358
379
|
confirmedOrgName.trim() !== org.displayName.trim()) {
|
|
359
380
|
await this.refuse(plan, "destructive or migrating step without the org's name typed correctly");
|
|
@@ -426,7 +447,7 @@ export class FoundryPlanApply {
|
|
|
426
447
|
* shape of their own. `confirmedName` must be the literal word `platform`
|
|
427
448
|
* rather than a declared name -- `DeclaredPlatform` has none.
|
|
428
449
|
*/
|
|
429
|
-
async applyPlatform(plan, confirmedName) {
|
|
450
|
+
async applyPlatform(plan, confirmedName, options = {}) {
|
|
430
451
|
const { store, executor } = this.requirePlatformDeps();
|
|
431
452
|
const declared = await store.get();
|
|
432
453
|
if (!declared)
|
|
@@ -449,6 +470,11 @@ export class FoundryPlanApply {
|
|
|
449
470
|
await this.refusePlatform(plan, platform.ownerOrgId, 'stale: live state has moved since this plan was computed');
|
|
450
471
|
throw new PlatformStalePlanError();
|
|
451
472
|
}
|
|
473
|
+
const unconfirmed = unconfirmedUpgrades(freshPlan.versionChanges, options.confirmUpgrade);
|
|
474
|
+
if (unconfirmed.length > 0) {
|
|
475
|
+
await this.refusePlatform(plan, platform.ownerOrgId, `version change without confirmUpgrade naming: ${unconfirmed.map((u) => u.service).join(', ')}`);
|
|
476
|
+
throw new UpgradeConfirmationRequiredError(unconfirmed);
|
|
477
|
+
}
|
|
452
478
|
if (planNeedsConfirmation(plan.steps) && confirmedName.trim() !== 'platform') {
|
|
453
479
|
await this.refusePlatform(plan, platform.ownerOrgId, "destructive or migrating step without 'platform' typed correctly");
|
|
454
480
|
throw new PlatformConfirmationRequiredError();
|
|
@@ -803,6 +829,10 @@ export class FoundryPlanApply {
|
|
|
803
829
|
const { system, kind, key } = planned.action;
|
|
804
830
|
serviceIds[`${system}:${kind}:${key}`] = result.resourceId;
|
|
805
831
|
}
|
|
832
|
+
else if (planned.action.desired.delete === true) {
|
|
833
|
+
const { system, kind, key } = planned.action;
|
|
834
|
+
delete serviceIds[`${system}:${kind}:${key}`];
|
|
835
|
+
}
|
|
806
836
|
checkpoints.push({ step: planned.step.description, completedAt: this.now().toISOString() });
|
|
807
837
|
}
|
|
808
838
|
}
|
|
@@ -216,6 +216,8 @@ export declare const PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY = "zitadel_platform_ow
|
|
|
216
216
|
export declare const PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY = "zitadel_platform_reserved_admin_username";
|
|
217
217
|
/** #394: recorded once the platform's own ZITADEL org has self-registration forced off. */
|
|
218
218
|
export declare const PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY = "zitadel_platform_registration_closed";
|
|
219
|
+
/** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
|
|
220
|
+
export declare const PLATFORM_ZITADEL_ORG_SCOPE_KEY = "zitadel_platform_org";
|
|
219
221
|
export declare const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = "zitadel_platform_admin_user_id";
|
|
220
222
|
export declare const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = "zitadel_platform_admin_user_undelivered";
|
|
221
223
|
/** #283: the created instance's id, recorded once its owner token is delivered. */
|
|
@@ -403,6 +405,15 @@ export declare const PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY = "stalwart_platf
|
|
|
403
405
|
*/
|
|
404
406
|
export declare const PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY = "mail_applied_hostname";
|
|
405
407
|
export declare const PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY = "mail_applied_git_ref";
|
|
408
|
+
/**
|
|
409
|
+
* #661: recorded with {@link PLATFORM_MAIL_ROUTE_WANTED} once the mail
|
|
410
|
+
* server's settings write included the rule that delivers hosted recipients
|
|
411
|
+
* locally (`@wtfalch/email` 0.20.0). A platform applied before it lacks the id,
|
|
412
|
+
* so `planMail` plans the step once more; that run redeploys nothing, mints
|
|
413
|
+
* no key and creates nothing in the sign-in server.
|
|
414
|
+
*/
|
|
415
|
+
export declare const PLATFORM_MAIL_ROUTE_SCOPE_KEY = "mail_route_applied";
|
|
416
|
+
export declare const PLATFORM_MAIL_ROUTE_WANTED = "local-first";
|
|
406
417
|
/**
|
|
407
418
|
* #177: where the mail server answers on the Coolify network
|
|
408
419
|
* (`http://<custom_internal_name>:<port>`), recorded by the step the same way
|
|
@@ -1029,6 +1040,12 @@ export interface PlanPlatformOptions {
|
|
|
1029
1040
|
* repair step is not planned.
|
|
1030
1041
|
*/
|
|
1031
1042
|
archonIdTokenUserinfo?: boolean;
|
|
1043
|
+
/**
|
|
1044
|
+
* The executor has the platform's sign-in dependencies and a ZITADEL
|
|
1045
|
+
* client for its instance. Not a block: without it the sign-in policy
|
|
1046
|
+
* step is not planned.
|
|
1047
|
+
*/
|
|
1048
|
+
authSignInPolicy?: boolean;
|
|
1032
1049
|
};
|
|
1033
1050
|
/**
|
|
1034
1051
|
* Steps whose live state differs from the declared one in a way the recorded
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { VERSIONS } from '../bootstrap/versions.js';
|
|
3
3
|
import { APP_ENTRY_KEY } from '../org.js';
|
|
4
4
|
import { VERSION_CHANGE_KIND, } from '../plan.js';
|
|
5
|
+
import { versionChangeNeedsConfirmation } from './confirm-upgrade.js';
|
|
5
6
|
import { ACCESS_PROVIDER_RECORD_KEY } from './gates.js';
|
|
6
7
|
import { legacyAppHostname } from './hostnames.js';
|
|
7
8
|
import { callsSfuAppliedFingerprint } from './platform-sfu.js';
|
|
@@ -78,6 +79,9 @@ export function versionChangeLine(service, from, to, note) {
|
|
|
78
79
|
kind: VERSION_CHANGE_KIND,
|
|
79
80
|
service,
|
|
80
81
|
description: `${service}: ${from} -> ${to} (${note})`,
|
|
82
|
+
from,
|
|
83
|
+
to,
|
|
84
|
+
needsConfirmation: versionChangeNeedsConfirmation(service, from, to),
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
/**
|
|
@@ -99,6 +103,8 @@ export function versionNotAppliedLine(service, appLive, applied, pinned) {
|
|
|
99
103
|
kind: VERSION_CHANGE_KIND,
|
|
100
104
|
service,
|
|
101
105
|
description: `${service}: pinned ${pinned}, running tag unknown`,
|
|
106
|
+
to: pinned,
|
|
107
|
+
needsConfirmation: false,
|
|
102
108
|
};
|
|
103
109
|
}
|
|
104
110
|
if (applied === pinned)
|
|
@@ -107,6 +113,10 @@ export function versionNotAppliedLine(service, appLive, applied, pinned) {
|
|
|
107
113
|
kind: VERSION_CHANGE_KIND,
|
|
108
114
|
service,
|
|
109
115
|
description: `${service}: pinned ${pinned}, running ${applied} (not applied by foundry)`,
|
|
116
|
+
from: applied,
|
|
117
|
+
to: pinned,
|
|
118
|
+
// #254: no apply acts on this line, so there is no upgrade to confirm.
|
|
119
|
+
needsConfirmation: false,
|
|
110
120
|
};
|
|
111
121
|
}
|
|
112
122
|
/**
|
|
@@ -243,6 +253,8 @@ export const PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY = 'zitadel_platform_owner_invi
|
|
|
243
253
|
export const PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY = 'zitadel_platform_reserved_admin_username';
|
|
244
254
|
/** #394: recorded once the platform's own ZITADEL org has self-registration forced off. */
|
|
245
255
|
export const PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY = 'zitadel_platform_registration_closed';
|
|
256
|
+
/** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
|
|
257
|
+
export const PLATFORM_ZITADEL_ORG_SCOPE_KEY = 'zitadel_platform_org';
|
|
246
258
|
export const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = 'zitadel_platform_admin_user_id';
|
|
247
259
|
export const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_admin_user_undelivered';
|
|
248
260
|
/** #283: the created instance's id, recorded once its owner token is delivered. */
|
|
@@ -441,6 +453,15 @@ export const PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY = 'stalwart_platform_admi
|
|
|
441
453
|
*/
|
|
442
454
|
export const PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY = 'mail_applied_hostname';
|
|
443
455
|
export const PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY = 'mail_applied_git_ref';
|
|
456
|
+
/**
|
|
457
|
+
* #661: recorded with {@link PLATFORM_MAIL_ROUTE_WANTED} once the mail
|
|
458
|
+
* server's settings write included the rule that delivers hosted recipients
|
|
459
|
+
* locally (`@wtfalch/email` 0.20.0). A platform applied before it lacks the id,
|
|
460
|
+
* so `planMail` plans the step once more; that run redeploys nothing, mints
|
|
461
|
+
* no key and creates nothing in the sign-in server.
|
|
462
|
+
*/
|
|
463
|
+
export const PLATFORM_MAIL_ROUTE_SCOPE_KEY = 'mail_route_applied';
|
|
464
|
+
export const PLATFORM_MAIL_ROUTE_WANTED = 'local-first';
|
|
444
465
|
/**
|
|
445
466
|
* #177: where the mail server answers on the Coolify network
|
|
446
467
|
* (`http://<custom_internal_name>:<port>`), recorded by the step the same way
|
|
@@ -1174,6 +1195,10 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1174
1195
|
const idTokenUserinfo = planArchonIdTokenUserinfo(platform, recordedIds, blocked.archonIdTokenUserinfo ?? false);
|
|
1175
1196
|
if (idTokenUserinfo)
|
|
1176
1197
|
plannedSteps.push(idTokenUserinfo);
|
|
1198
|
+
// #669: after the ID token repair too, which is proven; a policy call that fails every time holds up nothing.
|
|
1199
|
+
const signInPolicy = planAuthSignInPolicy(platform, recordedIds, blocked.authSignInPolicy ?? false);
|
|
1200
|
+
if (signInPolicy)
|
|
1201
|
+
plannedSteps.push(signInPolicy);
|
|
1177
1202
|
// #251: one line per service whose recorded applied ref no longer matches
|
|
1178
1203
|
// what the next apply would use (or, for zitadel, what is actually live).
|
|
1179
1204
|
// Never pushed into `plannedSteps` -- see `VersionChangeItem`'s own doc
|
|
@@ -1717,10 +1742,7 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
1717
1742
|
Boolean(recordedIds[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY]) &&
|
|
1718
1743
|
// The ZITADEL run options reach the container only through the auth
|
|
1719
1744
|
// step's redeploy, so a platform recorded before them still plans it.
|
|
1720
|
-
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS
|
|
1721
|
-
// #394: a platform applied before these existed plans the step again.
|
|
1722
|
-
Boolean(recordedIds[PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY]) &&
|
|
1723
|
-
Boolean(recordedIds[PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY])) {
|
|
1745
|
+
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS) {
|
|
1724
1746
|
return { step: null, blocked: null };
|
|
1725
1747
|
}
|
|
1726
1748
|
// The admin key was minted but never reached the host. ZITADEL shows a
|
|
@@ -1856,9 +1878,16 @@ function planMail(platform, recordedIds, executorBlocked, authReady) {
|
|
|
1856
1878
|
recordedIds[PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY] === platform.mail.gitRef &&
|
|
1857
1879
|
// #177: a mail step applied before this key existed never recorded it, and
|
|
1858
1880
|
// org steps read it for Agora's mail upstream. Missing means unapplied.
|
|
1859
|
-
recordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY]
|
|
1881
|
+
recordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY] &&
|
|
1882
|
+
recordedIds[PLATFORM_MAIL_ROUTE_SCOPE_KEY] === PLATFORM_MAIL_ROUTE_WANTED) {
|
|
1860
1883
|
return { step: null, blocked: null };
|
|
1861
1884
|
}
|
|
1885
|
+
// Only the route is missing: say so, the app is not redeployed.
|
|
1886
|
+
const onlyRouteMissing = recordedIds[key] &&
|
|
1887
|
+
recordedIds[PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY] &&
|
|
1888
|
+
recordedIds[PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY] === platform.mail.hostname &&
|
|
1889
|
+
recordedIds[PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY] === platform.mail.gitRef &&
|
|
1890
|
+
recordedIds[PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY];
|
|
1862
1891
|
// Stalwart's `x:ApiKey/set` shows a secret once. An id was minted but never
|
|
1863
1892
|
// reached the host: report it, plan nothing, the same rule {@link planAuth}
|
|
1864
1893
|
// follows for ZITADEL's own admin key.
|
|
@@ -1886,7 +1915,9 @@ function planMail(platform, recordedIds, executorBlocked, authReady) {
|
|
|
1886
1915
|
desired: { hostname: platform.mail.hostname, gitRef: platform.mail.gitRef },
|
|
1887
1916
|
},
|
|
1888
1917
|
step: {
|
|
1889
|
-
description:
|
|
1918
|
+
description: onlyRouteMissing
|
|
1919
|
+
? `write the mail server's local-delivery route at ${platform.mail.hostname}`
|
|
1920
|
+
: `provision mail (Stalwart) at ${platform.mail.hostname}`,
|
|
1890
1921
|
destroys: false,
|
|
1891
1922
|
migrates: false,
|
|
1892
1923
|
},
|
|
@@ -2231,6 +2262,31 @@ function planArchonIdTokenUserinfo(platform, recordedIds, wired) {
|
|
|
2231
2262
|
},
|
|
2232
2263
|
};
|
|
2233
2264
|
}
|
|
2265
|
+
/**
|
|
2266
|
+
* #394, #669: the platform's sign-in org reserves the username `admin` and
|
|
2267
|
+
* switches self-registration off. One `zitadel:sign-in-policy` step keyed
|
|
2268
|
+
* {@link PLATFORM_AUTH_KEY}, planned while either recorded key is missing and
|
|
2269
|
+
* the sign-in's ZITADEL org is recorded. Not planned before that, and not
|
|
2270
|
+
* blocked: nothing is owed until the sign-in exists.
|
|
2271
|
+
*/
|
|
2272
|
+
function planAuthSignInPolicy(platform, recordedIds, wired) {
|
|
2273
|
+
if (!wired || !platform.auth.enabled)
|
|
2274
|
+
return null;
|
|
2275
|
+
if (!recordedIds[PLATFORM_ZITADEL_ORG_SCOPE_KEY])
|
|
2276
|
+
return null;
|
|
2277
|
+
if (recordedIds[PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY] &&
|
|
2278
|
+
recordedIds[PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY]) {
|
|
2279
|
+
return null;
|
|
2280
|
+
}
|
|
2281
|
+
return {
|
|
2282
|
+
action: { system: 'zitadel', kind: 'sign-in-policy', key: PLATFORM_AUTH_KEY, desired: {} },
|
|
2283
|
+
step: {
|
|
2284
|
+
description: "reserve the username 'admin' and switch self-registration off on the platform's sign-in org",
|
|
2285
|
+
destroys: false,
|
|
2286
|
+
migrates: false,
|
|
2287
|
+
},
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2234
2290
|
/**
|
|
2235
2291
|
* #604: the platform's ZITADEL as a sign-in provider in Cloudflare Access: a
|
|
2236
2292
|
* ZITADEL client for Cloudflare, then edge's `ensureProvider`. One
|
|
@@ -10,7 +10,7 @@ import type { TunnelPort } from './edge-tunnels.js';
|
|
|
10
10
|
import type { CloudflarePort, CoolifyPort, FoundryStepExecutorPorts, ZitadelPort } from './executor.js';
|
|
11
11
|
import type { LiveResource, PlatformCloudflareReader, PlatformCoolifyReader } from './live.js';
|
|
12
12
|
import type { PlatformDriftedSteps, PlatformStepExecutionContext, PlatformStepExecutor } from './plan-apply.js';
|
|
13
|
-
import { type BrokerDeployRecord } from './platform-diff.js';
|
|
13
|
+
import { type BrokerDeployRecord, PLATFORM_ZITADEL_ORG_SCOPE_KEY } from './platform-diff.js';
|
|
14
14
|
import { type CallsSfuPlatformDeps } from './platform-sfu.js';
|
|
15
15
|
import { type PlatformUptimeDeps } from './uptime-worker.js';
|
|
16
16
|
/**
|
|
@@ -336,6 +336,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
336
336
|
archonRunsInsideApp?: boolean;
|
|
337
337
|
archonDeployRunning?: string;
|
|
338
338
|
archonIdTokenUserinfo?: boolean;
|
|
339
|
+
authSignInPolicy?: boolean;
|
|
339
340
|
}>;
|
|
340
341
|
/**
|
|
341
342
|
* foundry#542: why the uptime Worker cannot run yet, which optional secrets
|
|
@@ -603,8 +604,7 @@ export interface PlatformArchonRuntime {
|
|
|
603
604
|
* org (`foundry:<orgId>`), with the platform's step key in the org id's place.
|
|
604
605
|
*/
|
|
605
606
|
export declare const PLATFORM_ARCHON_ACCESS_APP_NAME = "foundry:platform:archon";
|
|
606
|
-
|
|
607
|
-
export declare const PLATFORM_ZITADEL_ORG_SCOPE_KEY = "zitadel_platform_org";
|
|
607
|
+
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
608
608
|
/** Archon's OIDC client id: public, not a secret, so a plain scope id. */
|
|
609
609
|
export declare const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = "zitadel_platform_archon_client_id";
|
|
610
610
|
/**
|
|
@@ -1246,4 +1246,3 @@ export declare function filesPublicHostsValue(publicHosts: string | undefined, s
|
|
|
1246
1246
|
* from the declared orgs, for its apply and its drift check alike.
|
|
1247
1247
|
*/
|
|
1248
1248
|
export declare function filesShareDomains(orgs: readonly DeclaredOrg[]): string[];
|
|
1249
|
-
export {};
|
|
@@ -9,7 +9,7 @@ import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrSta
|
|
|
9
9
|
import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
|
|
10
10
|
import { healthCheckSettings } from './health-check-settings.js';
|
|
11
11
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
12
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_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_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_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_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, 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_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_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_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, 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_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_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, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
12
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_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_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_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_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, 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_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_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_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, 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_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_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_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
13
13
|
import { applyCallsSfuPlatformStep, callsSfuAppliedFingerprint, } from './platform-sfu.js';
|
|
14
14
|
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
15
15
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
@@ -365,6 +365,16 @@ export class FoundryPlatformStepExecutor {
|
|
|
365
365
|
console.error(`platform:archon: could not reach the platform's ZITADEL client to plan the ID token repair: ${e instanceof Error ? e.message : String(e)}`);
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
|
+
// Not a block: without the auth dependencies and a ZITADEL client for the instance the sign-in policy step is not planned.
|
|
369
|
+
if (this.auth && platform.auth.enabled && recordedIds[PLATFORM_ZITADEL_ORG_SCOPE_KEY]) {
|
|
370
|
+
try {
|
|
371
|
+
this.auth.zitadelForInstance?.(`https://${platform.auth.hostname}`);
|
|
372
|
+
blocked.authSignInPolicy = true;
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
console.error(`platform:auth: could not reach the platform's ZITADEL client to plan the sign-in policy: ${e instanceof Error ? e.message : String(e)}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
368
378
|
// Not a block: tells the planner to order Archon's step last (`PlanPlatformOptions.blocked.archonRunsInsideApp`).
|
|
369
379
|
if (this.archon?.runsInsideApp) {
|
|
370
380
|
blocked.archonRunsInsideApp = true;
|
|
@@ -608,6 +618,14 @@ export class FoundryPlatformStepExecutor {
|
|
|
608
618
|
}
|
|
609
619
|
return switchOnArchonIdTokenUserinfo(context, this.archon);
|
|
610
620
|
}
|
|
621
|
+
if (action.system === 'zitadel' &&
|
|
622
|
+
action.kind === 'sign-in-policy' &&
|
|
623
|
+
action.key === PLATFORM_AUTH_KEY) {
|
|
624
|
+
if (!this.auth) {
|
|
625
|
+
throw new Error('platform:auth needs FoundryPlatformStepExecutor to be constructed with its auth dependencies (#168)');
|
|
626
|
+
}
|
|
627
|
+
return applyPlatformSignInPolicy(context, this.auth);
|
|
628
|
+
}
|
|
611
629
|
if (action.system === 'cloudflare' && action.kind === 'access-provider') {
|
|
612
630
|
if (!this.accessProvider) {
|
|
613
631
|
throw new Error('cloudflare:access-provider needs FoundryPlatformStepExecutor to be constructed with its Access provider dependencies (#604)');
|
|
@@ -1820,29 +1838,47 @@ async function provisionPlatformAuth(context, deps, owners = []) {
|
|
|
1820
1838
|
}
|
|
1821
1839
|
}
|
|
1822
1840
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1841
|
+
return { resourceId: zitadelAppId };
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Reserves the username `admin` and switches self-registration off in the
|
|
1845
|
+
* platform's sign-in org (`zitadel:sign-in-policy` keyed {@link PLATFORM_AUTH_KEY}).
|
|
1846
|
+
* Each call records its own key right after it succeeds, so a failure in the
|
|
1847
|
+
* second leaves the first recorded and throws; the next plan names the step
|
|
1848
|
+
* again. The error names the org only, never the cause.
|
|
1849
|
+
*/
|
|
1850
|
+
async function applyPlatformSignInPolicy(context, deps) {
|
|
1851
|
+
const { platform } = context;
|
|
1852
|
+
const recorded = context.recordedIds();
|
|
1853
|
+
const orgId = recorded[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
|
|
1854
|
+
if (!orgId) {
|
|
1855
|
+
throw new Error("platform:auth: the sign-in's ZITADEL org is not recorded yet -- cannot reserve the admin username or close registration");
|
|
1856
|
+
}
|
|
1857
|
+
const zitadel = deps.zitadelForInstance?.(`https://${platform.auth.hostname}`) ?? deps.zitadel;
|
|
1858
|
+
const record = async (key, value) => {
|
|
1859
|
+
context.recordScopeId(key, value);
|
|
1860
|
+
await context.persist?.();
|
|
1861
|
+
};
|
|
1862
|
+
if (!recorded[PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY]) {
|
|
1863
|
+
let userId;
|
|
1864
|
+
try {
|
|
1865
|
+
({ userId } = await zitadel.reserveUserName(orgId, PLATFORM_AUTH_RESERVED_USERNAME));
|
|
1835
1866
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
await record(PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, userId);
|
|
1867
|
+
catch {
|
|
1868
|
+
throw new Error(`platform:auth: reserving the username '${PLATFORM_AUTH_RESERVED_USERNAME}' failed in ZITADEL org ${orgId}`);
|
|
1839
1869
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1870
|
+
await record(PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, userId);
|
|
1871
|
+
}
|
|
1872
|
+
if (!recorded[PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY]) {
|
|
1873
|
+
try {
|
|
1874
|
+
await zitadel.closeSelfRegistration(orgId);
|
|
1843
1875
|
}
|
|
1876
|
+
catch {
|
|
1877
|
+
throw new Error(`platform:auth: closing self-registration failed in ZITADEL org ${orgId}`);
|
|
1878
|
+
}
|
|
1879
|
+
await record(PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, 'closed');
|
|
1844
1880
|
}
|
|
1845
|
-
return {
|
|
1881
|
+
return {};
|
|
1846
1882
|
}
|
|
1847
1883
|
/** Lettermint's SMTP relay, as ZITADEL's SMTP provider wants it: `host:port`, STARTTLS. */
|
|
1848
1884
|
const PLATFORM_AUTH_SMTP_HOST = 'smtp.lettermint.co:587';
|
|
@@ -2326,7 +2362,10 @@ async function provisionPlatformMail(context, deps) {
|
|
|
2326
2362
|
const { project } = await zitadel.findOrCreateProject(org.id, PLATFORM_MAIL_ZITADEL_PROJECT_NAME);
|
|
2327
2363
|
const recordedProjectId = recorded[PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY];
|
|
2328
2364
|
const adminKeyRecorded = Boolean(recorded[PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY]);
|
|
2329
|
-
|
|
2365
|
+
// #661: a platform applied before the local-delivery route existed gets it
|
|
2366
|
+
// here, over the same recovery-admin credential, and mints nothing.
|
|
2367
|
+
const routeRecorded = recorded[PLATFORM_MAIL_ROUTE_SCOPE_KEY] === PLATFORM_MAIL_ROUTE_WANTED;
|
|
2368
|
+
if (adminKeyRecorded && (recordedProjectId !== project.id || !routeRecorded)) {
|
|
2330
2369
|
// #346 round 3: the OIDC-directory write below (next block) runs only
|
|
2331
2370
|
// once, while the admin key is unrecorded -- so once it is recorded, a
|
|
2332
2371
|
// changed project id (the project was deleted and recreated, ZITADEL was
|
|
@@ -2366,6 +2405,7 @@ async function provisionPlatformMail(context, deps) {
|
|
|
2366
2405
|
},
|
|
2367
2406
|
});
|
|
2368
2407
|
await record(PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, project.id);
|
|
2408
|
+
await record(PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED);
|
|
2369
2409
|
}
|
|
2370
2410
|
catch (error) {
|
|
2371
2411
|
throw redactedMailError(error, [password]);
|
|
@@ -2405,6 +2445,7 @@ async function provisionPlatformMail(context, deps) {
|
|
|
2405
2445
|
oidc: { issuerUrl: issuer, audience: project.id },
|
|
2406
2446
|
},
|
|
2407
2447
|
});
|
|
2448
|
+
await record(PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED);
|
|
2408
2449
|
// Recorded and persisted before the secret leaves this function: if
|
|
2409
2450
|
// delivery fails for good, the next plan reports the lost key rather
|
|
2410
2451
|
// than skipping or minting again.
|
|
@@ -2718,9 +2759,8 @@ const PLATFORM_ARCHON_PROJECT_SCOPE_KEY = 'coolify_project_platform_archon';
|
|
|
2718
2759
|
const PLATFORM_ARCHON_DB_SCOPE_KEY = 'coolify_platform_archon_database';
|
|
2719
2760
|
const PLATFORM_ARCHON_TUNNEL_SCOPE_KEY = 'cloudflare_platform_archon_tunnel';
|
|
2720
2761
|
const PLATFORM_ARCHON_ACCESS_SCOPE_KEY = 'cloudflare_platform_archon_access_app';
|
|
2721
|
-
/** The platform's own ZITADEL org (`foundry`, the one `provisionPlatformAuth` mints its admin user in). */
|
|
2722
|
-
export const PLATFORM_ZITADEL_ORG_SCOPE_KEY = 'zitadel_platform_org';
|
|
2723
2762
|
const PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY = 'zitadel_platform_archon_project';
|
|
2763
|
+
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
2724
2764
|
/** The hostname the sign-in client's redirect URIs were made for; a change makes a new client. */
|
|
2725
2765
|
const PLATFORM_ARCHON_CLIENT_HOSTNAME_SCOPE_KEY = 'zitadel_platform_archon_client_hostname';
|
|
2726
2766
|
/** Archon's OIDC client id: public, not a secret, so a plain scope id. */
|
|
@@ -21,8 +21,7 @@ import type { VersionChangeItem } from '../plan.js';
|
|
|
21
21
|
* `coolifyApplicationUuid` is the id `plan/diff.ts`'s `appStepKey` keys an
|
|
22
22
|
* org's hosting entry under in `AppliedState.serviceIds`
|
|
23
23
|
* (`` `coolify:app:${appStepKey(org, entry.key)}` ``, `org.ts`); `repo` is
|
|
24
|
-
* `HostingEntry.repo
|
|
25
|
-
* declared resolves to (`plan/executor.ts`'s own `DEFAULT_APP_REPO`).
|
|
24
|
+
* `HostingEntry.repo`.
|
|
26
25
|
*/
|
|
27
26
|
export interface OrgAppVersionRef {
|
|
28
27
|
orgId: OrgId;
|
package/dist/plan.d.ts
CHANGED
|
@@ -99,4 +99,16 @@ export interface VersionChangeItem {
|
|
|
99
99
|
/** The service this line is about, e.g. 'mail', 'archon', 'zitadel'. */
|
|
100
100
|
service: string;
|
|
101
101
|
description: string;
|
|
102
|
+
/** The version running or last applied; absent when that is unknown. */
|
|
103
|
+
from?: string;
|
|
104
|
+
/** The version the declaration or the foundry pin now asks for. */
|
|
105
|
+
to?: string;
|
|
106
|
+
/**
|
|
107
|
+
* True when `apply` and `applyPlatform` refuse the plan until
|
|
108
|
+
* `confirmUpgrade` names {@link service}: any change on a stateful service
|
|
109
|
+
* (`plan/confirm-upgrade.ts`), or a major or minor jump on any service. An
|
|
110
|
+
* unknown running version is never a change, so it is false. So is a line
|
|
111
|
+
* `apply` does not act on ("not applied by foundry").
|
|
112
|
+
*/
|
|
113
|
+
needsConfirmation: boolean;
|
|
102
114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@wtfalch/audit": "0.6.0",
|
|
46
46
|
"@wtfalch/auth": "0.16.0",
|
|
47
47
|
"@wtfalch/edge": "0.12.0",
|
|
48
|
-
"@wtfalch/email": "^0.
|
|
48
|
+
"@wtfalch/email": "^0.20.0",
|
|
49
49
|
"@wtfalch/providers": "0.7.5",
|
|
50
50
|
"ssh2": "1.17.0",
|
|
51
51
|
"zod": "4.6.5"
|