@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.
- package/dist/apply.d.ts +16 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/migrations/0030_hosting_entry_hostnames.sql +24 -0
- package/dist/migrations/0031_adoptions.sql +37 -0
- package/dist/org.d.ts +49 -4
- package/dist/org.js +134 -1
- package/dist/plan/adopt.d.ts +112 -0
- package/dist/plan/adopt.js +252 -0
- package/dist/plan/adoption-vault.d.ts +9 -0
- package/dist/plan/adoption-vault.js +1 -0
- package/dist/plan/agora.d.ts +7 -0
- package/dist/plan/agora.js +9 -0
- package/dist/plan/broker-entries.d.ts +108 -0
- package/dist/plan/broker-entries.js +214 -0
- package/dist/plan/cloudflared-connector.d.ts +10 -2
- package/dist/plan/cloudflared-connector.js +24 -16
- package/dist/plan/diff.d.ts +14 -0
- package/dist/plan/diff.js +488 -47
- package/dist/plan/executor.d.ts +141 -3
- package/dist/plan/executor.js +1100 -149
- package/dist/plan/hostnames.d.ts +24 -0
- package/dist/plan/hostnames.js +56 -0
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/locked-hostnames.d.ts +8 -0
- package/dist/plan/locked-hostnames.js +26 -0
- package/dist/plan/login-host.d.ts +1 -1
- package/dist/plan/login-host.js +5 -2
- package/dist/plan/plan-apply.d.ts +63 -3
- package/dist/plan/plan-apply.js +163 -5
- package/dist/plan/platform-diff.d.ts +66 -13
- package/dist/plan/platform-diff.js +117 -15
- package/dist/plan/platform-providers.d.ts +95 -2
- package/dist/plan/platform-providers.js +393 -27
- package/dist/plan/providers-readers.d.ts +8 -1
- package/dist/plan/providers-readers.js +58 -4
- package/dist/plan.d.ts +6 -0
- package/dist/store/postgres.d.ts +6 -1
- package/dist/store/postgres.js +80 -5
- package/dist/store/schema.d.ts +162 -0
- package/dist/store/schema.js +19 -1
- package/dist/store.d.ts +37 -0
- 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
|
|
320
|
-
*
|
|
321
|
-
*
|
|
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
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
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
|
|
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
|
|
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
|
|
352
|
-
*
|
|
353
|
-
*
|
|
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}:${
|
|
356
|
+
export function appStepKey(org, entryKey = APP_ENTRY_KEY) {
|
|
357
|
+
return `${org.id}:${entryKey}`;
|
|
357
358
|
}
|
|
358
359
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
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
|
|
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
|
|
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;
|