@wtfalch/foundry 0.9.0 → 0.11.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.
Files changed (44) hide show
  1. package/dist/apply.d.ts +16 -0
  2. package/dist/index.d.ts +4 -3
  3. package/dist/index.js +1 -1
  4. package/dist/migrations/0030_hosting_entry_hostnames.sql +24 -0
  5. package/dist/migrations/0031_adoptions.sql +37 -0
  6. package/dist/org.d.ts +49 -4
  7. package/dist/org.js +134 -1
  8. package/dist/plan/adopt.d.ts +112 -0
  9. package/dist/plan/adopt.js +252 -0
  10. package/dist/plan/adoption-vault.d.ts +9 -0
  11. package/dist/plan/adoption-vault.js +1 -0
  12. package/dist/plan/agora.d.ts +7 -0
  13. package/dist/plan/agora.js +9 -0
  14. package/dist/plan/broker-entries.d.ts +108 -0
  15. package/dist/plan/broker-entries.js +214 -0
  16. package/dist/plan/cloudflared-connector.d.ts +10 -2
  17. package/dist/plan/cloudflared-connector.js +24 -16
  18. package/dist/plan/diff.d.ts +14 -0
  19. package/dist/plan/diff.js +488 -47
  20. package/dist/plan/executor.d.ts +141 -3
  21. package/dist/plan/executor.js +1100 -149
  22. package/dist/plan/hostnames.d.ts +24 -0
  23. package/dist/plan/hostnames.js +56 -0
  24. package/dist/plan/index.d.ts +2 -0
  25. package/dist/plan/index.js +1 -0
  26. package/dist/plan/locked-hostnames.d.ts +8 -0
  27. package/dist/plan/locked-hostnames.js +26 -0
  28. package/dist/plan/login-host.d.ts +1 -1
  29. package/dist/plan/login-host.js +5 -2
  30. package/dist/plan/plan-apply.d.ts +63 -3
  31. package/dist/plan/plan-apply.js +163 -5
  32. package/dist/plan/platform-diff.d.ts +66 -13
  33. package/dist/plan/platform-diff.js +117 -15
  34. package/dist/plan/platform-providers.d.ts +95 -2
  35. package/dist/plan/platform-providers.js +393 -27
  36. package/dist/plan/providers-readers.d.ts +8 -1
  37. package/dist/plan/providers-readers.js +58 -4
  38. package/dist/plan.d.ts +6 -0
  39. package/dist/store/postgres.d.ts +6 -1
  40. package/dist/store/postgres.js +80 -5
  41. package/dist/store/schema.d.ts +162 -0
  42. package/dist/store/schema.js +19 -1
  43. package/dist/store.d.ts +37 -0
  44. package/package.json +1 -1
@@ -316,26 +316,67 @@ export declare function platformMailMxHost(apex: string): string;
316
316
  export declare const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = "mail_applied_apex";
317
317
  export declare const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = "mail_applied_host";
318
318
  /**
319
- * The key of an org's `app` hosting entry in a `coolify:app` or
320
- * `cloudflare:access` step and in the ids recorded for it:
321
- * `<orgId>:<entry key>`, so a second entry cannot collide with the first.
319
+ * The key of a hosting entry in a `coolify:app` or `cloudflare:access` step
320
+ * and in the ids recorded for it: `<orgId>:<entry key>`, so a second entry
321
+ * cannot collide with the first.
322
322
  */
323
- export declare function appStepKey(org: DeclaredOrg): string;
323
+ export declare function appStepKey(org: DeclaredOrg, entryKey?: string): string;
324
324
  /**
325
- * #243: the org's `app` entry's `env` values the last `coolify:app` apply
326
- * pushed, recorded as one JSON object (env name to value; ids and a domain,
327
- * never a secret). `planApp` plans the step again when {@link resolveAppEnv}
328
- * differs from it, so a changed map reaches an app that is already live. The
329
- * `.app` suffix is the entry key; a scope key holds no `:`, so
330
- * `storeRecordedIds` still filters it out.
325
+ * The Coolify name of a hosting entry's app: `<slug>` for the `app` entry, so
326
+ * the move to a list finds the app that runs today, `<slug>-<key>` for any
327
+ * other.
331
328
  */
332
- export declare const APP_ENV_APPLIED_SCOPE_KEY = "app_env_applied.app";
329
+ export declare function appNameFor(org: DeclaredOrg, entry: HostingEntry): string;
330
+ /**
331
+ * #243: the entry's `env` values the last `coolify:app` apply pushed,
332
+ * recorded as one JSON object (env name to value; ids and a domain, never a
333
+ * secret). `planApp` plans the step again when {@link resolveAppEnv} differs
334
+ * from it, so a changed map reaches an app that is already live. The `.<key>`
335
+ * suffix is the entry key; a scope key holds no `:`, so `storeRecordedIds`
336
+ * still filters it out.
337
+ */
338
+ export declare function appEnvAppliedScopeKey(entryKey: string): string;
333
339
  /**
334
340
  * #239: set to '1' once an app has been restarted after its database URLs
335
341
  * were pushed. Not a URL. Without it a crash between the push and the restart
336
342
  * would leave a stale container that no plan notices.
337
343
  */
338
- export declare const DB_ENV_APPLIED_SCOPE_KEY = "db_env_applied.app";
344
+ export declare function dbEnvAppliedScopeKey(entryKey: string): string;
345
+ /**
346
+ * The settings `coolifyApp` owns on an entry's app (`folder`, `port`,
347
+ * `autoDeploy`) as the JSON the marker below holds. `coolifyApp` records it
348
+ * on create and after it patches the app, and `planApp` plans the step again
349
+ * when the declared settings differ from it.
350
+ */
351
+ export interface AppSettings {
352
+ folder: string | null;
353
+ port: number | null;
354
+ autoDeploy: boolean;
355
+ }
356
+ export declare function appSettingsOf(entry: HostingEntry): AppSettings;
357
+ /** What an app that predates the marker has: Coolify's own defaults, so it plans no change. */
358
+ export declare const DEFAULT_APP_SETTINGS: AppSettings;
359
+ export declare function appSettingsAppliedScopeKey(entryKey: string): string;
360
+ /**
361
+ * Set before an adopted app's settings are patched, cleared once its deploy
362
+ * succeeded (an empty value is not pending): a failed deploy replans whatever
363
+ * the markers say (#311).
364
+ */
365
+ export declare function appDeployPendingScopeKey(entryKey: string): string;
366
+ /** The settings the app has: the marker, or the defaults when there is none. */
367
+ export declare function appliedAppSettings(recorded: Readonly<Record<string, string>>, entryKey: string): AppSettings;
368
+ /**
369
+ * The names `coolifyApp` last put in the entry's Coolify `domains`, as a JSON
370
+ * array (`[]` for an internal entry). `planApp` plans the step again, with a
371
+ * redeploy, when {@link entryHostnames} differs from it.
372
+ */
373
+ export declare function appDomainsAppliedScopeKey(entryKey: string): string;
374
+ /**
375
+ * The names the app has: the marker, or for an app that predates it the one
376
+ * name it was created with (the `app` entry's address, nothing for any other
377
+ * entry), so an app that runs today plans no change.
378
+ */
379
+ export declare function appliedAppDomains(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, entryKey: string): string[];
339
380
  /**
340
381
  * The entry's `env` values that have a value, in env-name order, and the env
341
382
  * names whose source has none yet. Shared by `planApp` and the executor's
@@ -510,11 +551,23 @@ export interface PlanPlatformOptions {
510
551
  * include list is not the declared one. `auth` (#216 round 4): the
511
552
  * login-client PAT recorded under
512
553
  * {@link PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY} is no longer the sole
513
- * live PAT on that ZITADEL machine user.
554
+ * live PAT on that ZITADEL machine user. `backupStorage` (#257 fix round
555
+ * 2): the registered R2 storage's live fingerprint no longer matches the
556
+ * host's configured bucket/endpoint/key/secret, or the recorded storage
557
+ * uuid is no longer in Coolify's live list -- see `planBackups`'s own doc
558
+ * comment for why this is the one drift flag that can fire even once
559
+ * every per-service backup step is claimed. `backupStorageBlocked` (#257
560
+ * fix round 4, point 4): set instead of `backupStorage` when the liveness
561
+ * check itself failed (Coolify unreachable, credentials revoked) -- the
562
+ * plan is never failed over it, but it is reported in `unmanaged` rather
563
+ * than only logged, so a persistent failure is visible instead of silently
564
+ * retried forever.
514
565
  */
515
566
  drifted?: {
516
567
  archon?: boolean;
517
568
  auth?: boolean;
569
+ backupStorage?: boolean;
570
+ backupStorageBlocked?: string;
518
571
  };
519
572
  }
520
573
  /**
@@ -1,6 +1,7 @@
1
1
  import { VERSIONS } from '../bootstrap/versions.js';
2
2
  import { APP_ENTRY_KEY } from '../org.js';
3
3
  import { VERSION_CHANGE_KIND, } from '../plan.js';
4
+ import { legacyAppHostname } from './hostnames.js';
4
5
  /**
5
6
  * The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
6
7
  * mirroring `diff.ts`'s `toPlan`. There is no separate platform-shaped `Plan`
@@ -348,28 +349,80 @@ export function platformMailMxHost(apex) {
348
349
  export const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = 'mail_applied_apex';
349
350
  export const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = 'mail_applied_host';
350
351
  /**
351
- * The key of an org's `app` hosting entry in a `coolify:app` or
352
- * `cloudflare:access` step and in the ids recorded for it:
353
- * `<orgId>:<entry key>`, so a second entry cannot collide with the first.
352
+ * The key of a hosting entry in a `coolify:app` or `cloudflare:access` step
353
+ * and in the ids recorded for it: `<orgId>:<entry key>`, so a second entry
354
+ * cannot collide with the first.
354
355
  */
355
- export function appStepKey(org) {
356
- return `${org.id}:${APP_ENTRY_KEY}`;
356
+ export function appStepKey(org, entryKey = APP_ENTRY_KEY) {
357
+ return `${org.id}:${entryKey}`;
357
358
  }
358
359
  /**
359
- * #243: the org's `app` entry's `env` values the last `coolify:app` apply
360
- * pushed, recorded as one JSON object (env name to value; ids and a domain,
361
- * never a secret). `planApp` plans the step again when {@link resolveAppEnv}
362
- * differs from it, so a changed map reaches an app that is already live. The
363
- * `.app` suffix is the entry key; a scope key holds no `:`, so
364
- * `storeRecordedIds` still filters it out.
360
+ * The Coolify name of a hosting entry's app: `<slug>` for the `app` entry, so
361
+ * the move to a list finds the app that runs today, `<slug>-<key>` for any
362
+ * other.
365
363
  */
366
- export const APP_ENV_APPLIED_SCOPE_KEY = `app_env_applied.${APP_ENTRY_KEY}`;
364
+ export function appNameFor(org, entry) {
365
+ return entry.key === APP_ENTRY_KEY ? org.slug : `${org.slug}-${entry.key}`;
366
+ }
367
+ /**
368
+ * #243: the entry's `env` values the last `coolify:app` apply pushed,
369
+ * recorded as one JSON object (env name to value; ids and a domain, never a
370
+ * secret). `planApp` plans the step again when {@link resolveAppEnv} differs
371
+ * from it, so a changed map reaches an app that is already live. The `.<key>`
372
+ * suffix is the entry key; a scope key holds no `:`, so `storeRecordedIds`
373
+ * still filters it out.
374
+ */
375
+ export function appEnvAppliedScopeKey(entryKey) {
376
+ return `app_env_applied.${entryKey}`;
377
+ }
367
378
  /**
368
379
  * #239: set to '1' once an app has been restarted after its database URLs
369
380
  * were pushed. Not a URL. Without it a crash between the push and the restart
370
381
  * would leave a stale container that no plan notices.
371
382
  */
372
- export const DB_ENV_APPLIED_SCOPE_KEY = `db_env_applied.${APP_ENTRY_KEY}`;
383
+ export function dbEnvAppliedScopeKey(entryKey) {
384
+ return `db_env_applied.${entryKey}`;
385
+ }
386
+ export function appSettingsOf(entry) {
387
+ return { folder: entry.folder, port: entry.port, autoDeploy: entry.autoDeploy };
388
+ }
389
+ /** What an app that predates the marker has: Coolify's own defaults, so it plans no change. */
390
+ export const DEFAULT_APP_SETTINGS = { folder: null, port: null, autoDeploy: true };
391
+ export function appSettingsAppliedScopeKey(entryKey) {
392
+ return `app_settings_applied.${entryKey}`;
393
+ }
394
+ /**
395
+ * Set before an adopted app's settings are patched, cleared once its deploy
396
+ * succeeded (an empty value is not pending): a failed deploy replans whatever
397
+ * the markers say (#311).
398
+ */
399
+ export function appDeployPendingScopeKey(entryKey) {
400
+ return `app_deploy_pending.${entryKey}`;
401
+ }
402
+ /** The settings the app has: the marker, or the defaults when there is none. */
403
+ export function appliedAppSettings(recorded, entryKey) {
404
+ const marker = recorded[appSettingsAppliedScopeKey(entryKey)];
405
+ return marker ? JSON.parse(marker) : DEFAULT_APP_SETTINGS;
406
+ }
407
+ /**
408
+ * The names `coolifyApp` last put in the entry's Coolify `domains`, as a JSON
409
+ * array (`[]` for an internal entry). `planApp` plans the step again, with a
410
+ * redeploy, when {@link entryHostnames} differs from it.
411
+ */
412
+ export function appDomainsAppliedScopeKey(entryKey) {
413
+ return `app_domains_applied.${entryKey}`;
414
+ }
415
+ /**
416
+ * The names the app has: the marker, or for an app that predates it the one
417
+ * name it was created with (the `app` entry's address, nothing for any other
418
+ * entry), so an app that runs today plans no change.
419
+ */
420
+ export function appliedAppDomains(org, recorded, entryKey) {
421
+ const marker = recorded[appDomainsAppliedScopeKey(entryKey)];
422
+ if (marker)
423
+ return JSON.parse(marker);
424
+ return entryKey === APP_ENTRY_KEY ? [legacyAppHostname(org)] : [];
425
+ }
373
426
  /**
374
427
  * The entry's `env` values that have a value, in env-name order, and the env
375
428
  * names whose source has none yet. Shared by `planApp` and the executor's
@@ -576,7 +629,7 @@ export function planPlatform(platform, live, options = {}) {
576
629
  plannedSteps.push(archon.step);
577
630
  // #257: after every service above, so a database any of them just
578
631
  // recorded is already visible to `recordedIds` on this same plan.
579
- const backups = planBackups(byKey, claimed, recordedIds, blocked.backupExecutorMissing ?? false);
632
+ const backups = planBackups(byKey, claimed, recordedIds, blocked.backupExecutorMissing ?? false, drifted.backupStorage ?? false, drifted.backupStorageBlocked);
580
633
  plannedSteps.push(...backups.steps);
581
634
  // #251: one line per service whose recorded applied ref no longer matches
582
635
  // what the next apply would use (or, for zitadel, what is actually live).
@@ -778,15 +831,36 @@ export const PLATFORM_BACKUP_SERVICE_KEYS = [
778
831
  * Once a step's own `coolify:backup:<service>` resource is live, it is
779
832
  * claimed and never re-planned: like every other provision-only step here,
780
833
  * there is no park branch for a backup already scheduled.
834
+ *
835
+ * #257 fix round 2: that "claimed, never re-planned" rule is exactly why a
836
+ * rotated R2 credential would otherwise never reach Coolify again once every
837
+ * service's own backup is live -- nothing left in this loop ever calls
838
+ * `ensurePlatformBackupStorage` (`platform-providers.ts`) again. `storageDrifted`
839
+ * (the host's own `driftedSteps`, read fresh each plan) is the signal that
840
+ * self-heal is needed; when it is true, this plans exactly one extra
841
+ * `coolify:backup-storage:platform` step, independent of every per-service
842
+ * step's own claimed status, so the self-heal runs again whether or not any
843
+ * service step is also being (re)planned this round. It is never itself
844
+ * "claimed" against a live resource -- there is nothing to read live for a
845
+ * storage push, only to react to -- so it simply stops being planned the
846
+ * moment `storageDrifted` goes false again.
847
+ *
848
+ * #257 fix round 4, point 4: `storageBlocked`, when set, means the host's own
849
+ * `driftedSteps` could not even tell whether the storage is drifted (a
850
+ * Coolify read failed) -- reported in `blocked` instead of planning the
851
+ * self-heal step, so a persistent failure is visible in the plan rather than
852
+ * only logged and retried silently forever.
781
853
  */
782
- function planBackups(byKey, claimed, recordedIds, executorMissing) {
854
+ function planBackups(byKey, claimed, recordedIds, executorMissing, storageDrifted, storageBlocked) {
783
855
  const steps = [];
784
856
  const blocked = [];
857
+ let anyDatabase = false;
785
858
  for (const service of PLATFORM_BACKUP_SERVICE_KEYS) {
786
859
  const key = resourceKey('coolify', 'backup', service);
787
860
  const live = byKey.get(key);
788
861
  if (live) {
789
862
  claimed.add(key);
863
+ anyDatabase = true;
790
864
  continue;
791
865
  }
792
866
  // No database yet is not reported: unlike a Coolify reader or a backup
@@ -798,6 +872,7 @@ function planBackups(byKey, claimed, recordedIds, executorMissing) {
798
872
  const databaseId = recordedIds[PLATFORM_BACKUP_DATABASE_SCOPE_KEYS[service]];
799
873
  if (!databaseId)
800
874
  continue;
875
+ anyDatabase = true;
801
876
  if (executorMissing) {
802
877
  blocked.push({
803
878
  description: `platform:backup:${service} is blocked, not planned: the host built the platform executor without its backup dependencies`,
@@ -819,6 +894,33 @@ function planBackups(byKey, claimed, recordedIds, executorMissing) {
819
894
  },
820
895
  });
821
896
  }
897
+ // #257 fix round 4, point 4: a blocked liveness check is reported instead
898
+ // of planning the self-heal step -- there is nothing to push until the
899
+ // read itself works again.
900
+ if (!executorMissing && storageBlocked && anyDatabase) {
901
+ blocked.push({ description: storageBlocked, key: 'backup-storage' });
902
+ }
903
+ else if (
904
+ // #257 fix round 2: same "nothing to report yet" gate every other branch
905
+ // above follows -- a platform with no database yet has no storage to
906
+ // self-heal either, whatever a stale `storageDrifted` reading says.
907
+ !executorMissing &&
908
+ storageDrifted &&
909
+ anyDatabase) {
910
+ steps.push({
911
+ action: {
912
+ system: 'coolify',
913
+ kind: 'backup-storage',
914
+ key: 'platform',
915
+ desired: {},
916
+ },
917
+ step: {
918
+ description: "self-heal the platform's R2 backup storage registered with Coolify (a rotated credential or a changed bucket/endpoint)",
919
+ destroys: false,
920
+ migrates: false,
921
+ },
922
+ });
923
+ }
822
924
  return { steps, blocked };
823
925
  }
824
926
  /**
@@ -5,6 +5,7 @@ import type { StepAction } from './diff.js';
5
5
  import type { CloudflarePort, CoolifyPort, FoundryStepExecutorPorts, ZitadelPort } from './executor.js';
6
6
  import type { LiveResource, PlatformCloudflareReader, PlatformCoolifyReader } from './live.js';
7
7
  import type { PlatformStepExecutionContext, PlatformStepExecutor } from './plan-apply.js';
8
+ import { type BrokerDeployRecord } from './platform-diff.js';
8
9
  import { type CallsSfuPlatformDeps } from './platform-sfu.js';
9
10
  /**
10
11
  * #168: everything {@link provisionPlatformAuth} needs beyond
@@ -206,6 +207,8 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
206
207
  driftedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
207
208
  archon?: boolean;
208
209
  auth?: boolean;
210
+ backupStorage?: boolean;
211
+ backupStorageBlocked?: string;
209
212
  }>;
210
213
  execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
211
214
  resourceId?: string;
@@ -368,6 +371,40 @@ export declare const PLATFORM_ARCHON_ACCESS_APP_NAME = "foundry:platform:archon"
368
371
  export declare const PLATFORM_ZITADEL_ORG_SCOPE_KEY = "zitadel_platform_org";
369
372
  /** Archon's OIDC client id: public, not a secret, so a plain scope id. */
370
373
  export declare const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = "zitadel_platform_archon_client_id";
374
+ /**
375
+ * One `AUTH_APPS` entry, read back. Only the fields this step itself reads or
376
+ * writes are named; every other field `wtfalch/auth/broker/src/apps.ts`'s
377
+ * `App` knows about (`from`, `productName`, `locale`, `registration`,
378
+ * `emailLink`) rides along unnamed through the index signature, so merging an
379
+ * entry (see `provisionPlatformArchon`) never has to know the broker's full
380
+ * shape to avoid dropping a field it does not itself set.
381
+ */
382
+ type BrokerAppEntry = Record<string, unknown> & {
383
+ organizationId: string;
384
+ key: string;
385
+ name: string;
386
+ clientIds: string[];
387
+ origins: string[];
388
+ };
389
+ /**
390
+ * One `AUTH_APPS[index]` entry, checked the way
391
+ * `wtfalch/auth/broker/src/apps.ts`'s own `parseApps`/`parseAppFields`
392
+ * (https://github.com/wtfalch/auth/blob/main/broker/src/apps.ts) check it at
393
+ * boot, in the same order: a `key` of at least 32 characters and a non-empty
394
+ * `organizationId`; a `name` that passes the broker's own `isSafeText` rule
395
+ * (#218); a `productName`, when present, that is a string and also passes it
396
+ * (#218); a `from` that passes {@link isValidSenderFrom}; a `locale` of
397
+ * `'en'` or `'nb'` when present; non-blank `clientIds`; and `origins` each in
398
+ * {@link isCanonicalOrigin} form. #211 round 2: on a bad
399
+ * entry this throws naming the broker app id and the entry's index, never
400
+ * the value -- `key` may hold another organisation's secret, and even
401
+ * `organizationId`/`name` are not this step's to print in an error only an
402
+ * operator reads. `entry` may be anything at all (`null`, a string, an
403
+ * array): every check below is a `typeof`/`Array.isArray` guard first, so a
404
+ * malformed entry never reaches a property read on a non-object and throws a
405
+ * `TypeError` instead of this named error.
406
+ */
407
+ export declare function validateBrokerAppEntry(entry: unknown, brokerAppId: string, index: number): BrokerAppEntry;
371
408
  /**
372
409
  * #172/#201: mints Archon's own AI management key -- the plain bearer key
373
410
  * (`wtfk_...`) `AiOrganisationPort` (`executor.ts`) authenticates with,
@@ -500,6 +537,51 @@ export declare function createAiManagementKeyMinter(options: {
500
537
  fetchImpl?: typeof fetch;
501
538
  now?: () => number;
502
539
  }): AiManagementKeyMinter;
540
+ /**
541
+ * #211: names of the envs the platform's sign-in broker (`wtfalch/auth`'s
542
+ * `broker/server.ts`) refuses to start without, that `provisionPlatformAuth`
543
+ * itself has no way to supply yet -- `ZITADEL_SERVICE_USER_TOKEN`/`_FILE`
544
+ * (#209 mints and pushes this once it lands) and `RESEND_API_TOKEN` (#210,
545
+ * blocked on `wtfalch/auth#138`). Read from the broker's own live env, not a
546
+ * recorded scope id: whichever of #209/#210 lands first (or a human, setting
547
+ * either directly on the app) is enough to clear that one input, in either
548
+ * order, with no coupling to this step's own execution.
549
+ *
550
+ * #211 round 3: `AUTH_APPS` is a third required input -- the broker's own
551
+ * `parseApps` refuses to start with no `AUTH_APPS` set or an empty array
552
+ * (`wtfalch/auth/broker/src/apps.ts`), and this check used to name only the
553
+ * other two, so a deploy could go ahead with a broker that still would not
554
+ * come up. Also treat `null`/`undefined` as missing, not only `''`: the
555
+ * `CoolifyPort.listEnvs` doc comment already warns a live value does not
556
+ * always match its declared `string` type.
557
+ */
558
+ export declare function missingBrokerInputs(coolify: Pick<CoolifyPort, 'listEnvs'>, brokerAppId: string): Promise<string[]>;
559
+ /**
560
+ * #211 round 5: the one rule `provisionPlatformAuth` and
561
+ * `provisionPlatformArchon` both call to decide whether the broker needs a
562
+ * (re)deploy -- replacing the two ad hoc "was it ever ostensibly deployed"
563
+ * booleans each function used to keep, which is what let a recreated broker,
564
+ * and a crash between writing `AUTH_APPS` and deploying, both go unnoticed
565
+ * (the round 5 issue text's own diagnosis: "they share one cause"). `false`
566
+ * while any required input is still missing -- deploying a broker that
567
+ * cannot start is pointless regardless of what changed. Otherwise `true`
568
+ * when nothing has ever been recorded, when the broker that was deployed is
569
+ * a different app (recreated under a new id), or when the `AUTH_APPS` that
570
+ * was deployed is not the `AUTH_APPS` that is live/about-to-be-live now.
571
+ * `authAppsRaw` is that exact string -- read live by `provisionPlatformAuth`,
572
+ * which never writes `AUTH_APPS` itself, or supplied by
573
+ * `provisionPlatformArchon` as the string it already knows it just wrote (or
574
+ * left unchanged), so neither caller needs a second read to agree with
575
+ * itself about what is now true of the app.
576
+ */
577
+ export declare function brokerDeployNeeded(missingInputs: readonly string[], brokerAppId: string, authAppsRaw: string | undefined, recordedDeploy: BrokerDeployRecord | null): boolean;
578
+ /**
579
+ * #211 round 5: the {@link BrokerDeployRecord} JSON a successful deploy
580
+ * writes under {@link PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY} (via each
581
+ * caller's own `record()`), replacing the plain broker-id string round 2-4
582
+ * wrote there. Paired with {@link brokerDeployNeeded}, which reads it back.
583
+ */
584
+ export declare function brokerDeployRecordValue(brokerAppId: string, authAppsRaw: string | undefined): string;
503
585
  /**
504
586
  * #171: everything {@link provisionPlatformFiles} needs beyond
505
587
  * {@link PlatformStepExecutionContext}, bundled for the same reason as
@@ -607,10 +689,21 @@ export declare const INTEGRATIONS_MANAGEMENT_KEY_ID = "foundry-1";
607
689
  * platform database's dump lands in. A person creates or confirms the
608
690
  * bucket and these credentials by hand (the issue's own "Human steps"); this
609
691
  * package only ever registers the bucket with Coolify and schedules the
610
- * dump, never creates or reads the bucket itself.
692
+ * dump, never creates or reads the bucket itself. `raw` carries the S3
693
+ * storage update call, `PATCH /s3-storages/{uuid}` -- checked against the
694
+ * same live Coolify 4.3.8 instance `@wtfalch/providers`' own CLAUDE.md
695
+ * records as the last version this estate verified its `{uuid}` routes
696
+ * against (that package's own "Known traps": the bundled `openapi.yaml`
697
+ * omits every one of them, so `routes/api.php`-level comments are what to
698
+ * trust). `raw` also carries the schedule-repoint call (#257 fix round 3),
699
+ * `PATCH /databases/{uuid}/backups/{backup_uuid}`, documented in Coolify's
700
+ * public API reference ("Update database backup") as taking an
701
+ * `s3_storage_uuid` field. `CoolifyPort` has not wrapped either route with a
702
+ * typed method, so both go through the same `raw` escape hatch `coolifyApp`'s
703
+ * own stop/start calls use for a REST route the port has not wrapped yet.
611
704
  */
612
705
  export interface PlatformBackupDeps {
613
- coolify: Pick<CoolifyPort, 'listS3Storages' | 'createS3Storage' | 'listDatabaseBackups' | 'createDatabaseBackup'>;
706
+ coolify: Pick<CoolifyPort, 'listS3Storages' | 'createS3Storage' | 'listDatabaseBackups' | 'createDatabaseBackup' | 'raw'>;
614
707
  r2: {
615
708
  endpoint: string;
616
709
  bucket: string;