@wtfalch/foundry 0.16.16 → 0.17.1

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.
Files changed (67) hide show
  1. package/dist/apply.d.ts +22 -1
  2. package/dist/bootstrap/bootstrap.d.ts +6 -1
  3. package/dist/bootstrap/bootstrap.js +26 -3
  4. package/dist/bootstrap/declare.d.ts +11 -1
  5. package/dist/bootstrap/declare.js +18 -6
  6. package/dist/index.d.ts +11 -6
  7. package/dist/index.js +8 -1
  8. package/dist/migrations/0038_calls_version_pin.sql +8 -0
  9. package/dist/migrations/0039_destroy_record.sql +7 -0
  10. package/dist/migrations/0040_company_included_uuid.sql +72 -0
  11. package/dist/migrations/0041_company_included_uuid_keys.sql +132 -0
  12. package/dist/migrations/0042_mutation_leases.sql +12 -0
  13. package/dist/migrations/0043_platform_names.sql +25 -0
  14. package/dist/migrations/0044_platform_mail_lettermint_project.sql +11 -0
  15. package/dist/org.d.ts +128 -3
  16. package/dist/org.js +186 -1
  17. package/dist/plan/adopt.d.ts +76 -0
  18. package/dist/plan/adopt.js +184 -0
  19. package/dist/plan/agora.d.ts +21 -0
  20. package/dist/plan/agora.js +37 -0
  21. package/dist/plan/broker-entries.d.ts +9 -1
  22. package/dist/plan/broker-entries.js +13 -5
  23. package/dist/plan/chat-matrix.d.ts +1 -2
  24. package/dist/plan/chat-matrix.js +15 -4
  25. package/dist/plan/core-files.d.ts +25 -0
  26. package/dist/plan/core-files.js +25 -0
  27. package/dist/plan/diff.d.ts +27 -3
  28. package/dist/plan/diff.js +264 -86
  29. package/dist/plan/edge-worker.d.ts +114 -0
  30. package/dist/plan/edge-worker.js +201 -0
  31. package/dist/plan/entry-ai-key.d.ts +3 -3
  32. package/dist/plan/entry-files-key.d.ts +3 -3
  33. package/dist/plan/entry-names-key.d.ts +73 -0
  34. package/dist/plan/entry-names-key.js +236 -0
  35. package/dist/plan/executor.d.ts +292 -24
  36. package/dist/plan/executor.js +745 -77
  37. package/dist/plan/index.d.ts +6 -2
  38. package/dist/plan/index.js +3 -1
  39. package/dist/plan/live.d.ts +9 -2
  40. package/dist/plan/login-host.d.ts +13 -2
  41. package/dist/plan/login-host.js +26 -7
  42. package/dist/plan/plan-apply.d.ts +153 -6
  43. package/dist/plan/plan-apply.js +639 -78
  44. package/dist/plan/platform-diff.d.ts +106 -1
  45. package/dist/plan/platform-diff.js +182 -2
  46. package/dist/plan/platform-org.d.ts +45 -1
  47. package/dist/plan/platform-org.js +34 -3
  48. package/dist/plan/platform-providers.d.ts +45 -1
  49. package/dist/plan/platform-providers.js +585 -16
  50. package/dist/plan/providers-readers.d.ts +2 -0
  51. package/dist/plan/providers-readers.js +4 -0
  52. package/dist/plan/rollout.d.ts +52 -0
  53. package/dist/plan/rollout.js +37 -0
  54. package/dist/plan/step-system.d.ts +22 -0
  55. package/dist/plan/step-system.js +38 -0
  56. package/dist/plan.d.ts +3 -0
  57. package/dist/platform.d.ts +22 -3
  58. package/dist/platform.js +6 -0
  59. package/dist/state.d.ts +13 -0
  60. package/dist/store/audit.js +15 -1
  61. package/dist/store/postgres.d.ts +1 -1
  62. package/dist/store/postgres.js +142 -15
  63. package/dist/store/schema.d.ts +202 -5
  64. package/dist/store/schema.js +18 -1
  65. package/dist/store.d.ts +47 -4
  66. package/dist/store.js +9 -1
  67. package/package.json +1 -1
@@ -2,16 +2,20 @@ export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, File
2
2
  export { readLiveResources, readLivePlatformResources } from './live.js';
3
3
  export type { StepAction, PlannedStep, PlanDetail } from './diff.js';
4
4
  export { computePlanDetail, toPlan } from './diff.js';
5
+ export type { StepSystem } from './step-system.js';
6
+ export { stepSystemOf } from './step-system.js';
5
7
  export type { PlatformPlanDetail } from './platform-diff.js';
6
- export { planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, } from './platform-diff.js';
8
+ export { planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_NAMES_KEY, PLATFORM_NAMES_KEY_REF, PLATFORM_NAMES_CLOUDFLARE_TOKEN_REF, } from './platform-diff.js';
7
9
  export type { HeldKeyResolver, OrgPlatformReadiness } from './platform-org.js';
8
10
  export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
9
11
  export type { PlatformOrgClientFactories } from './platform-org-ports.js';
10
12
  export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
13
+ export type { AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot } from './adopt.js';
14
+ export { AdoptRefusedError } from './adopt.js';
11
15
  export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan-apply.js';
12
16
  export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
13
17
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
14
- export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './platform-providers.js';
18
+ export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformNamesDeps, PlatformNamesRuntime, } from './platform-providers.js';
15
19
  export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
16
20
  export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
17
21
  export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
@@ -1,8 +1,10 @@
1
1
  export { readLiveResources, readLivePlatformResources } from './live.js';
2
2
  export { computePlanDetail, toPlan } from './diff.js';
3
- export { planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, } from './platform-diff.js';
3
+ export { stepSystemOf } from './step-system.js';
4
+ export { planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_NAMES_KEY, PLATFORM_NAMES_KEY_REF, PLATFORM_NAMES_CLOUDFLARE_TOKEN_REF, } from './platform-diff.js';
4
5
  export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
5
6
  export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
7
+ export { AdoptRefusedError } from './adopt.js';
6
8
  export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
7
9
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
8
10
  // #171: how a host signs files management requests with the delivered key.
@@ -26,9 +26,11 @@ export interface LiveResource {
26
26
  * `platform-diff.ts`'s `planSfu` always plans its step when
27
27
  * `DeclaredPlatform.sfu.enabled` is true, rather than diffing against a
28
28
  * live resource the way every other system here does. It is still part of
29
- * this union so `StepAction.system` (`diff.ts`) can name it.
29
+ * this union so `StepAction.system` (`diff.ts`) can name it. `'names'`
30
+ * (#317) is the same: a names key has no live lookup of its own, the same
31
+ * shape `ai`/`files` entry keys already follow.
30
32
  */
31
- system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu';
33
+ system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu' | 'names';
32
34
  kind: string;
33
35
  key: string;
34
36
  config: Record<string, unknown>;
@@ -48,6 +50,11 @@ export interface ZitadelReader {
48
50
  }
49
51
  export interface CoolifyReader {
50
52
  listResources(orgId: OrgId): Promise<LiveResource[]>;
53
+ /**
54
+ * Reads one Coolify project by its uuid, for an org that records none yet
55
+ * (adopt, archon#271). Optional: without it nothing can be adopted.
56
+ */
57
+ listProject?(projectUuid: string): Promise<LiveResource[]>;
51
58
  }
52
59
  export interface CloudflareReader {
53
60
  listResources(orgId: OrgId): Promise<LiveResource[]>;
@@ -21,8 +21,19 @@ import type { DeclaredOrg } from '../org.js';
21
21
  * `RESERVED_AUTH_AND_ENVIRONMENTS` already reserves the `auth` *slug* so no
22
22
  * org's own `<slug>.oikos.host` can collide with this subdomain, the same
23
23
  * way `AGORA_SUFFIX` reserves `-agora`.
24
+ *
25
+ * #488: `auth.<domain>` is refused when it would equal `platformAuthHostname`
26
+ * -- the platform's own org, declared with-domain on the platform's own
27
+ * domain, would otherwise compute the shared ZITADEL instance's own login
28
+ * host (`platform.auth.hostname`, e.g. `auth.companyincriminated.com` ->
29
+ * `auth.companyincluded.com`). Archon's login-host write throws on that
30
+ * duplicate host (`archon`'s `validateLoginHostRegistry`); if it were ever
31
+ * written instead, `auth`'s own `company-host.ts` denies every request
32
+ * through a duplicate host, the shared one included. `platformAuthHostname`
33
+ * is `null` when the caller has none to compare against (no platform
34
+ * declared, or sign-in not ready yet), in which case this never refuses.
24
35
  */
25
- export declare function loginHostnameFor(org: DeclaredOrg): string;
36
+ export declare function loginHostnameFor(org: DeclaredOrg, platformAuthHostname?: string | null): string;
26
37
  /**
27
38
  * One entry in zitadel-login's own `LOGIN_HOST_BINDINGS`
28
39
  * (`auth/docs/company-login-hosts.md`) -- a single-line JSON document naming
@@ -66,7 +77,7 @@ export interface LoginHostBindingsPort {
66
77
  * on the next run.
67
78
  */
68
79
  export declare const LOGIN_BINDING_SCOPE_KEY = "login_host_binding";
69
- /** The org's recorded app client ids (hosted app, each entry's own, cms, agora), sorted. */
80
+ /** The org's recorded app client ids (hosted app, each entry's own, cms, agora, boule), deduplicated and sorted. */
70
81
  export declare function loginHostClientIds(recorded: Readonly<Record<string, string>>): string[];
71
82
  /** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
72
83
  export declare function loginBindingState(host: string, clientIds: readonly string[]): string;
@@ -1,4 +1,5 @@
1
1
  import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, } from './agora.js';
2
+ import { BOULE_CLIENT_ID_SCOPE_KEY } from './boule.js';
2
3
  /**
3
4
  * #267: what both the planner (`diff.ts`) and the executor (`executor.ts`)
4
5
  * need to agree on for an org's own company login host -- the hostname and
@@ -21,12 +22,30 @@ import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE
21
22
  * `RESERVED_AUTH_AND_ENVIRONMENTS` already reserves the `auth` *slug* so no
22
23
  * org's own `<slug>.oikos.host` can collide with this subdomain, the same
23
24
  * way `AGORA_SUFFIX` reserves `-agora`.
25
+ *
26
+ * #488: `auth.<domain>` is refused when it would equal `platformAuthHostname`
27
+ * -- the platform's own org, declared with-domain on the platform's own
28
+ * domain, would otherwise compute the shared ZITADEL instance's own login
29
+ * host (`platform.auth.hostname`, e.g. `auth.companyincriminated.com` ->
30
+ * `auth.companyincluded.com`). Archon's login-host write throws on that
31
+ * duplicate host (`archon`'s `validateLoginHostRegistry`); if it were ever
32
+ * written instead, `auth`'s own `company-host.ts` denies every request
33
+ * through a duplicate host, the shared one included. `platformAuthHostname`
34
+ * is `null` when the caller has none to compare against (no platform
35
+ * declared, or sign-in not ready yet), in which case this never refuses.
24
36
  */
25
- export function loginHostnameFor(org) {
37
+ export function loginHostnameFor(org, platformAuthHostname = null) {
26
38
  // `issuer.usesDomain` is the issuer's opt-in into the org's domain
27
39
  // (`org.ts`'s `IssuerConfig`): a domain alone never moves it, so a domain
28
40
  // with `usesDomain` false stays on the tier 1 host.
29
- return org.domain && org.issuer.usesDomain ? `auth.${org.domain}` : `${org.slug}-auth.oikos.host`;
41
+ if (org.domain && org.issuer.usesDomain) {
42
+ const host = `auth.${org.domain}`;
43
+ if (platformAuthHostname && host === platformAuthHostname) {
44
+ return `${org.slug}-auth.oikos.host`;
45
+ }
46
+ return host;
47
+ }
48
+ return `${org.slug}-auth.oikos.host`;
30
49
  }
31
50
  /**
32
51
  * Scope id holding what the binding was last written with
@@ -36,18 +55,18 @@ export function loginHostnameFor(org) {
36
55
  * on the next run.
37
56
  */
38
57
  export const LOGIN_BINDING_SCOPE_KEY = 'login_host_binding';
39
- /** The org's recorded app client ids (hosted app, each entry's own, cms, agora), sorted. */
58
+ /** The org's recorded app client ids (hosted app, each entry's own, cms, agora, boule), deduplicated and sorted. */
40
59
  export function loginHostClientIds(recorded) {
41
- return [
60
+ const ids = [
42
61
  recorded[APP_CLIENT_ID_SCOPE_KEY],
43
62
  recorded[CMS_CLIENT_ID_SCOPE_KEY],
44
63
  recorded[AGORA_CLIENT_ID_SCOPE_KEY],
64
+ recorded[BOULE_CLIENT_ID_SCOPE_KEY],
45
65
  ...Object.entries(recorded)
46
66
  .filter(([key]) => key.startsWith(ENTRY_CLIENT_ID_SCOPE_PREFIX))
47
67
  .map(([, id]) => id),
48
- ]
49
- .filter((id) => Boolean(id))
50
- .sort();
68
+ ].filter((id) => Boolean(id));
69
+ return [...new Set(ids)].sort();
51
70
  }
52
71
  /** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
53
72
  export function loginBindingState(host, clientIds) {
@@ -1,9 +1,11 @@
1
- import type { PlanApply } from '../apply.js';
1
+ import type { DestroyProgress, PlanApply } from '../apply.js';
2
2
  import type { DeclaredOrg, OrgId } from '../org.js';
3
3
  import type { Plan, PlanStep } from '../plan.js';
4
4
  import type { DeclaredPlatform } from '../platform.js';
5
+ import type { JobRunner } from '../runner/types.js';
5
6
  import type { AppliedState } from '../state.js';
6
- import type { AppliedVersionsStore, DeclaredOrgStore, PlatformStore } from '../store.js';
7
+ import { type AppliedVersionsStore, type DeclaredOrgStore, type PlatformStore } from '../store.js';
8
+ import { type AdoptCandidate, type AdoptInput, type AdoptPlan } from './adopt.js';
7
9
  import { type AgoraHostPort } from './agora.js';
8
10
  import { type StepAction } from './diff.js';
9
11
  import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
@@ -50,6 +52,16 @@ export interface AuditLog {
50
52
  target?: 'platform';
51
53
  at: Date;
52
54
  }): Promise<void>;
55
+ /** `FoundryPlanApply.adopt` and `release` (archon#271): no `Plan` is applied, so this is its own entry. `entryKey` names the hosting entry. */
56
+ recordAdoption(entry: {
57
+ actor: AuditActor;
58
+ orgId: OrgId;
59
+ action: 'adopt' | 'release';
60
+ entryKey: string;
61
+ outcome: 'adopted' | 'released' | 'refused';
62
+ reason?: string;
63
+ at: Date;
64
+ }): Promise<void>;
53
65
  /** `FoundryPlanApply.destroy`'s own record -- no `Plan` involved, so this is its own entry shape rather than a reuse of `recordApply`'s. */
54
66
  recordDestroy(entry: {
55
67
  actor: AuditActor;
@@ -249,6 +261,13 @@ export interface StepDestroyer {
249
261
  readonly recordedIds: Readonly<Record<string, string>>;
250
262
  }): Promise<void>;
251
263
  }
264
+ /** #441: another apply, destroy, adopt or release holds the lease for this company or the platform. Nothing ran; try again when it finishes. */
265
+ export declare class MutationInProgressError extends Error {
266
+ readonly scope: string;
267
+ readonly heldUntil: Date;
268
+ /** @param heldUntil when the holder's lease runs out if it is never released. */
269
+ constructor(scope: string, heldUntil: Date);
270
+ }
252
271
  export declare class DeclaredOrgNotFoundError extends Error {
253
272
  constructor(orgId: OrgId);
254
273
  }
@@ -314,6 +333,12 @@ export interface FoundryPlanApplyOptions {
314
333
  * reported blocked instead of planned.
315
334
  */
316
335
  broker?: boolean;
336
+ /**
337
+ * #318: true when the host's executor was built with a `WorkersPort`.
338
+ * Optional, and false without it: each entry's `edge` Worker is then
339
+ * reported blocked instead of planned.
340
+ */
341
+ edgeWorkers?: boolean;
317
342
  /**
318
343
  * #177: true when the host's executor was built with its own `filesOrganisation`
319
344
  * / `aiOrganisation` port. Without one, the org step needs the platform's
@@ -321,8 +346,35 @@ export interface FoundryPlanApplyOptions {
321
346
  */
322
347
  filesOrganisation?: boolean;
323
348
  aiOrganisation?: boolean;
349
+ /**
350
+ * #317: true when the host's executor was built with a `NamesPort`.
351
+ * Optional, and false without it: each entry's `uses.names` step is then
352
+ * reported blocked instead of planned to fail.
353
+ */
354
+ names?: boolean;
324
355
  /** Overridable for tests; defaults to `() => new Date()`. */
325
356
  now?: () => Date;
357
+ /** #441: how long a mutation lease lasts before another caller may take it over. Defaults to 30 minutes. */
358
+ leaseTtlMs?: number;
359
+ /**
360
+ * #441 (part b): a durable, DB-row-backed record of `apply`, `destroy`,
361
+ * `adopt` and `release` for one org, held under the same mutation lease
362
+ * above rather than instead of it -- the lease is what actually keeps two
363
+ * holders from running `fn` at once (`withRunnerSerialization`'s own doc
364
+ * comment explains why the runner cannot safely take that job on its
365
+ * own). Optional, and a no-op without it -- every existing caller of this
366
+ * constructor is unaffected, and the lease alone still serializes every
367
+ * mutation the way it already did (#441, part a).
368
+ *
369
+ * What it adds: a row that never expires on a timer the way the lease
370
+ * does, so a crashed holder leaves something a host can see and query
371
+ * (`JobRunner.currentRun`/`getRun`) rather than nothing but a scope that
372
+ * quietly frees itself. Resuming after a crash reuses that row's id
373
+ * rather than starting a fresh one. Pass the same host-supplied
374
+ * `JobRunner` (`createJobRunner`, migrated with `runner/schema.ts`) that a
375
+ * host may also poll directly for progress.
376
+ */
377
+ runner?: JobRunner;
326
378
  /**
327
379
  * #253: appends a history row every time `runSteps` records a new
328
380
  * version as applied, beside the single-snapshot `store.putAppliedState`
@@ -376,13 +428,54 @@ export declare class FoundryPlanApply implements PlanApply {
376
428
  private readonly jobsScheduler;
377
429
  private readonly loginHostBindings;
378
430
  private readonly broker;
431
+ private readonly edgeWorkers;
379
432
  private readonly hostFiles;
380
433
  private readonly hostAi;
434
+ private readonly hostNames;
381
435
  private readonly now;
436
+ private readonly leaseTtlMs;
382
437
  private readonly appliedVersions;
383
438
  private readonly platformDeps;
439
+ private readonly runner;
384
440
  constructor(options: FoundryPlanApplyOptions);
385
441
  plan(id: OrgId): Promise<Plan>;
442
+ /**
443
+ * Archon#271: what adopting a live app and database into entry
444
+ * `input.entryKey` would do. A read, like {@link plan}: it reads the project
445
+ * through the reader and changes nothing. A mismatch is reported in the
446
+ * result, with no steps. Needs the apply permission, since it reads a
447
+ * project the caller names.
448
+ */
449
+ planAdopt(id: OrgId, input: AdoptInput): Promise<AdoptPlan>;
450
+ /**
451
+ * Archon#271: the live apps and databases in `projectUuid` this org has not
452
+ * recorded, for Changes' "Live, not theirs". A read, but it needs the apply
453
+ * permission: it reads a project the caller names.
454
+ */
455
+ adoptCandidates(id: OrgId, entryKey: string, projectUuid?: string): Promise<AdoptCandidate[]>;
456
+ /**
457
+ * Archon#271: records an app and database that already run in Coolify as
458
+ * entry `input.entryKey`'s resources, so the next plan finds them and plans
459
+ * no create. Verifies first and refuses on a mismatch or an entry that is
460
+ * already applied or held by another org. Same permission as {@link apply}.
461
+ * It never calls an executor: nothing in Coolify changes. Nothing redeploys
462
+ * here, but the next normal plan may propose updates to the adopted app
463
+ * that do (see `adopt.ts`); show that plan before applying it. Two
464
+ * concurrent adopts of one resource are not locked against each other.
465
+ */
466
+ adopt(id: OrgId, input: AdoptInput): Promise<AppliedState>;
467
+ /**
468
+ * Archon#271: the undo of {@link adopt}. Drops the entry's recorded Coolify
469
+ * ids and its `adopted.<entryKey>` snapshot (and `coolify_project` when
470
+ * adopt recorded it and no other Coolify id remains). It leaves the
471
+ * declaration alone: the entry goes back to declared and not applied, so the
472
+ * next plan creates it, exactly as before adopt. The host then adopts it
473
+ * again or removes the entry from the declaration. Deletes and stops nothing
474
+ * in Coolify. Only an adopted entry can be released. Same permission as
475
+ * {@link apply}. Every path writes an audit row.
476
+ */
477
+ release(id: OrgId, entryKey: string): Promise<AppliedState>;
478
+ private computeAdopt;
386
479
  apply(plan: Plan, confirmedOrgName: string): Promise<AppliedState>;
387
480
  /**
388
481
  * After an org apply: gives the platform files runner a changed share-domain
@@ -390,9 +483,10 @@ export declare class FoundryPlanApply implements PlanApply {
390
483
  * declared orgs) without a platform pass. Touches only the share hosts and
391
484
  * redeploys the runner; the platform files step itself never runs here.
392
485
  * Skips quietly for an actor who may not apply the platform, and when the
393
- * platform files step was never applied. There is no platform lock, so the
394
- * service ids are re-read just before the write and only the share-domains
395
- * key is recorded. A failure is rethrown naming that the org part succeeded.
486
+ * platform files step was never applied. It holds the platform lease for
487
+ * the refresh and fences its write with it; while another platform change
488
+ * holds the lease it skips (this class has no log), and the next platform
489
+ * apply does the refresh. Only the share-domains key is recorded. A failure is rethrown naming that the org part succeeded.
396
490
  */
397
491
  private refreshPlatformFilesShare;
398
492
  /**
@@ -421,7 +515,60 @@ export declare class FoundryPlanApply implements PlanApply {
421
515
  * declared org in place with whatever remains recorded in its applied
422
516
  * state, so a retried `destroy` only tears down what's left.
423
517
  */
424
- destroy(id: OrgId, confirmedOrgName: string): Promise<void>;
518
+ destroy(id: OrgId, confirmedOrgName: string, options?: {
519
+ onProgress?: (event: DestroyProgress) => void;
520
+ }): Promise<void>;
521
+ /** The steps `destroy` runs for `id`, in order. Refuses a non-test org like `destroy` does; writes no audit row since it changes nothing. */
522
+ planDestroy(id: OrgId): Promise<{
523
+ steps: string[];
524
+ kept: string[];
525
+ }>;
526
+ /**
527
+ * #441 (part b): records `fn`'s run in the durable runner, when one is
528
+ * configured, and calls it. Must run with the mutation lease already
529
+ * held -- every one of this method's four callers (`apply`, `destroy`,
530
+ * `adopt`, `release`) calls it from inside its own `withMutationLease`
531
+ * callback, never around it. The reason is `JobRunner.runSteps` itself:
532
+ * it checkpoints a run's one step (the literal id `'mutate'`, the same
533
+ * for all four callers so they share one row per org) by a plain read of
534
+ * the row followed by a later blind write, with nothing to stop two
535
+ * concurrent calls for the same `runId` from both reading "not yet
536
+ * checkpointed" and both calling `fn` -- the runner sequences and
537
+ * checkpoints (its own doc comment), it does not itself exclude a second
538
+ * caller the way the lease's atomic acquire does. Holding the lease first
539
+ * is what makes it safe to reuse an existing, not-yet-succeeded run
540
+ * ({@link JobRunner.currentRun}) rather than refuse it outright: by the
541
+ * time this method runs, this caller is already the lease's sole holder,
542
+ * so any row `currentRun` finds can only be a previous, now-abandoned
543
+ * attempt (the lease's own atomic acquire already refused a second live
544
+ * caller before either of them got here) -- reusing it is the resume #9
545
+ * calls "a human decides when to call it again", not a race.
546
+ *
547
+ * Two *first* attempts for the same org can still race the insert itself
548
+ * when neither has an existing row yet -- `JobRunner.enqueueApply`'s own
549
+ * unique constraint decides it at the database level, a real race rather
550
+ * than one decided by this process's await order, and the loser's
551
+ * {@link AlreadyRunningError} is refused and audited through `refuse`,
552
+ * the same as a lost lease. In practice this needs two lease holders at
553
+ * once to reach it, which the lease itself already prevents; it is this
554
+ * method's defence against a bug in the lease's own acquire path, not
555
+ * the primary lock.
556
+ *
557
+ * `fn`'s result is threaded out through a closure, since
558
+ * {@link RunnerStep.run} returns nothing.
559
+ */
560
+ private withRunnerSerialization;
561
+ /**
562
+ * #441: runs `fn` while holding the one mutation lease for `scope`, so two
563
+ * `FoundryPlanApply` instances on one store never run provider steps for the
564
+ * same company or platform at once. A second caller is refused with
565
+ * {@link MutationInProgressError}; `fn` gets the lease as the fence its
566
+ * store writes carry, so a worker that lost the lease cannot overwrite the
567
+ * newer holder's state. Both refusals are audited through `refuse`.
568
+ */
569
+ private withMutationLease;
570
+ /** #441: before each provider step, extends the lease; a holder that lost it stops here, before the step runs. */
571
+ private renewLease;
425
572
  private refuse;
426
573
  private refusePlatform;
427
574
  private refuseDestroy;