@wtfalch/foundry 0.61.0 → 0.63.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 (97) hide show
  1. package/dist/bootstrap/declare.js +4 -1
  2. package/dist/bootstrap/versions.d.ts +2 -0
  3. package/dist/index.d.ts +9 -1
  4. package/dist/index.js +3 -0
  5. package/dist/migrations/0065_native_build_runs.sql +32 -0
  6. package/dist/migrations/0066_org_kosmos.sql +10 -0
  7. package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
  8. package/dist/migrations/0068_platform_registry.sql +17 -0
  9. package/dist/modules/sign-in.js +1 -1
  10. package/dist/native-build.d.ts +50 -0
  11. package/dist/native-build.js +72 -0
  12. package/dist/org.d.ts +22 -3
  13. package/dist/org.js +25 -2
  14. package/dist/plan/agora.d.ts +1 -1
  15. package/dist/plan/agora.js +4 -0
  16. package/dist/plan/archon-blue-green.d.ts +5 -0
  17. package/dist/plan/archon-blue-green.js +2 -0
  18. package/dist/plan/blue-green-apply.d.ts +5 -0
  19. package/dist/plan/blue-green-apply.js +197 -22
  20. package/dist/plan/blue-green.d.ts +80 -9
  21. package/dist/plan/blue-green.js +115 -14
  22. package/dist/plan/boule.d.ts +1 -1
  23. package/dist/plan/boule.js +2 -0
  24. package/dist/plan/broker-entries.d.ts +9 -0
  25. package/dist/plan/broker-entries.js +29 -0
  26. package/dist/plan/deploy-health.js +9 -2
  27. package/dist/plan/desktop-callback.js +4 -1
  28. package/dist/plan/diff.d.ts +5 -0
  29. package/dist/plan/diff.js +184 -63
  30. package/dist/plan/edge-access.d.ts +2 -3
  31. package/dist/plan/edge-access.js +8 -0
  32. package/dist/plan/executor.d.ts +111 -19
  33. package/dist/plan/executor.js +578 -72
  34. package/dist/plan/index.d.ts +2 -0
  35. package/dist/plan/index.js +1 -0
  36. package/dist/plan/jobs-scheduler-http.d.ts +2 -0
  37. package/dist/plan/jobs-scheduler-http.js +11 -0
  38. package/dist/plan/kosmos.d.ts +56 -0
  39. package/dist/plan/kosmos.js +84 -0
  40. package/dist/plan/locked-hostnames.js +5 -2
  41. package/dist/plan/org-names.d.ts +3 -3
  42. package/dist/plan/org-names.js +3 -1
  43. package/dist/plan/org-postgres-migrate.js +1 -0
  44. package/dist/plan/own-limits.d.ts +12 -0
  45. package/dist/plan/own-limits.js +2 -0
  46. package/dist/plan/plan-apply.d.ts +51 -1
  47. package/dist/plan/plan-apply.js +298 -25
  48. package/dist/plan/platform-diff.d.ts +6 -0
  49. package/dist/plan/platform-diff.js +74 -0
  50. package/dist/plan/platform-org.d.ts +2 -0
  51. package/dist/plan/platform-providers.d.ts +46 -1
  52. package/dist/plan/platform-providers.js +162 -1
  53. package/dist/plan/platform-registry-keys.d.ts +58 -0
  54. package/dist/plan/platform-registry-keys.js +69 -0
  55. package/dist/plan/platform-registry.d.ts +67 -0
  56. package/dist/plan/platform-registry.js +298 -0
  57. package/dist/plan/post-logout-uris.d.ts +1 -1
  58. package/dist/plan/post-logout-uris.js +5 -2
  59. package/dist/plan/providers-readers.js +20 -1
  60. package/dist/plan/release-host.d.ts +31 -2
  61. package/dist/plan/release-host.js +114 -6
  62. package/dist/plan/stage-copy-host.d.ts +104 -0
  63. package/dist/plan/stage-copy-host.js +259 -0
  64. package/dist/plan/stage-copy.js +44 -0
  65. package/dist/plan/stage-env.d.ts +9 -0
  66. package/dist/plan/stage-env.js +38 -0
  67. package/dist/plan/stage-guards.d.ts +57 -1
  68. package/dist/plan/stage-guards.js +106 -0
  69. package/dist/plan/stage-isolation.js +56 -0
  70. package/dist/plan/stage-services.d.ts +93 -0
  71. package/dist/plan/stage-services.js +153 -0
  72. package/dist/plan/stage-signin.d.ts +152 -0
  73. package/dist/plan/stage-signin.js +133 -0
  74. package/dist/plan/stage-sink.d.ts +2 -0
  75. package/dist/plan/stage-sink.js +6 -1
  76. package/dist/platform.d.ts +7 -1
  77. package/dist/platform.js +1 -0
  78. package/dist/release/decide.d.ts +12 -0
  79. package/dist/release/decide.js +37 -2
  80. package/dist/release/migration-compatibility.d.ts +66 -0
  81. package/dist/release/migration-compatibility.js +340 -0
  82. package/dist/release/run.d.ts +71 -1
  83. package/dist/release/run.js +92 -8
  84. package/dist/release/state.d.ts +14 -1
  85. package/dist/release/state.js +43 -0
  86. package/dist/release/types.d.ts +20 -0
  87. package/dist/release/types.js +2 -0
  88. package/dist/requested-changes.d.ts +11 -2
  89. package/dist/requested-changes.js +13 -1
  90. package/dist/slug.d.ts +1 -1
  91. package/dist/slug.js +11 -2
  92. package/dist/store/postgres.js +21 -0
  93. package/dist/store/schema.d.ts +87 -0
  94. package/dist/store/schema.js +10 -0
  95. package/package.json +3 -2
  96. package/versions.digests.json +1 -0
  97. package/versions.json +1 -0
@@ -10,6 +10,8 @@ import { remoteDatabaseAddress, remoteDatabaseHost } from './remote-database.js'
10
10
  import { STAGE_SIGN_IN_NAMES, buildStageEnv } from './stage-env.js';
11
11
  import { buildStageProbes, runStageProbes, stageProbesRecord, } from './stage-guards.js';
12
12
  import { assertStageIsolated, envClass, stageIsolationHash, } from './stage-isolation.js';
13
+ import { StageServiceError, ensureStageServices } from './stage-services.js';
14
+ import { STAGE_SIGNIN_NONE, StageSignInError, ensureStagePublicClient, ensureStageSignIn, } from './stage-signin.js';
13
15
  import { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_APP_SCOPE_KEY, STAGE_SINK_HTTP_PORT, STAGE_SINK_IMAGE_TAG, STAGE_SINK_REPOSITORY, stageMailEnv, stageSinkAppName, } from './stage-sink.js';
14
16
  import { readWorkspaceCommit, wsKey } from './workspace.js';
15
17
  /** How long one Coolify call of the sink may take. */
@@ -95,7 +97,10 @@ export async function applyBlueGreenServer(deps, subject, server) {
95
97
  await deps.persist();
96
98
  }
97
99
  }
98
- deps.record(bgKey.applied(id, server.uuid), structureHash(subject, { primary: isPrimary }));
100
+ deps.record(bgKey.applied(id, server.uuid), structureHash(subject, {
101
+ primary: isPrimary,
102
+ ...(deps.guardInputs ? { guards: deps.guardInputs } : {}),
103
+ }));
99
104
  await deps.persist();
100
105
  }
101
106
  /**
@@ -481,30 +486,92 @@ export async function ensureStage(deps, subject, serverUuid) {
481
486
  const stageHas = new Map((await coolify.listEnvs(appId)).filter((e) => !e.is_preview).map((e) => [e.key, e.value]));
482
487
  const mailChanged = [...mail, ...signInEnv].some((e) => stageHas.get(e.key) !== e.value);
483
488
  stageEnv.push(...mail);
484
- const stageSurface = {
489
+ const stageSurfaceOf = () => ({
485
490
  env: stageEnv.map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
486
491
  resources: stageResources(subject),
492
+ });
493
+ const prodSurface = {
494
+ env: [...prodEnv, ...greenEnv]
495
+ .filter((e) => !e.is_preview && e.value !== undefined)
496
+ .map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
497
+ resources: prodResources(subject),
498
+ internalHosts: Object.values(prodResources(subject)),
487
499
  };
488
- const violations = assertStageIsolated({
500
+ const sinkMail = Object.fromEntries(mail.map((e) => [e.key, e.value]));
501
+ const signInIsolation = deps.signIn
502
+ ? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
503
+ : {};
504
+ const isolationViolations = (surface) => assertStageIsolated({
489
505
  kind,
490
- prod: {
491
- env: [...prodEnv, ...greenEnv]
492
- .filter((e) => !e.is_preview && e.value !== undefined)
493
- .map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
494
- resources: prodResources(subject),
495
- internalHosts: Object.values(prodResources(subject)),
496
- },
497
- stage: stageSurface,
498
- sinkMail: Object.fromEntries(mail.map((e) => [e.key, e.value])),
499
- ...(deps.signIn
500
- ? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
501
- : {}),
506
+ prod: prodSurface,
507
+ stage: surface,
508
+ sinkMail,
509
+ ...signInIsolation,
502
510
  });
511
+ // The env written this run, and the env stage already holds (put there by an earlier apply, an older
512
+ // builder or a hand): a forbidden name or a production value already on the live stage app is refused too.
513
+ // A name this run writes again is replaced, so only the other live names are checked here.
514
+ const written = new Set(stageEnv.map((e) => e.key));
515
+ const liveStage = {
516
+ env: [...stageHas]
517
+ .filter(([name, value]) => value !== undefined && !written.has(name))
518
+ .map(([name, value]) => ({ name, class: envClass(name), value })),
519
+ resources: {},
520
+ };
521
+ let stageSurface = stageSurfaceOf();
522
+ const violations = [
523
+ ...new Set([
524
+ ...isolationViolations(stageSurface),
525
+ ...assertStageIsolated({
526
+ kind,
527
+ prod: prodSurface,
528
+ stage: liveStage,
529
+ sinkMail,
530
+ ...signInIsolation,
531
+ }),
532
+ ]),
533
+ ].sort();
503
534
  if (violations.length > 0) {
504
535
  // Stage fails closed for itself: nothing is written to it, production's structure goes on.
505
536
  await stageFailed(deps, subject, `blocked:${violations.join(',')}`);
506
537
  return;
507
538
  }
539
+ // Stage's own files, ai and jobs organisations and keys, for the services production's app uses. They are made
540
+ // before the probes, which need the keys; nothing is written to the stage app until the probes were rejected.
541
+ const prodValues = new Map(prodEnv
542
+ .filter((e) => !e.is_preview && e.value !== undefined)
543
+ .map((e) => [e.key, e.value]));
544
+ let servicesRecord;
545
+ try {
546
+ const made = await ensureStageServices({
547
+ id: subject.id,
548
+ kind,
549
+ prod: prodValues,
550
+ port: deps.stageGuards?.services,
551
+ recorded: deps.recorded,
552
+ record: deps.record,
553
+ persist: deps.persist,
554
+ secret: deps.secret,
555
+ log: (line) => console.info(`${deps.label}: ${line}`),
556
+ });
557
+ stageEnv.push(...made.env);
558
+ servicesRecord = made.record;
559
+ }
560
+ catch (error) {
561
+ // A StageServiceError holds ids and refs only; any other cause can echo a credential.
562
+ if (error instanceof StageServiceError)
563
+ console.error(`${deps.label}: ${error.message}`);
564
+ await stageFailed(deps, subject, 'blocked:services', error);
565
+ return;
566
+ }
567
+ if (servicesRecord !== 'none') {
568
+ stageSurface = stageSurfaceOf();
569
+ const withServices = isolationViolations(stageSurface);
570
+ if (withServices.length > 0) {
571
+ await stageFailed(deps, subject, `blocked:${withServices.join(',')}`);
572
+ return;
573
+ }
574
+ }
508
575
  // Stage's credentials are tried against production before they are written; one that production accepts
509
576
  // blocks stage (nothing is written to it), and what was tried is recorded by name so the copy slice sees it.
510
577
  const stageValues = new Map((await coolify.listEnvs(appId))
@@ -512,7 +579,7 @@ export async function ensureStage(deps, subject, serverUuid) {
512
579
  .map((e) => [e.key, e.value]));
513
580
  for (const e of stageEnv)
514
581
  stageValues.set(e.key, e.value);
515
- const isolationHash = stageIsolationHash(stageSurface);
582
+ let isolationHash = stageIsolationHash(stageSurface);
516
583
  let probeResult;
517
584
  try {
518
585
  probeResult = await runStageProbes(buildStageProbes({
@@ -524,9 +591,12 @@ export async function ensureStage(deps, subject, serverUuid) {
524
591
  ? { liveCustomersOrgId: deps.signIn.live.customersOrgId }
525
592
  : {}),
526
593
  ...(deps.signIn ? { signInRedirectUri: deps.signIn.redirectUri } : {}),
527
- prod: new Map(prodEnv
528
- .filter((e) => !e.is_preview && e.value !== undefined)
529
- .map((e) => [e.key, e.value])),
594
+ prod: new Map([
595
+ ...prodValues,
596
+ ...(deps.stageGuards?.services?.prodJobsProjectId
597
+ ? [['JOBS_PROJECT_ID', deps.stageGuards.services.prodJobsProjectId]]
598
+ : []),
599
+ ]),
530
600
  }));
531
601
  }
532
602
  catch (error) {
@@ -534,7 +604,7 @@ export async function ensureStage(deps, subject, serverUuid) {
534
604
  await stageFailed(deps, subject, 'blocked:database', error);
535
605
  return;
536
606
  }
537
- const probes = stageProbesRecord(probeResult, isolationHash, stageValues);
607
+ let probes = stageProbesRecord(probeResult, isolationHash, stageValues);
538
608
  if (probeResult.failed.length > 0) {
539
609
  deps.record(bgKey.stageProbes(subject.id), probes.value);
540
610
  await stageFailed(deps, subject, `blocked:${probeResult.failed.join(',')}`);
@@ -544,9 +614,108 @@ export async function ensureStage(deps, subject, serverUuid) {
544
614
  await stageFailed(deps, subject, 'blocked:gate');
545
615
  return;
546
616
  }
617
+ // Stage's own sign-in: made only now, after the isolation check, the probes and the gate. Its credentials are
618
+ // added to the env and the isolation check runs once more over the whole of it.
619
+ const signIn = deps.stageGuards?.signIn;
620
+ let signInGuard;
621
+ if (signIn?.required === true && signIn.public === true) {
622
+ // A static SPA (Agora): a client and nothing secret. Its config is built from production's with the stage
623
+ // client id, then the isolation check runs once more over the whole env.
624
+ try {
625
+ const made = await ensureStagePublicClient({
626
+ id: subject.id,
627
+ port: signIn,
628
+ recorded: deps.recorded,
629
+ record: deps.record,
630
+ persist: deps.persist,
631
+ });
632
+ const rebuilt = buildStageEnv({
633
+ kind,
634
+ appUrl: stageAppUrl(subject),
635
+ logDir: deps.logDir,
636
+ database,
637
+ prod: prodEnv,
638
+ existing: [],
639
+ mint: () => '',
640
+ stageClientId: made.clientId,
641
+ });
642
+ const config = rebuilt.env.find((e) => e.key === 'AGORA_CONFIG');
643
+ if (config)
644
+ stageEnv.push(config);
645
+ signInGuard = made.guard;
646
+ }
647
+ catch (error) {
648
+ if (error instanceof StageSignInError)
649
+ console.error(`${deps.label}: ${error.message}`);
650
+ deps.record(bgKey.stageSignIn(subject.id), '');
651
+ await stageFailed(deps, subject, 'blocked:signin', error);
652
+ return;
653
+ }
654
+ for (const e of stageEnv)
655
+ stageValues.set(e.key, e.value);
656
+ stageSurface = stageSurfaceOf();
657
+ const again = isolationViolations(stageSurface);
658
+ if (again.length > 0) {
659
+ deps.record(bgKey.stageSignIn(subject.id), '');
660
+ await stageFailed(deps, subject, `blocked:${again.join(',')}`);
661
+ return;
662
+ }
663
+ isolationHash = stageIsolationHash(stageSurface);
664
+ probes = stageProbesRecord(probeResult, isolationHash, stageValues);
665
+ }
666
+ else if (signIn?.required === true) {
667
+ try {
668
+ const made = await ensureStageSignIn({
669
+ id: subject.id,
670
+ port: signIn,
671
+ recorded: deps.recorded,
672
+ record: deps.record,
673
+ persist: deps.persist,
674
+ secret: deps.secret,
675
+ generate: () => randomBytes(32).toString('base64url'),
676
+ sleep: deps.sleep,
677
+ });
678
+ stageEnv.push(...made.env);
679
+ signInGuard = made.guard;
680
+ }
681
+ catch (error) {
682
+ // A StageSignInError holds ids and refs only; any other cause can echo a credential.
683
+ if (error instanceof StageSignInError)
684
+ console.error(`${deps.label}: ${error.message}`);
685
+ deps.record(bgKey.stageSignIn(subject.id), '');
686
+ await stageFailed(deps, subject, 'blocked:signin', error);
687
+ return;
688
+ }
689
+ if (!stageEnv.some((e) => e.key === 'AUTH_COOKIE_SECRET')) {
690
+ // A cookie secret of stage's own, kept across applies; never a copy of production's.
691
+ const have = stageHas.get('AUTH_COOKIE_SECRET') || randomBytes(32).toString('base64url');
692
+ deps.secret(have);
693
+ stageEnv.push({ key: 'AUTH_COOKIE_SECRET', value: have });
694
+ }
695
+ for (const e of stageEnv)
696
+ stageValues.set(e.key, e.value);
697
+ stageSurface = stageSurfaceOf();
698
+ const again = isolationViolations(stageSurface);
699
+ if (again.length > 0) {
700
+ deps.record(bgKey.stageSignIn(subject.id), '');
701
+ await stageFailed(deps, subject, `blocked:${again.join(',')}`);
702
+ return;
703
+ }
704
+ isolationHash = stageIsolationHash(stageSurface);
705
+ probes = stageProbesRecord(probeResult, isolationHash, stageValues);
706
+ }
707
+ else if (signIn?.required === false) {
708
+ signInGuard = STAGE_SIGNIN_NONE;
709
+ }
710
+ else {
711
+ console.info(`${deps.label}: stage sign-in was not set up: the host gave no sign-in for this subject, so ${bgKey.stageSignIn(subject.id)} stays unrecorded`);
712
+ }
547
713
  await upsertEnv(deps, appId, stageEnv);
714
+ if (signInGuard)
715
+ deps.record(bgKey.stageSignIn(subject.id), signInGuard);
548
716
  if (mail.length > 0)
549
717
  deps.record(bgKey.stageSink(subject.id), 'applied');
718
+ deps.record(bgKey.stageServices(subject.id), servicesRecord);
550
719
  deps.record(bgKey.stageIsolation(subject.id), isolationHash);
551
720
  deps.record(bgKey.stageProbes(subject.id), probes.value);
552
721
  await deps.persist();
@@ -589,8 +758,11 @@ async function ensureStageGate(deps, subject) {
589
758
  if (!gate || deps.recorded()[bgKey.stageGate(subject.id)])
590
759
  return true;
591
760
  let id;
761
+ let membersHash;
592
762
  try {
593
- id = (await gate.create()).id;
763
+ const created = await gate.create();
764
+ id = created.id;
765
+ membersHash = created.membersHash;
594
766
  }
595
767
  catch (error) {
596
768
  // An Access error can echo the request, which names the people allowed in: only its name is logged.
@@ -598,6 +770,8 @@ async function ensureStageGate(deps, subject) {
598
770
  return false;
599
771
  }
600
772
  deps.record(bgKey.stageGate(subject.id), id);
773
+ if (membersHash)
774
+ deps.record(bgKey.stageGateMembers(subject.id), membersHash);
601
775
  await deps.persist();
602
776
  return true;
603
777
  }
@@ -607,7 +781,8 @@ async function stageAppId(deps, subject, name, serverUuid) {
607
781
  name,
608
782
  server: serverUuid,
609
783
  branch: subject.branch,
610
- autoDeploy: true,
784
+ // Stage deploys only when a person promotes a commit to it (`deploy_stage`); a merge never reaches it.
785
+ autoDeploy: false,
611
786
  });
612
787
  }
613
788
  function isNotFound(error) {
@@ -1,4 +1,4 @@
1
- import type { BlueGreenConfig, DeclaredOrg, HostingEntry } from '../org.js';
1
+ import { type BlueGreenConfig, type DeclaredOrg, type HostingEntry } from '../org.js';
2
2
  import { type StageKind } from './stage-isolation.js';
3
3
  /**
4
4
  * Blue/green releases (docs/adr/0001-blue-green-releases.md). A color is a property any app can have
@@ -29,6 +29,8 @@ export declare const bgKey: {
29
29
  digest: (id: string, color: Color) => string;
30
30
  weights: (id: string) => string;
31
31
  stage: (id: string) => string;
32
+ /** The last `deploy_stage`: `<ISO time>|ok|<commit>` or `<ISO time>|failed|<step>`. */
33
+ stageDeploy: (id: string) => string;
32
34
  window: (id: string) => string;
33
35
  held: (id: string, digest: string) => string;
34
36
  /** The weights the load balancer on `server` was last written with. */
@@ -55,8 +57,29 @@ export declare const bgKey: {
55
57
  stageIsolation: (id: string) => string;
56
58
  /** The id of the Access gate over the stage host: staff only, no bypass, no service-token rule. */
57
59
  stageGate: (id: string) => string;
60
+ /** The hash of the sorted email list last written to the stage gate (owners plus the people holding `releases:preview`). */
61
+ stageGateMembers: (id: string) => string;
58
62
  /** `ok:<hash>` once every apply-time probe was rejected; `notrun:<names>` or `failed:<names>` otherwise. */
59
63
  stageProbes: (id: string) => string;
64
+ /** Stage's own sign-in client: its ZITADEL app id, and its client id (public, not a secret). */
65
+ stageClient: (id: string) => string;
66
+ stageClientId: (id: string) => string;
67
+ /** The held ref of stage's app key, recorded and persisted as soon as it is minted (no `:`: undelivered). */
68
+ stageSignInKey: (id: string) => string;
69
+ /** `{name, organizationId}` of stage's broker entry, so org destroy can remove it. No key. */
70
+ stageBroker: (id: string) => string;
71
+ /** `ok:<hash of the client, app and broker entry ids>` or `none`; the copy waits for it. */
72
+ stageSignIn: (id: string) => string;
73
+ /** `none` (production's app uses no files, ai or jobs) or `ok:<hash>` once every service it uses is held by stage or listed absent. */
74
+ stageServices: (id: string) => string;
75
+ /** `ok:<hash of the ids>` or `absent:<service>:<reason>` for one service (`files`, `ai` or `jobs`). */
76
+ stageService: (id: string, service: string) => string;
77
+ /** The id of stage's own organisation (files, ai) or project (jobs). An id, not a secret. */
78
+ stageServiceOrg: (id: string, service: string) => string;
79
+ /** The name stage's project or organisation was made under, when the service is deleted by name (jobs). */
80
+ stageServiceName: (id: string, service: string) => string;
81
+ /** `{ids, refs}` of stage's keys in that service: key ids and held refs, never a value. */
82
+ stageServiceKeys: (id: string, service: string) => string;
60
83
  /** `applied` once the stage app holds the sink's mail env. The copy refuses without it. */
61
84
  stageSink: (id: string) => string;
62
85
  /** `<ISO time>|<dump execution id>|ok`, written by a finished copy of production into stage. */
@@ -164,11 +187,14 @@ export interface BlueGreenSubject {
164
187
  * (address check, crash loop) and moves up by time.
165
188
  */
166
189
  reporting: boolean;
167
- /** Set for Boule and Agora, which are not hosting entries. */
190
+ /** Set for Boule, Agora and kosmos, which are not hosting entries. */
168
191
  builtin?: BuiltinApp;
169
192
  }
170
- /** The apps foundry runs for every company, which can declare blue/green (`org.boule`, `org.agora`). */
171
- export type BuiltinApp = 'boule' | 'agora';
193
+ /**
194
+ * The apps foundry runs for a company that can declare blue/green: Boule and Agora, every company's own, plus
195
+ * kosmos once the company turns it on (`org.boule`, `org.agora`, `org.kosmos`; docs/adr/0001, rule 1).
196
+ */
197
+ export type BuiltinApp = 'boule' | 'agora' | 'kosmos';
172
198
  export declare const BUILTIN_APPS: readonly BuiltinApp[];
173
199
  /** What foundry builds each built-in app from; `executor.ts` creates the app from the same values. */
174
200
  export declare const BUILTIN_APP: {
@@ -188,6 +214,14 @@ export declare const BUILTIN_APP: {
188
214
  readonly database: false;
189
215
  readonly reporting: false;
190
216
  };
217
+ readonly kosmos: {
218
+ readonly repo: "wtfalch/kosmos";
219
+ readonly dockerfile: "/Dockerfile";
220
+ readonly port: 3000;
221
+ readonly healthPath: "/api/health";
222
+ readonly database: true;
223
+ readonly reporting: true;
224
+ };
191
225
  };
192
226
  /**
193
227
  * A hosting entry that declares `blueGreen`, as a subject; `null` when it does not or cannot run.
@@ -195,11 +229,12 @@ export declare const BUILTIN_APP: {
195
229
  */
196
230
  export declare function entrySubject(org: DeclaredOrg, entry: HostingEntry, recorded?: Readonly<Record<string, string>>): BlueGreenSubject | null;
197
231
  /**
198
- * The company's Boule or Agora as a subject; `null` when it does not declare blue/green. The running app keeps
199
- * its name `<slug>-<app>` and is blue, like platform-archon. Both run on the primary server only.
232
+ * The company's Boule, Agora or kosmos as a subject; `null` when it does not declare blue/green (see
233
+ * {@link builtinDeclaresBlueGreen}). The running app keeps its name `<slug>-<app>` and is blue, like
234
+ * platform-archon. All three run on the primary server only.
200
235
  */
201
236
  export declare function builtinSubject(org: DeclaredOrg, app: BuiltinApp, recorded?: Readonly<Record<string, string>>): BlueGreenSubject | null;
202
- /** Every blue/green subject of an org: the hosting entries that declare it, then Boule and Agora. */
237
+ /** Every blue/green subject of an org: the hosting entries that declare it, then Boule, Agora and kosmos. */
203
238
  export declare function blueGreenSubjects(org: DeclaredOrg, recorded?: Readonly<Record<string, string>>): BlueGreenSubject[];
204
239
  /** Whether the org declares blue/green for any of its apps. */
205
240
  export declare const declaresBlueGreen: (org: DeclaredOrg) => boolean;
@@ -281,9 +316,38 @@ export interface BlueGreenPlanStep {
281
316
  /** A short hash of what decides the shape of a server's apps, so a change replans the server. */
282
317
  export declare function structureHash(subject: BlueGreenSubject, input: {
283
318
  primary: boolean;
319
+ guards?: readonly string[];
284
320
  }): string;
285
- /** The kind of stage app a subject has: platform-archon, else a customer app. */
286
- export declare const stageKindOf: (subject: Pick<BlueGreenSubject, "id">) => StageKind;
321
+ /**
322
+ * The kind of stage app a subject has: Boule and Agora by `builtin` (a hosting entry that happens to be keyed
323
+ * `boule` is still a customer app), platform-archon by its id, else a customer app (kosmos included: it has no lists of its own).
324
+ */
325
+ export declare const stageKindOf: (subject: Pick<BlueGreenSubject, "id" | "builtin">) => StageKind;
326
+ /** What decides which stage guards can be made. Each is a fact the planner and the executor both hold. */
327
+ export interface StageGuardFacts {
328
+ /** The host supplied the probe port. */
329
+ probesPort: boolean;
330
+ /** The host has an Access port. */
331
+ accessPort: boolean;
332
+ /** The platform's recorded Access provider. */
333
+ providerId?: string | null | undefined;
334
+ /** The domain the stage host is under. */
335
+ domain?: string | null | undefined;
336
+ /** The company's sign-in organisation, the claim the gate requires. */
337
+ claimValue?: string | null | undefined;
338
+ /** How many owner emails the gate admits. */
339
+ emails: number;
340
+ /** A `hosting.access` rule already names the stage host: no gate is made over it. */
341
+ hostRuled: boolean;
342
+ }
343
+ /**
344
+ * The stage guards that can be made now, by name. The names go into the applied structure hash, so a guard
345
+ * that becomes possible after the first apply changes the hash and plans the structure step once more.
346
+ */
347
+ export declare function stageGuardInputs(facts: StageGuardFacts): string[];
348
+ /** The guard names as the plan's `desired.guards` carries them, and back. */
349
+ export declare const formatGuards: (guards: readonly string[]) => string;
350
+ export declare const parseGuards: (value: unknown) => string[];
287
351
  /** The names stage can collide on. */
288
352
  export declare function stageResources(subject: BlueGreenSubject): Record<string, string>;
289
353
  /** The names of the subject's live parts: stage must equal none of them. */
@@ -310,6 +374,11 @@ export interface PlanBlueGreenInput {
310
374
  accessProviderId?: string | null;
311
375
  /** The company's stage mail sink is recorded and its live app is stopped or gone (read from Coolify at plan time). */
312
376
  stageSinkDown?: boolean;
377
+ /** Which stage-guard ports the host has. Absent: neither, so no guard input is counted. */
378
+ stageGuardPorts?: {
379
+ probes: boolean;
380
+ access: boolean;
381
+ };
313
382
  }
314
383
  /**
315
384
  * Plans the blue/green parts of an org: per usable server, one structure step (the apps, the balancer, the
@@ -359,6 +428,8 @@ export declare function planArchonBlueGreen(input: {
359
428
  servers: readonly BlueGreenServer[] | undefined;
360
429
  /** Why the executor cannot run it (no way to hold the health token), or absent. */
361
430
  executorBlocked?: string | undefined;
431
+ /** The stage guards that can be made now ({@link stageGuardInputs}); they go into the applied hash. */
432
+ guards?: readonly string[];
362
433
  }): {
363
434
  step: BlueGreenPlanStep | null;
364
435
  unmanaged: UnmanagedLine[];