@wtfalch/foundry 0.60.0 → 0.62.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 (236) hide show
  1. package/dist/apps/cloudflare.d.ts +173 -0
  2. package/dist/apps/cloudflare.js +17 -0
  3. package/dist/apps/deploy.d.ts +116 -0
  4. package/dist/apps/deploy.js +791 -0
  5. package/dist/apps/github.d.ts +47 -0
  6. package/dist/apps/github.js +201 -0
  7. package/dist/apps/index.d.ts +9 -0
  8. package/dist/apps/index.js +9 -0
  9. package/dist/apps/migrations.d.ts +110 -0
  10. package/dist/apps/migrations.js +458 -0
  11. package/dist/audit/connector.d.ts +43 -0
  12. package/dist/audit/connector.js +22 -0
  13. package/dist/audit/evidence.d.ts +49 -0
  14. package/dist/audit/evidence.js +56 -0
  15. package/dist/audit/index.d.ts +9 -0
  16. package/dist/audit/index.js +9 -0
  17. package/dist/audit/migrations.d.ts +5 -0
  18. package/dist/audit/migrations.js +5 -0
  19. package/dist/audit/registry.d.ts +49 -0
  20. package/dist/audit/registry.js +44 -0
  21. package/dist/audit/seal.d.ts +49 -0
  22. package/dist/audit/seal.js +131 -0
  23. package/dist/audit/sealer-role.d.ts +33 -0
  24. package/dist/audit/sealer-role.js +56 -0
  25. package/dist/audit/signer.d.ts +40 -0
  26. package/dist/audit/signer.js +78 -0
  27. package/dist/audit/tick.d.ts +36 -0
  28. package/dist/audit/tick.js +87 -0
  29. package/dist/catalogue/copy-en.d.ts +1 -0
  30. package/dist/catalogue/copy-en.js +129 -0
  31. package/dist/catalogue/index.d.ts +9 -0
  32. package/dist/catalogue/index.js +21 -0
  33. package/dist/catalogue/integrations.d.ts +4 -0
  34. package/dist/catalogue/integrations.js +104 -0
  35. package/dist/catalogue/systems.d.ts +7 -0
  36. package/dist/catalogue/systems.js +31 -0
  37. package/dist/catalogue/types.d.ts +110 -0
  38. package/dist/catalogue/types.js +1 -0
  39. package/dist/entry-rules.d.ts +53 -0
  40. package/dist/entry-rules.js +71 -0
  41. package/dist/hosted-label.d.ts +13 -0
  42. package/dist/hosted-label.js +81 -0
  43. package/dist/index.d.ts +14 -5
  44. package/dist/index.js +7 -3
  45. package/dist/migrations/0062_hosting_github.sql +6 -0
  46. package/dist/migrations/0063_boule_apps.sql +7 -0
  47. package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
  48. package/dist/migrations/0065_native_build_runs.sql +32 -0
  49. package/dist/migrations/0066_org_kosmos.sql +10 -0
  50. package/dist/modules/ai.d.ts +3 -0
  51. package/dist/modules/ai.js +64 -0
  52. package/dist/modules/assistants.d.ts +3 -0
  53. package/dist/modules/assistants.js +72 -0
  54. package/dist/modules/audit-ledger.d.ts +29 -0
  55. package/dist/modules/audit-ledger.js +49 -0
  56. package/dist/modules/audit.d.ts +7 -0
  57. package/dist/modules/audit.js +44 -0
  58. package/dist/modules/database.d.ts +23 -0
  59. package/dist/modules/database.js +189 -0
  60. package/dist/modules/email.d.ts +3 -0
  61. package/dist/modules/email.js +18 -0
  62. package/dist/modules/entry-parts.d.ts +23 -0
  63. package/dist/modules/entry-parts.js +63 -0
  64. package/dist/modules/files.d.ts +3 -0
  65. package/dist/modules/files.js +64 -0
  66. package/dist/modules/index.d.ts +74 -0
  67. package/dist/modules/index.js +774 -0
  68. package/dist/modules/jobs.d.ts +3 -0
  69. package/dist/modules/jobs.js +23 -0
  70. package/dist/modules/organisations.d.ts +3 -0
  71. package/dist/modules/organisations.js +20 -0
  72. package/dist/modules/outputs.d.ts +9 -0
  73. package/dist/modules/outputs.js +38 -0
  74. package/dist/modules/sign-in.d.ts +3 -0
  75. package/dist/modules/sign-in.js +197 -0
  76. package/dist/modules/signed-records.d.ts +3 -0
  77. package/dist/modules/signed-records.js +32 -0
  78. package/dist/modules/types.d.ts +194 -0
  79. package/dist/modules/types.js +1 -0
  80. package/dist/modules/workspaces.d.ts +3 -0
  81. package/dist/modules/workspaces.js +75 -0
  82. package/dist/native-build.d.ts +70 -0
  83. package/dist/native-build.js +92 -0
  84. package/dist/org.d.ts +184 -24
  85. package/dist/org.js +444 -49
  86. package/dist/plan/agora.d.ts +1 -1
  87. package/dist/plan/agora.js +4 -0
  88. package/dist/plan/apps-db-route.d.ts +90 -0
  89. package/dist/plan/apps-db-route.js +168 -0
  90. package/dist/plan/apps-zones.d.ts +56 -0
  91. package/dist/plan/apps-zones.js +118 -0
  92. package/dist/plan/archon-blue-green.js +11 -1
  93. package/dist/plan/blue-green-apply.d.ts +37 -1
  94. package/dist/plan/blue-green-apply.js +55 -15
  95. package/dist/plan/blue-green.d.ts +60 -8
  96. package/dist/plan/blue-green.js +115 -10
  97. package/dist/plan/boule.d.ts +23 -1
  98. package/dist/plan/boule.js +35 -2
  99. package/dist/plan/broker-entries.d.ts +28 -3
  100. package/dist/plan/broker-entries.js +74 -3
  101. package/dist/plan/broker-registrar.d.ts +168 -0
  102. package/dist/plan/broker-registrar.js +440 -0
  103. package/dist/plan/broker-workspaces-database.d.ts +48 -0
  104. package/dist/plan/broker-workspaces-database.js +87 -0
  105. package/dist/plan/deploy-health.js +9 -2
  106. package/dist/plan/desktop-callback.js +15 -7
  107. package/dist/plan/diff.d.ts +8 -2
  108. package/dist/plan/diff.js +552 -104
  109. package/dist/plan/edge-access.d.ts +6 -0
  110. package/dist/plan/edge-access.js +1 -0
  111. package/dist/plan/edge-dns.d.ts +6 -0
  112. package/dist/plan/edge-dns.js +9 -7
  113. package/dist/plan/entry-ai-key.d.ts +1 -1
  114. package/dist/plan/entry-ai-key.js +2 -2
  115. package/dist/plan/entry-audit.d.ts +58 -0
  116. package/dist/plan/entry-audit.js +122 -0
  117. package/dist/plan/entry-customer-grants.d.ts +75 -0
  118. package/dist/plan/entry-customer-grants.js +154 -0
  119. package/dist/plan/entry-edge-key.d.ts +26 -1
  120. package/dist/plan/entry-edge-key.js +42 -3
  121. package/dist/plan/entry-jobs.d.ts +64 -0
  122. package/dist/plan/entry-jobs.js +92 -0
  123. package/dist/plan/entry-repo-key-apply.d.ts +55 -0
  124. package/dist/plan/entry-repo-key-apply.js +185 -0
  125. package/dist/plan/entry-repo-key.d.ts +92 -0
  126. package/dist/plan/entry-repo-key.js +235 -0
  127. package/dist/plan/entry-workspaces.d.ts +55 -0
  128. package/dist/plan/entry-workspaces.js +90 -0
  129. package/dist/plan/executor.d.ts +231 -19
  130. package/dist/plan/executor.js +1648 -129
  131. package/dist/plan/gates.d.ts +1 -1
  132. package/dist/plan/gates.js +62 -9
  133. package/dist/plan/github-deploy-keys.d.ts +78 -0
  134. package/dist/plan/github-deploy-keys.js +305 -0
  135. package/dist/plan/github-source.d.ts +53 -0
  136. package/dist/plan/github-source.js +140 -0
  137. package/dist/plan/held-key-refs.d.ts +12 -0
  138. package/dist/plan/held-key-refs.js +12 -0
  139. package/dist/plan/hostnames.d.ts +9 -0
  140. package/dist/plan/hostnames.js +28 -1
  141. package/dist/plan/index.d.ts +9 -0
  142. package/dist/plan/index.js +6 -0
  143. package/dist/plan/jobs-scheduler-http.js +3 -0
  144. package/dist/plan/keys-registrar-http.d.ts +32 -0
  145. package/dist/plan/keys-registrar-http.js +105 -0
  146. package/dist/plan/keys-registrar.d.ts +67 -0
  147. package/dist/plan/keys-registrar.js +103 -0
  148. package/dist/plan/kosmos.d.ts +56 -0
  149. package/dist/plan/kosmos.js +84 -0
  150. package/dist/plan/live.d.ts +1 -1
  151. package/dist/plan/locked-hostnames.d.ts +2 -2
  152. package/dist/plan/locked-hostnames.js +7 -4
  153. package/dist/plan/login-host.js +11 -5
  154. package/dist/plan/machine-paths.d.ts +36 -0
  155. package/dist/plan/machine-paths.js +137 -0
  156. package/dist/plan/mcp.d.ts +1 -1
  157. package/dist/plan/mcp.js +49 -17
  158. package/dist/plan/org-apps-admin.d.ts +108 -0
  159. package/dist/plan/org-apps-admin.js +434 -0
  160. package/dist/plan/org-names.d.ts +3 -3
  161. package/dist/plan/org-names.js +15 -2
  162. package/dist/plan/org-postgres-migrate.js +1 -0
  163. package/dist/plan/own-diff.d.ts +23 -0
  164. package/dist/plan/own-diff.js +53 -0
  165. package/dist/plan/own-hostnames.d.ts +2 -0
  166. package/dist/plan/own-hostnames.js +1 -0
  167. package/dist/plan/own-jobs-route.d.ts +47 -0
  168. package/dist/plan/own-jobs-route.js +91 -0
  169. package/dist/plan/own-limits.d.ts +6 -0
  170. package/dist/plan/own-limits.js +1 -0
  171. package/dist/plan/own-services.d.ts +4 -0
  172. package/dist/plan/own-services.js +4 -0
  173. package/dist/plan/plan-apply.d.ts +39 -0
  174. package/dist/plan/plan-apply.js +163 -9
  175. package/dist/plan/platform-diff.d.ts +2 -2
  176. package/dist/plan/platform-diff.js +7 -2
  177. package/dist/plan/platform-org.d.ts +30 -0
  178. package/dist/plan/platform-org.js +12 -1
  179. package/dist/plan/platform-providers.d.ts +2 -0
  180. package/dist/plan/platform-providers.js +13 -2
  181. package/dist/plan/post-logout-uris.d.ts +1 -1
  182. package/dist/plan/post-logout-uris.js +12 -3
  183. package/dist/plan/private-pages.d.ts +26 -0
  184. package/dist/plan/private-pages.js +39 -0
  185. package/dist/plan/providers-readers.js +20 -1
  186. package/dist/plan/proxy-labels.d.ts +12 -1
  187. package/dist/plan/proxy-labels.js +32 -2
  188. package/dist/plan/release-host.d.ts +6 -2
  189. package/dist/plan/release-host.js +36 -8
  190. package/dist/plan/stage-env.d.ts +19 -1
  191. package/dist/plan/stage-env.js +25 -1
  192. package/dist/plan/stage-guards.d.ts +20 -0
  193. package/dist/plan/stage-guards.js +24 -0
  194. package/dist/plan/stage-isolation.d.ts +24 -0
  195. package/dist/plan/stage-isolation.js +29 -1
  196. package/dist/plan/worker-app-database.d.ts +46 -42
  197. package/dist/plan/worker-app-database.js +150 -153
  198. package/dist/plan/worker-app.d.ts +5 -65
  199. package/dist/plan/worker-app.js +28 -105
  200. package/dist/plan/worker.d.ts +2 -2
  201. package/dist/plan/worker.js +2 -1
  202. package/dist/plan/workspace-client.d.ts +25 -0
  203. package/dist/plan/workspace-client.js +48 -0
  204. package/dist/plan/workspace-sign-in.d.ts +55 -0
  205. package/dist/plan/workspace-sign-in.js +273 -0
  206. package/dist/plan/workspace-worker.d.ts +4 -0
  207. package/dist/plan/workspace-worker.js +13 -0
  208. package/dist/plan/workspace.d.ts +101 -0
  209. package/dist/plan/workspace.js +305 -0
  210. package/dist/release/decide.d.ts +30 -1
  211. package/dist/release/decide.js +73 -11
  212. package/dist/release/health.d.ts +8 -3
  213. package/dist/release/health.js +58 -5
  214. package/dist/release/migration-compatibility.d.ts +66 -0
  215. package/dist/release/migration-compatibility.js +340 -0
  216. package/dist/release/run.d.ts +2 -5
  217. package/dist/release/run.js +4 -0
  218. package/dist/release/state.d.ts +3 -1
  219. package/dist/release/state.js +6 -0
  220. package/dist/release/types.d.ts +20 -0
  221. package/dist/requested-changes.d.ts +11 -2
  222. package/dist/requested-changes.js +13 -1
  223. package/dist/service-labels.d.ts +4 -0
  224. package/dist/service-labels.js +11 -0
  225. package/dist/slug.d.ts +2 -0
  226. package/dist/slug.js +20 -7
  227. package/dist/store/postgres.js +25 -2
  228. package/dist/store/schema.d.ts +73 -1
  229. package/dist/store/schema.js +10 -0
  230. package/dist/worker-app-config.d.ts +1 -11
  231. package/dist/worker-app-config.js +4 -17
  232. package/dist/worker-app-migrations.d.ts +105 -0
  233. package/dist/worker-app-migrations.js +166 -0
  234. package/dist/worker-app-upload.d.ts +103 -0
  235. package/dist/worker-app-upload.js +128 -0
  236. package/package.json +19 -2
@@ -1,5 +1,5 @@
1
1
  import { type DeclaredOrg, type HostingEntry } from '../org.js';
2
- import type { DeclaredOrgStore, PlatformStore } from '../store.js';
2
+ import type { DeclaredOrgStore, HeldLease, MutationLease, PlatformStore } from '../store.js';
3
3
  import type { CoolifyPort } from './executor.js';
4
4
  /**
5
5
  * F5 (https://github.com/wtfalch/foundry/issues/314): an entry with
@@ -62,6 +62,11 @@ export interface BrokerEntryWant {
62
62
  loginHost: string;
63
63
  onboardingUrl?: string;
64
64
  };
65
+ /** Present only for an entry with `uses.auth.workspaces`. The key's hash is not part of it: see {@link writeBrokerEntry}. */
66
+ workspaces?: {
67
+ roles: string[];
68
+ inviteUrl: string;
69
+ };
65
70
  /**
66
71
  * `true` for `signIn: 'email-code'`, `false` for an entry that declares `signIn: 'password'`.
67
72
  * Absent when the entry declares no `signIn`: a hand-set `emailLink` stays as it is.
@@ -95,10 +100,22 @@ export interface BrokerPort {
95
100
  is_preview: boolean;
96
101
  }>>;
97
102
  setAuthApps(raw: string): Promise<void>;
103
+ /** Sets one env of the broker app. Needed only to give the broker its `WORKSPACES_DATABASE_URL`. */
104
+ setEnv?(key: string, value: string): Promise<void>;
98
105
  deploy(): Promise<void>;
99
106
  /** The platform's `PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY` value, if any. */
100
107
  recordedDeploy(): Promise<string | undefined>;
101
- recordDeploy(value: string): Promise<void>;
108
+ /** With `fence` (a `platform` lease the caller holds, from {@link withPlatformLease}) the write carries it and takes no lease itself. */
109
+ recordDeploy(value: string, fence?: MutationLease): Promise<void>;
110
+ /**
111
+ * T3b: runs `fn` holding the `platform` mutation lease, so two companies' applies never read, change and
112
+ * write a shared broker env (`AUTH_REGISTRARS`) at once. `fn` gets the lease (undefined for a port with
113
+ * no lease store), which its `recordDeploy` carries. Returns {@link HeldLease} while another holder has
114
+ * it, and `fn` does not run. Optional: a port without it writes unleased.
115
+ */
116
+ withPlatformLease?<T>(fn: (fence: MutationLease | undefined) => Promise<T>): Promise<{
117
+ value: T;
118
+ } | HeldLease>;
102
119
  /**
103
120
  * The id of Archon's own sign-in organisation, when the platform has made it.
104
121
  * No other entry may sign in that organisation, so an entry that does is
@@ -129,10 +146,14 @@ export declare class PlatformBrokerPort implements BrokerPort {
129
146
  is_preview: boolean;
130
147
  }[]>;
131
148
  setAuthApps(raw: string): Promise<void>;
149
+ setEnv(key: string, value: string): Promise<void>;
132
150
  deploy(): Promise<void>;
133
151
  recordedDeploy(): Promise<string | undefined>;
134
152
  archonOrganisationId(): Promise<string | undefined>;
135
- recordDeploy(value: string): Promise<void>;
153
+ withPlatformLease<T>(fn: (fence: MutationLease | undefined) => Promise<T>): Promise<{
154
+ value: T;
155
+ } | HeldLease>;
156
+ recordDeploy(value: string, fence?: MutationLease): Promise<void>;
136
157
  }
137
158
  /**
138
159
  * Writes one entry's broker entry beside the others and deploys the broker
@@ -149,6 +170,10 @@ export declare function writeBrokerEntry(broker: BrokerPort, input: {
149
170
  want: BrokerEntryWant;
150
171
  appKey: string;
151
172
  previous: BrokerEntryWant | null;
173
+ /** sha256 hex of the entry's workspaces key. Required with `want.workspaces`; the key itself never reaches here. */
174
+ workspacesKeyHash?: string;
175
+ /** The operator's held connection string for the broker's workspaces database, written to the broker when it differs. */
176
+ workspacesDatabaseUrl?: string;
152
177
  }): Promise<{
153
178
  invalidForeign: string[];
154
179
  replaced: string | null;
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { isFoundryOwnedBrokerEntryName, } from '../org.js';
3
3
  import { entryClientIdScopeKey } from './agora.js';
4
4
  import { MCP_DATABASE_URL_ENV, MCP_OAUTH_SECRET_ENV } from './broker-mcp.js';
5
+ import { BROKER_WORKSPACES_DATABASE_ENV, wantedWorkspaces, workspacesUrl, } from './entry-workspaces.js';
5
6
  import { entryHostnames } from './hostnames.js';
6
7
  import { loginHostnameFor } from './login-host.js';
7
8
  import { PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, parseBrokerDeployRecord, } from './platform-diff.js';
@@ -80,6 +81,8 @@ export function entryAuthEnv(org, entry, recorded, issuer) {
80
81
  }
81
82
  : {};
82
83
  Object.assign(values, mcpValues);
84
+ if (auth.workspaces)
85
+ Object.assign(values, { WORKSPACES_URL: workspacesUrl(org) });
83
86
  return Object.fromEntries(Object.entries(values).filter(([, value]) => Boolean(value)));
84
87
  }
85
88
  /** The marker `pushAppEnv` records for an entry's sign-in env; the planner compares against it. */
@@ -105,8 +108,17 @@ export function wantedBrokerEntry(org, entry, recorded) {
105
108
  if (auth.mcp && !mcp) {
106
109
  missing.push({ what: 'its MCP audience project id', producedBy: ['zitadel', 'mcp-project'] });
107
110
  }
108
- if (!organizationId || !clientId || !mailDomain || (auth.mcp && !mcp))
111
+ const workspaces = wantedWorkspaces(org, entry);
112
+ if (auth.workspaces && !workspaces) {
113
+ missing.push({ what: 'its public address', producedBy: ['coolify', 'app'] });
114
+ }
115
+ if (!organizationId ||
116
+ !clientId ||
117
+ !mailDomain ||
118
+ (auth.mcp && !mcp) ||
119
+ (auth.workspaces && !workspaces)) {
109
120
  return { missing };
121
+ }
110
122
  return {
111
123
  want: {
112
124
  name: brokerEntryName(org, entry),
@@ -117,10 +129,13 @@ export function wantedBrokerEntry(org, entry, recorded) {
117
129
  registration: auth.registration === 'open',
118
130
  replaces: auth.replacesBrokerEntry ?? null,
119
131
  ...(mcp ? { mcp } : {}),
132
+ ...(workspaces ? { workspaces } : {}),
120
133
  ...(auth.signIn ? { emailLink: auth.signIn === 'email-code' } : {}),
121
134
  },
122
135
  };
123
136
  }
137
+ /** How long {@link PlatformBrokerPort.withPlatformLease} holds the `platform` lease: an env write, two reads, a deploy call and a record. */
138
+ const PLATFORM_LEASE_TTL_MS = 120_000;
124
139
  /** {@link BrokerPort} over the platform's recorded ids and a Coolify client. */
125
140
  export class PlatformBrokerPort {
126
141
  options;
@@ -141,6 +156,9 @@ export class PlatformBrokerPort {
141
156
  { key: 'AUTH_APPS', value: raw },
142
157
  ]);
143
158
  }
159
+ async setEnv(key, value) {
160
+ await this.options.coolify.bulkUpsertEnvs(await this.appId(), [{ key, value }]);
161
+ }
144
162
  async deploy() {
145
163
  await this.options.coolify.deploy(await this.appId());
146
164
  }
@@ -150,8 +168,27 @@ export class PlatformBrokerPort {
150
168
  async archonOrganisationId() {
151
169
  return (await this.options.store.getServiceIds())[PLATFORM_ARCHON_ORG_SCOPE_KEY] || undefined;
152
170
  }
153
- async recordDeploy(value) {
171
+ async withPlatformLease(fn) {
172
+ const { leases } = this.options;
173
+ if (!leases)
174
+ return { value: await fn(undefined) };
175
+ const lease = await leases.acquireMutationLease('platform', randomUUID(), PLATFORM_LEASE_TTL_MS);
176
+ if (!('token' in lease))
177
+ return lease;
178
+ try {
179
+ return { value: await fn(lease) };
180
+ }
181
+ finally {
182
+ await leases.releaseMutationLease(lease).catch(() => undefined);
183
+ }
184
+ }
185
+ async recordDeploy(value, fence) {
154
186
  const { leases, store } = this.options;
187
+ if (fence) {
188
+ const ids = await store.getServiceIds();
189
+ await store.putServiceIds({ ...ids, [PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY]: value }, fence);
190
+ return;
191
+ }
155
192
  const lease = await leases?.acquireMutationLease('platform', randomUUID(), 60_000);
156
193
  if (lease && !('token' in lease))
157
194
  return;
@@ -187,6 +224,24 @@ export async function writeBrokerEntry(broker, input) {
187
224
  throw new Error(`${label} would sign in Archon's own organisation, which no other entry may`);
188
225
  }
189
226
  const brokerEnvs = await broker.listEnvs();
227
+ // The broker does not start without the database once an entry declares workspaces, and then no one signs in.
228
+ let pendingDatabase = null;
229
+ if (want.workspaces) {
230
+ if (!input.workspacesKeyHash) {
231
+ throw new Error(`${label}: the workspaces key hash is missing, so nothing was written`);
232
+ }
233
+ const liveDatabase = brokerEnvs.find((e) => e.key === BROKER_WORKSPACES_DATABASE_ENV && !e.is_preview)?.value;
234
+ const wanted = input.workspacesDatabaseUrl ?? liveDatabase;
235
+ if (!wanted) {
236
+ throw new Error(`${label}: the broker has no ${BROKER_WORKSPACES_DATABASE_ENV}, so nothing was written`);
237
+ }
238
+ if (wanted !== liveDatabase) {
239
+ if (!broker.setEnv) {
240
+ throw new Error(`${label}: the broker port cannot set ${BROKER_WORKSPACES_DATABASE_ENV}`);
241
+ }
242
+ pendingDatabase = wanted;
243
+ }
244
+ }
190
245
  const env = brokerEnvs.find((e) => e.key === 'AUTH_APPS' && !e.is_preview);
191
246
  // A value that reads back empty is not an empty list: writing over it
192
247
  // would drop every hand-written entry.
@@ -235,6 +290,15 @@ export async function writeBrokerEntry(broker, input) {
235
290
  entry.mcp = want.mcp;
236
291
  else
237
292
  entry.mcp = undefined;
293
+ // The previous write's `workspaces` goes when the entry no longer declares it; the key hash goes with it.
294
+ if (want.workspaces && input.workspacesKeyHash) {
295
+ entry.workspaces = want.workspaces;
296
+ entry.workspacesKey = input.workspacesKeyHash;
297
+ }
298
+ else {
299
+ entry.workspaces = undefined;
300
+ entry.workspacesKey = undefined;
301
+ }
238
302
  // A hand-set `emailLink` stays unless the entry declares `signIn`, or declared it before and stopped.
239
303
  if (want.emailLink !== undefined)
240
304
  entry.emailLink = want.emailLink;
@@ -275,6 +339,12 @@ export async function writeBrokerEntry(broker, input) {
275
339
  next.push(existing);
276
340
  });
277
341
  place();
342
+ // The database first: a failure after it leaves a broker env nothing reads yet, never an entry the broker cannot start with.
343
+ let databaseWritten = false;
344
+ if (pendingDatabase !== null) {
345
+ await broker.setEnv?.(BROKER_WORKSPACES_DATABASE_ENV, pendingDatabase);
346
+ databaseWritten = true;
347
+ }
278
348
  const nextRaw = JSON.stringify(next);
279
349
  const changed = JSON.stringify(current) !== nextRaw;
280
350
  if (changed) {
@@ -291,7 +361,8 @@ export async function writeBrokerEntry(broker, input) {
291
361
  // so that one does not hold back the deploy.
292
362
  const missing = (await missingBrokerInputs({ listEnvs: () => broker.listEnvs() }, brokerAppId)).filter((input) => input !== 'AUTH_APPS');
293
363
  const live = changed ? nextRaw : raw;
294
- if (brokerDeployNeeded(missing, brokerAppId, live, parseBrokerDeployRecord(await broker.recordedDeploy()))) {
364
+ if (databaseWritten ||
365
+ brokerDeployNeeded(missing, brokerAppId, live, parseBrokerDeployRecord(await broker.recordedDeploy()))) {
295
366
  await broker.deploy();
296
367
  // The platform's record of what the broker last deployed, so its next
297
368
  // apply does not read this write as drift and deploy the broker again.
@@ -0,0 +1,168 @@
1
+ import type { DeclaredOrg } from '../org.js';
2
+ import type { BrokerEntryInput, BrokerPort } from './broker-entries.js';
3
+ /**
4
+ * T3 (campaign 2026-10-20, apps-modules): a company with customers gets one registrar key at the sign-in
5
+ * broker, so its Boule can register the company's own apps there (`PUT /apps/:organizationId/:name`, auth ADR
6
+ * 0001 and 0003) without Archon. Foundry makes the key, holds it at {@link registrarRef}, writes the
7
+ * company's record into the broker's `AUTH_REGISTRARS` (`broker/src/apps.ts`'s `parseRegistrars`: the key
8
+ * itself, the company's sign-in organisation, its customers organisations, its login host) and gives Boule
9
+ * `AUTH_REGISTRAR_URL` (the broker's base, `<issuer>/api`, the address `@wtfalch/auth` already calls) and
10
+ * `AUTH_REGISTRAR_KEY`.
11
+ *
12
+ * Two steps deliver it, and either may run first: the Boule step (`coolify:boule`) puts the key in Boule's
13
+ * env, the registrar step (`coolify:broker-registrar`) puts the record in the broker's. Each mint gets an id
14
+ * (never the key) recorded at {@link REGISTRAR_MINTED_SCOPE_KEY}; each step records which mint it delivered,
15
+ * so a key made again reaches both places. The plan never holds the key.
16
+ */
17
+ export declare const AUTH_REGISTRARS_ENV = "AUTH_REGISTRARS";
18
+ export { AUTH_REGISTRAR_KEY_ENV, AUTH_REGISTRAR_URL_ENV, registrarUrl, registrarWanted, } from './boule.js';
19
+ /** The id of the held key's mint (a random id, not the key), set once it is held. Cleared when the key is forgotten. */
20
+ export declare const REGISTRAR_MINTED_SCOPE_KEY = "registrar_minted";
21
+ /** The mint id whose key the last successful Boule env write carried. */
22
+ export declare const REGISTRAR_DELIVERED_SCOPE_KEY = "registrar_delivered";
23
+ /** What the last successful broker write said (JSON of {@link RegistrarRecord} plus the mint id). Never the key. */
24
+ export declare const REGISTRAR_APPLIED_SCOPE_KEY = "broker_registrar_applied";
25
+ /** The broker inputs that were missing when the record was written, so the broker was not deployed with it. */
26
+ export declare const REGISTRAR_DEPLOY_WAITS_SCOPE_KEY = "broker_registrar_deploy_waits";
27
+ /** Why the last removal wrote nothing (another company's record the broker refuses), so the key was kept. Never a key. */
28
+ export declare const REGISTRAR_REMOVAL_HELD_SCOPE_KEY = "broker_registrar_removal_held";
29
+ /** The held-key ref of a company's registrar key. */
30
+ export declare const registrarRef: (orgId: string) => string;
31
+ /** 32 random bytes, base64url: 43 characters, over the broker's 32-character floor. */
32
+ export declare const mintRegistrarKey: () => string;
33
+ interface HeldKeys {
34
+ resolve(ref: string): Promise<string | null | undefined>;
35
+ put?(ref: string, value: string): Promise<void>;
36
+ }
37
+ /**
38
+ * The company's registrar key: read from the held keys, else made and held first, and its mint id recorded
39
+ * and persisted before the key goes anywhere, so a crash after the `put` hands the same key out next time. A
40
+ * mint recorded but no longer held (F5, Q36: the held value was lost) is made again by this step, never by a
41
+ * person removing a record: the new mint id is recorded and persisted BEFORE the new key is held, so a crash
42
+ * between the two leaves a mint nobody was given and the next run makes another, and a crash after the `put`
43
+ * hands the held key out. The mint id differs from the lost one, so the Boule step and the broker step (each
44
+ * records which mint it delivered) both deliver the new key; the broker's write replaces the company's old
45
+ * record by its organisation. Errors name the ref, never the key. `secret` registers it for redaction; `log`
46
+ * gets one plain line when a lost key is made again.
47
+ */
48
+ export declare function ensureRegistrarKey(input: {
49
+ orgId: string;
50
+ slug: string;
51
+ heldKeys: HeldKeys | undefined;
52
+ recorded: () => Readonly<Record<string, string>>;
53
+ record: (key: string, value: string) => void;
54
+ persist: () => Promise<void>;
55
+ secret: (value: string) => void;
56
+ log?: (line: string) => void;
57
+ }): Promise<{
58
+ key: string;
59
+ mint: string;
60
+ }>;
61
+ /** The company's record in `AUTH_REGISTRARS`, without its key. */
62
+ export interface RegistrarRecord {
63
+ /** The company's own sign-in organisation (its Boule's `AUTH_ORGANIZATION_ID`). */
64
+ organizationId: string;
65
+ /** Its customers organisation, then its test customers organisation once that exists. */
66
+ customers: string[];
67
+ loginHost: string;
68
+ }
69
+ /**
70
+ * What the company's record says, or each input still missing and the step that records it. The customers
71
+ * organisation is an input only while `customers.enabled`: a company whose Boule runs apps (F5) without
72
+ * customers has a record with no customers, and the broker registers its apps against the sign-in organisation.
73
+ */
74
+ export declare function wantedRegistrar(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): {
75
+ want: RegistrarRecord;
76
+ } | {
77
+ missing: BrokerEntryInput[];
78
+ };
79
+ /** The applied marker: the record and the mint it carries the key of. */
80
+ export declare const registrarAppliedValue: (record: RegistrarRecord, mint: string) => string;
81
+ /** The organisation id the last broker write named, or null. */
82
+ export declare function appliedRegistrarOrganization(recorded: Readonly<Record<string, string>>): string | null;
83
+ /**
84
+ * `null` when the broker's `parseRegistrars` takes `list`, else why not, naming the entry by its place and
85
+ * organisation, never its key. The broker stops at boot on a list it refuses, and then nobody signs in, so
86
+ * nothing is written that it would refuse.
87
+ */
88
+ export declare function registrarsProblem(list: readonly unknown[],
89
+ /** How an entry is named; by default its place in `list`. */
90
+ labelOf?: (index: number) => string): string | null;
91
+ /** What {@link writeRegistrar} did: whether the value changed, what the broker waits for, and why a removal wrote nothing. */
92
+ export interface RegistrarWrite {
93
+ changed: boolean;
94
+ waits: string[];
95
+ /**
96
+ * A removal that wrote nothing because another company's record in the list is one the broker refuses
97
+ * (it would not start on that list with or without this company's record). The company's record stays,
98
+ * so its key is kept. Names the entry by its place, never a key.
99
+ */
100
+ removalHeld?: string;
101
+ }
102
+ /**
103
+ * Puts the company's record (or, with `record` null, takes it away) in the broker's `AUTH_REGISTRARS`,
104
+ * beside every other company's record, which is copied back as it was. The company's own record is the one
105
+ * naming its organisation, the organisation the last write named, any of `ownOrganizationIds`, or the one
106
+ * carrying `ownKey`. The read, change, write and deploy run under the `platform` lease (`BrokerPort`'s
107
+ * `withPlatformLease`), and the value is read again after the write: a record not as written fails the
108
+ * step. Deploys the broker when the value changed and the broker has its other inputs; otherwise returns
109
+ * the inputs it waits for, and the broker reads the record on its next deploy. Errors name the company and
110
+ * the reason, never a key.
111
+ */
112
+ export declare function writeRegistrar(broker: BrokerPort, input: {
113
+ slug: string;
114
+ record: (RegistrarRecord & {
115
+ key: string;
116
+ }) | null;
117
+ previousOrganizationId: string | null;
118
+ /** More organisations whose record is this company's (its recorded sign-in organisation), for a removal after a crash before the applied marker. */
119
+ ownOrganizationIds?: readonly string[];
120
+ /** The company's held key: a record carrying it is the company's whatever organisation it names. */
121
+ ownKey?: string | null;
122
+ /** Told every key it reads or writes (every company's), so the caller can redact them from an error. */
123
+ secret?: (value: string) => void;
124
+ /** One plain line per thing worth a log (a replaced hand-written record, an unleased write). Never a key. */
125
+ log?: (line: string) => void;
126
+ }): Promise<RegistrarWrite>;
127
+ /** A planned step, as `diff.ts` collects them. */
128
+ interface RegistrarDraftStep {
129
+ action: {
130
+ system: 'coolify';
131
+ kind: string;
132
+ key: string;
133
+ desired: Record<string, unknown>;
134
+ };
135
+ step: {
136
+ description: string;
137
+ destroys: boolean;
138
+ migrates: boolean;
139
+ };
140
+ }
141
+ /**
142
+ * The registrar for an org: whether the Boule step delivers the key (`deliver`), the broker step, and every
143
+ * reason either is held back as a plan line. Nothing at all for a company that never had customers, so
144
+ * its plan is unchanged.
145
+ */
146
+ export declare function planBrokerRegistrar(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, plannedSteps: ReadonlyArray<{
147
+ action: {
148
+ system: string;
149
+ kind: string;
150
+ };
151
+ }>, host: {
152
+ brokerWired: boolean;
153
+ canHoldSecrets: boolean;
154
+ issuerKnown: boolean;
155
+ }): {
156
+ deliver: boolean;
157
+ /** Customers are off but the record cannot be taken away yet: Boule keeps its registrar env, in step with it. */
158
+ keepBouleEnv: boolean;
159
+ step: RegistrarDraftStep | null;
160
+ blocked: string[];
161
+ };
162
+ /**
163
+ * The Boule env drift the registrar adds: the key missing, or not the mint the held key is (made again since
164
+ * Boule got it). And, once customers are off, the rows foundry wrote, which the Boule step takes away.
165
+ */
166
+ export declare function bouleRegistrarDrift(org: Pick<DeclaredOrg, 'customers' | 'boule'>, recorded: Readonly<Record<string, string>>, liveEnv: Readonly<Record<string, string>>, deliver: boolean,
167
+ /** {@link planBrokerRegistrar}'s `keepBouleEnv`: the record stays at the broker, so Boule keeps the key. */
168
+ keep?: boolean): string[];