@lamalibre/create-lamaste 4.4.1 → 4.6.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 (170) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +234 -18
  3. package/src/index.js +1 -0
  4. package/src/lib/backend-cookie.js +65 -0
  5. package/src/lib/ip-panel-vhost.js +208 -0
  6. package/src/lib/service-config.js +5 -3
  7. package/src/tasks/nginx.js +18 -135
  8. package/src/tasks/redeploy.js +39 -1
  9. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  10. package/vendor/docs/00-introduction/quickstart.md +5 -5
  11. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  12. package/vendor/docs/01-concepts/authentication.md +1 -1
  13. package/vendor/docs/01-concepts/authorization.md +66 -48
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/mtls.md +2 -2
  16. package/vendor/docs/01-concepts/security-model.md +70 -33
  17. package/vendor/docs/01-concepts/tickets.md +2 -2
  18. package/vendor/docs/01-concepts/tunneling.md +4 -4
  19. package/vendor/docs/02-guides/agent-setup.md +5 -5
  20. package/vendor/docs/02-guides/certificate-management.md +2 -2
  21. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  22. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  23. package/vendor/docs/02-guides/installation.md +3 -3
  24. package/vendor/docs/02-guides/managing-users.md +113 -25
  25. package/vendor/docs/02-guides/onboarding.md +2 -2
  26. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  27. package/vendor/docs/02-guides/static-sites.md +4 -0
  28. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  29. package/vendor/docs/03-architecture/installer.md +4 -4
  30. package/vendor/docs/03-architecture/management-flow.md +3 -1
  31. package/vendor/docs/03-architecture/nginx-configuration.md +229 -24
  32. package/vendor/docs/03-architecture/overview.md +13 -17
  33. package/vendor/docs/03-architecture/panel-client.md +6 -6
  34. package/vendor/docs/03-architecture/panel-server.md +3 -2
  35. package/vendor/docs/03-architecture/state-management.md +4 -8
  36. package/vendor/docs/03-architecture/system-overview.md +1 -1
  37. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +378 -34
  38. package/vendor/docs/04-api-reference/app-host.md +3 -3
  39. package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
  40. package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
  41. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  42. package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
  43. package/vendor/docs/04-api-reference/sites.md +4 -0
  44. package/vendor/docs/04-api-reference/tunnels.md +87 -47
  45. package/vendor/docs/04-api-reference/users.md +241 -45
  46. package/vendor/docs/05-operations/backup-and-restore.md +35 -26
  47. package/vendor/docs/05-operations/monitoring.md +39 -0
  48. package/vendor/docs/05-operations/uninstalling.md +2 -1
  49. package/vendor/docs/05-operations/upgrades.md +24 -6
  50. package/vendor/docs/06-reference/config-files.md +100 -202
  51. package/vendor/docs/06-reference/glossary.md +3 -3
  52. package/vendor/docs/06-reference/installer-flags.md +13 -13
  53. package/vendor/docs/06-reference/ports-and-services.md +25 -26
  54. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  55. package/vendor/docs/gatekeeper-design.md +10 -8
  56. package/vendor/docs/identity-directory-design.md +48 -14
  57. package/vendor/docs/package.json +1 -1
  58. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  59. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  60. package/vendor/gatekeeper/dist/index.d.ts +4 -3
  61. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  62. package/vendor/gatekeeper/dist/index.js +4 -2
  63. package/vendor/gatekeeper/dist/index.js.map +1 -1
  64. package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
  65. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
  66. package/vendor/gatekeeper/dist/lib/authz.js +31 -0
  67. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
  68. package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
  69. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
  70. package/vendor/gatekeeper/dist/lib/constants.js +5 -2
  71. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
  72. package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
  73. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  74. package/vendor/gatekeeper/dist/lib/grants.js +6 -1
  75. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  76. package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
  77. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
  78. package/vendor/gatekeeper/dist/lib/groups.js +5 -1
  79. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
  80. package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
  81. package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
  82. package/vendor/gatekeeper/dist/lib/principals.js +67 -0
  83. package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
  84. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +10 -0
  85. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
  86. package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
  87. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
  88. package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
  89. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  90. package/vendor/gatekeeper/dist/server/index.js +49 -56
  91. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  92. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
  93. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  94. package/vendor/gatekeeper/dist/server/routes/authz.js +332 -104
  95. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  96. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  97. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
  98. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  99. package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
  100. package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
  101. package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
  102. package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
  103. package/vendor/gatekeeper/package.json +2 -2
  104. package/vendor/server/bin/lamaste-server.js +15 -1
  105. package/vendor/server/package.json +2 -2
  106. package/vendor/server/src/commands/plugin-identity.js +335 -0
  107. package/vendor/server/src/commands/plugin-web-surface.js +269 -0
  108. package/vendor/server/src/commands/plugins.js +156 -11
  109. package/vendor/server/src/commands/registry-credential.js +2 -1
  110. package/vendor/server/src/commands/sites.js +15 -1
  111. package/vendor/server/src/commands/tunnels.js +102 -2
  112. package/vendor/server/src/commands/uninstall.js +3 -1
  113. package/vendor/server/src/commands/users.js +358 -0
  114. package/vendor/server/src/plugin-approval.js +186 -5
  115. package/vendor/server/src/plugin-identity.js +382 -0
  116. package/vendor/server/src/plugin-web-surface.js +307 -0
  117. package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
  118. package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
  119. package/vendor/server-ui/dist/index.html +2 -2
  120. package/vendor/serverd/package.json +2 -2
  121. package/vendor/serverd/src/index.js +34 -5
  122. package/vendor/serverd/src/lib/audience.js +576 -0
  123. package/vendor/serverd/src/lib/authelia.js +78 -7
  124. package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
  125. package/vendor/serverd/src/lib/chisel.js +10 -2
  126. package/vendor/serverd/src/lib/config.js +68 -43
  127. package/vendor/serverd/src/lib/directory.js +1113 -0
  128. package/vendor/serverd/src/lib/entitlements.js +517 -0
  129. package/vendor/serverd/src/lib/gatekeeper-client.js +226 -10
  130. package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
  131. package/vendor/serverd/src/lib/identity-sync.js +905 -0
  132. package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
  133. package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
  134. package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
  135. package/vendor/serverd/src/lib/mtls.js +6 -2
  136. package/vendor/serverd/src/lib/nginx.js +546 -48
  137. package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
  138. package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
  139. package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
  140. package/vendor/serverd/src/lib/plugin-packages.js +74 -2
  141. package/vendor/serverd/src/lib/plugins.js +95 -7
  142. package/vendor/serverd/src/lib/registry-credential.js +9 -6
  143. package/vendor/serverd/src/lib/request-logging.js +19 -0
  144. package/vendor/serverd/src/lib/retained-data.js +50 -4
  145. package/vendor/serverd/src/lib/state.js +36 -0
  146. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  147. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  148. package/vendor/serverd/src/lib/user-caller.js +67 -0
  149. package/vendor/serverd/src/lib/user-surface.js +754 -0
  150. package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
  151. package/vendor/serverd/src/lib/web-surface.js +766 -0
  152. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  153. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  154. package/vendor/serverd/src/routes/invite.js +53 -12
  155. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  156. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  157. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  158. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  159. package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
  160. package/vendor/serverd/src/routes/management/plugins.js +2 -0
  161. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  162. package/vendor/serverd/src/routes/management/sites.js +143 -65
  163. package/vendor/serverd/src/routes/management/tunnels.js +109 -22
  164. package/vendor/serverd/src/routes/management/users.js +290 -208
  165. package/vendor/serverd/src/routes/management.js +6 -0
  166. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  167. package/vendor/serverd/src/routes/user-access.js +17 -4
  168. package/vendor/serverd/src/routes/user-surface.js +69 -0
  169. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  170. package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
@@ -5,14 +5,17 @@
5
5
  *
6
6
  * Approval binds bytes (see `hook-set.ts` in the core library): one digest
7
7
  * (`approvalDigest`) covers the relay hook set — the normalised relay
8
- * declarations and the SHA-256 of the relay entry module — and the agent
9
- * half Lamaste installs onto the hosting agent — its package, version,
10
- * tarball integrity, service and system packages. Approval is needed when
11
- * either exists. Lamaste runs relay hooks, hosts the plugin and installs its
12
- * agent half only while the installed version's digest is the approved
13
- * one. Settings are outside the digest, but an update that turns a secret
14
- * setting into a plain one waits for an administrator's approval all the
15
- * same (`plugin-lifecycle.js`): the same pending update, approved here.
8
+ * declarations and the SHA-256 of the relay entry module — the agent half
9
+ * Lamaste installs onto the hosting agent — its package, version, tarball
10
+ * integrity, service and system packages — and the plugin's identity block
11
+ * — the roles its users hold and the attributes it receives about them.
12
+ * Approval is needed when any exists. Lamaste runs relay hooks, hosts the
13
+ * plugin, installs its agent half and sends it its users only while the
14
+ * installed version's digest is the approved one. Settings are outside the
15
+ * digest, but an update that turns a secret setting into a plain one waits
16
+ * for an administrator's approval all the same (`plugin-lifecycle.js`), and
17
+ * so does one that adds or removes a role or widens the attributes: the
18
+ * same pending update, approved here.
16
19
  *
17
20
  * The runner is about the relay part alone: it holds a root-owned copy of
18
21
  * the approved module that lamaste-priv installed after checking its
@@ -216,22 +219,44 @@ export function installedAgentHalf(plugin) {
216
219
  }
217
220
 
218
221
  /**
219
- * What approving a plugin version covers — its relay hook set and the
220
- * agent half Lamaste installs — and the digest the approval names. `digest`
221
- * is null when there is nothing to approve.
222
+ * The web tree a plugin version's approval covers: `{ treeSha256 }` when it
223
+ * declares a web surface (`userSurface.web`), null otherwise. A declared
224
+ * surface without the tree's digest cannot be approved.
225
+ */
226
+ function installedWebTree(plugin) {
227
+ if (!plugin.userSurface?.web) return null;
228
+ if (typeof plugin.webTreeSha256 !== 'string') {
229
+ throw new Error(`Plugin "${plugin.name}" declares a web surface but its web tree is unknown`);
230
+ }
231
+ return { treeSha256: plugin.webTreeSha256 };
232
+ }
233
+
234
+ /**
235
+ * What approving a plugin version covers — its relay hook set, the agent
236
+ * half Lamaste installs, its identity block (normalized, as the registry
237
+ * stores it) and its web tree — and the digest the approval names.
238
+ * `digest` is null when there is nothing to approve. A plugin without an
239
+ * identity block or a web surface has the digest it had before they
240
+ * existed.
222
241
  */
223
242
  export function installedApproval(plugin) {
224
243
  const relay = installedRelayHooks(plugin);
225
244
  const agentHalf = installedAgentHalf(plugin);
226
- const required = relay !== null || agentHalf !== null;
245
+ const identity = plugin.identity ?? null;
246
+ const web = installedWebTree(plugin);
247
+ const required = relay !== null || agentHalf !== null || identity !== null || web !== null;
227
248
  return {
228
249
  relay,
229
250
  agentHalf,
251
+ identity,
252
+ web,
230
253
  required,
231
254
  digest: required
232
255
  ? approvalDigest({
233
256
  relay: relay ? { set: relay.set, entrySha256: relay.entrySha256 } : null,
234
257
  agentHalf,
258
+ identity,
259
+ web,
235
260
  })
236
261
  : null,
237
262
  };
@@ -243,9 +268,10 @@ function isApproved(approval, state) {
243
268
  }
244
269
 
245
270
  /**
246
- * Whether the installed version is approved — it brings neither relay hooks
247
- * nor an agent half Lamaste installs, or its digest is the approved one.
248
- * Relay hooks run, and the plugin is hosted, only then.
271
+ * Whether the installed version is approved — it brings no relay hooks, no
272
+ * agent half Lamaste installs and no identity block, or its digest is the
273
+ * approved one. Relay hooks run, the plugin is hosted and its users are
274
+ * sent to it only then.
249
275
  */
250
276
  export async function installedVersionApproved(plugin) {
251
277
  const approval = installedApproval(plugin);
@@ -253,17 +279,40 @@ export async function installedVersionApproved(plugin) {
253
279
  return isApproved(approval, await readHookState(plugin.name));
254
280
  }
255
281
 
256
- /** What the panel shows about a plugin's hooks, its agent half and their approval. */
282
+ /**
283
+ * What the panel shows about a plugin's hooks, its agent half, its identity
284
+ * (`identity`: whether the installed version declares it, whether that
285
+ * version is approved, and the normalized block) and their approval.
286
+ */
257
287
  export async function hookStatus(plugin) {
258
288
  const approval = installedApproval(plugin);
259
- const { relay, agentHalf } = approval;
289
+ const { relay, agentHalf, identity, web } = approval;
260
290
  const agent = agentHookSetOf({ hooks: plugin.hooks });
261
291
  const state = await readHookState(plugin.name);
262
292
  const approved = isApproved(approval, state);
293
+ let pendingUpdate = null;
294
+ if (state?.pendingUpdate) {
295
+ const { withIdentityGrantImpact } = await import('./entitlements.js');
296
+ pendingUpdate = await withIdentityGrantImpact(
297
+ pendingUpdateView(state.pendingUpdate),
298
+ plugin.route,
299
+ );
300
+ }
263
301
  return {
264
302
  relay: relay ? relayHookSetPreview(relay.set, relay.entrySha256) : null,
265
303
  agent,
266
304
  agentHalf: agentHalf ? agentHalfPreview(derivePluginRoute(plugin.name), agentHalf) : null,
305
+ identity: {
306
+ declared: identity !== null,
307
+ approved: identity !== null && approved,
308
+ block: identity,
309
+ },
310
+ web: {
311
+ declared: web !== null,
312
+ approved: web !== null && approved,
313
+ treeSha256: web?.treeSha256 ?? null,
314
+ surface: plugin.userSurface?.web ?? null,
315
+ },
267
316
  approval: {
268
317
  required: approval.required,
269
318
  state: !approval.required ? 'not-required' : approved ? 'approved' : 'pending',
@@ -279,7 +328,7 @@ export async function hookStatus(plugin) {
279
328
  installedDigest: state?.installedDigest ?? null,
280
329
  current: relay !== null && approved && state?.installedDigest === relay.digest,
281
330
  },
282
- pendingUpdate: state?.pendingUpdate ? pendingUpdateView(state.pendingUpdate) : null,
331
+ pendingUpdate,
283
332
  };
284
333
  }
285
334
 
@@ -287,22 +336,30 @@ export async function hookStatus(plugin) {
287
336
  export const DIGEST_UNAPPROVED = 'digest.unapproved';
288
337
  /** Why an update waits: it turns secret settings into plain ones. */
289
338
  export const SETTINGS_SECRET_TO_PLAIN = 'settings.secret-to-plain';
339
+ /** Why an update waits: it adds or removes a role, or widens the attributes it receives. */
340
+ export const IDENTITY_CHANGED = 'identity.changed';
290
341
 
291
342
  /**
292
343
  * The reason a staged version waits because what it brings — its relay
293
- * hooks (`relay`), the agent half Lamaste installs (`agentHalf`); either
294
- * truthy when it brings one — is not what was approved.
344
+ * hooks (`relay`), the agent half Lamaste installs (`agentHalf`), its
345
+ * identity block (`identity`), its web tree (`web`); each truthy when it
346
+ * brings one — is not what was approved.
295
347
  *
296
348
  * @returns {{ code: string, reason: string }}
297
349
  */
298
- export function digestUnapprovedReason({ relay, agentHalf }) {
299
- const what =
300
- relay && agentHalf
301
- ? 'its relay hooks and agent half are'
302
- : agentHalf
303
- ? 'its agent half is'
304
- : 'its relay hooks are';
305
- return { code: DIGEST_UNAPPROVED, reason: `${what} not what was approved` };
350
+ export function digestUnapprovedReason({ relay, agentHalf, identity = null, web = null }) {
351
+ const parts = [
352
+ ...(relay || (!agentHalf && !identity && !web) ? ['relay hooks'] : []),
353
+ ...(agentHalf ? ['agent half'] : []),
354
+ ...(identity ? ['identity'] : []),
355
+ ...(web ? ['web surface'] : []),
356
+ ];
357
+ const named =
358
+ parts.length === 1
359
+ ? parts[0]
360
+ : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;
361
+ const verb = parts.length === 1 && parts[0] !== 'relay hooks' ? 'is' : 'are';
362
+ return { code: DIGEST_UNAPPROVED, reason: `its ${named} ${verb} not what was approved` };
306
363
  }
307
364
 
308
365
  /**
@@ -314,7 +371,12 @@ export function digestUnapprovedReason({ relay, agentHalf }) {
314
371
  function pendingApprovalRequired(pending) {
315
372
  return (
316
373
  pending.approvalRequired ?? [
317
- digestUnapprovedReason({ relay: pending.preview, agentHalf: pending.agentHalf }),
374
+ digestUnapprovedReason({
375
+ relay: pending.preview,
376
+ agentHalf: pending.agentHalf,
377
+ identity: pending.identity?.after ?? null,
378
+ web: pending.web?.after ?? null,
379
+ }),
318
380
  ]
319
381
  );
320
382
  }
@@ -323,14 +385,21 @@ function pendingApprovalRequired(pending) {
323
385
  * A staged update waiting for approval as clients see it — in the hook
324
386
  * status and in the update response alike: its version, the digest to
325
387
  * approve it with (null when it brings neither relay hooks nor an agent
326
- * half Lamaste installs: it is approved by its version), what it brings
327
- * (its relay hook set and the agent half Lamaste installs, each null when
328
- * it brings none), how it changes the settings schema (`settings`:
329
- * `{ added: [{ key, type }], removed: [{ key, type }], retyped: [{ key,
330
- * from, to }], newRequired: [key], secretToPlain: [key] }`, null when
331
- * neither version declares settings — not part of the digest), why it
332
- * waits for an administrator (`approvalRequired`: `[{ code, reason }]`,
333
- * `digest.unapproved` and `settings.secret-to-plain`; never empty), and —
388
+ * half Lamaste installs nor an identity block: it is approved by its
389
+ * version), what it brings (its relay hook set and the agent half Lamaste
390
+ * installs, each null when it brings none), how it changes the settings
391
+ * schema (`settings`: `{ added: [{ key, type }], removed: [{ key, type }],
392
+ * retyped: [{ key, from, to }], newRequired: [key], secretToPlain: [key] }`,
393
+ * null when neither version declares settings — not part of the digest),
394
+ * how it changes the identity block (`identity`: `{ before, after,
395
+ * rolesAdded, rolesRemoved, attributesAdded, attributesRemoved,
396
+ * labelsChanged }` with the normalized blocks, null when neither version
397
+ * declares one — part of the digest), its web tree (`web`: `{ before,
398
+ * after }`, the installed version's tree digest and the staged one's
399
+ * `{ sha256, files, totalBytes }`, null when neither declares a web
400
+ * surface — part of the digest), why it waits for an administrator
401
+ * (`approvalRequired`: `[{ code, reason }]`, `digest.unapproved`,
402
+ * `settings.secret-to-plain` and `identity.changed`; never empty), and —
334
403
  * when the plugin has auto-approve on — why auto-approve did not cover it
335
404
  * (`autoApproveWithheld`, sentences; null otherwise). The stored record
336
405
  * names the relay hook set's preview `preview`.
@@ -343,6 +412,8 @@ export function pendingUpdateView(pending) {
343
412
  relay: pending.preview ?? null,
344
413
  agentHalf: pending.agentHalf ?? null,
345
414
  settings: pending.settings ?? null,
415
+ identity: pending.identity ?? null,
416
+ web: pending.web ?? null,
346
417
  approvalRequired: pendingApprovalRequired(pending),
347
418
  autoApproveWithheld: pending.autoApproveWithheld ?? null,
348
419
  };
@@ -462,12 +533,14 @@ export async function restartRelayHookRunner(plugin, logger) {
462
533
 
463
534
  /**
464
535
  * An administrator approves what a plugin version brings — its relay hook
465
- * set and the agent half Lamaste installs — with the digest they were
466
- * shown: the installed version's, or a pending update's. A pending update
467
- * is also approved by naming its `version`, with its digest when it has
468
- * one — the only way to approve one that brings neither relay hooks nor an
469
- * agent half (its digest is null), and what binds the approval of a
470
- * settings change to the release that was shown.
536
+ * set, the agent half Lamaste installs and its identity block — with the
537
+ * digest they were shown: the installed version's, or a pending update's.
538
+ * A pending update is also approved by naming its `version`, with its
539
+ * digest when it has one — the only way to approve one that brings neither
540
+ * relay hooks nor an agent half nor an identity block (its digest is
541
+ * null), and what binds the approval of a settings change to the release
542
+ * that was shown. Approving an installed version that declares identity
543
+ * computes its audience and sends it to its agent half.
471
544
  *
472
545
  * The installed version's approval is recorded here, in the hook state and
473
546
  * the audit log. A pending update's is checked against the update that is
@@ -506,7 +579,7 @@ export async function approveHookSet(
506
579
  const pendingDigest = pending.digest ?? null;
507
580
  if (digest === null && pendingDigest !== null) {
508
581
  throw httpError(
509
- `Version ${version} brings relay hooks or an agent half — approve it with the approval digest it shows`,
582
+ `Version ${version} brings relay hooks, an agent half or an identity block — approve it with the approval digest it shows`,
510
583
  400,
511
584
  );
512
585
  }
@@ -530,7 +603,7 @@ export async function approveHookSet(
530
603
  if (pending && pending.digest === digest) return pendingUpdateApproval(pending);
531
604
  if (!approval.required) {
532
605
  throw httpError(
533
- `Plugin "${plugin.name}" has no relay hooks and no agent half Lamaste installs — nothing to approve`,
606
+ `Plugin "${plugin.name}" has no relay hooks, no agent half Lamaste installs and no identity block — nothing to approve`,
534
607
  400,
535
608
  );
536
609
  }
@@ -553,6 +626,15 @@ export async function approveHookSet(
553
626
  details: { digest, version: plugin.version, for: 'installed', certSerial },
554
627
  });
555
628
  await ensureRelayHooksInstalled(plugin, logger);
629
+ // An approved identity block lets the plugin's users through: its
630
+ // audience is computed and sent to its agent half (when it is hosted), and
631
+ // the hosts its user surface is bound to start serving it.
632
+ if (approval.identity) {
633
+ const { refreshPluginIdentity } = await import('./identity-sync.js');
634
+ await refreshPluginIdentity(plugin.name, logger);
635
+ const { refreshUserSurfaceVhosts } = await import('./user-surface.js');
636
+ await refreshUserSurfaceVhosts(plugin.route, logger);
637
+ }
556
638
  return { target: 'installed', digest };
557
639
  }
558
640
 
@@ -69,6 +69,8 @@ import { recordAudit } from './audit.js';
69
69
  import { onHealthTransition } from './tunnel-hooks.js';
70
70
  import { agentLastSeenAt, agentPresenceMap } from './agent-presence.js';
71
71
  import { refreshPluginSettingsBestEffort } from './plugin-settings.js';
72
+ import { refreshPluginIdentity } from './identity-sync.js';
73
+ import { refreshUserSurfaceVhosts } from './user-surface.js';
72
74
 
73
75
  const STATE_DIR = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
74
76
 
@@ -311,7 +313,7 @@ export async function hostPlugin(name, agentLabel, { actor, logger, sourceIp })
311
313
  }
312
314
  if (!(await installedVersionApproved(plugin))) {
313
315
  throw httpError(
314
- `Plugin "${name}" brings relay hooks or an agent half that are not approved — review and approve them first`,
316
+ `Plugin "${name}" brings relay hooks, an agent half or an identity block that are not approved — review and approve them first`,
315
317
  409,
316
318
  { code: 'hooks_not_approved' },
317
319
  );
@@ -339,9 +341,17 @@ export async function hostPlugin(name, agentLabel, { actor, logger, sourceIp })
339
341
  logger,
340
342
  run: async (op) => {
341
343
  const result = await runInstallPipeline(op, ctx);
342
- // The agent half starts without settings — or with those it kept on
343
- // this agent from an earlier hosting: the relay's cache follows it.
344
- if (result.status === 'succeeded') await refreshPluginSettingsBestEffort(plugin.name, logger);
344
+ if (result.status === 'succeeded') {
345
+ // The agent half starts without settings — or with those it kept on
346
+ // this agent from an earlier hosting: the relay's cache follows it.
347
+ await refreshPluginSettingsBestEffort(plugin.name, logger);
348
+ // A plugin that declares identity learns its users now: its first
349
+ // sync replays the whole audience.
350
+ await refreshPluginIdentity(plugin.name, logger);
351
+ // Its user surface's locations take the body limit of the internal
352
+ // tunnel the hosting brought.
353
+ await refreshUserSurfaceVhosts(plugin.route, logger);
354
+ }
345
355
  return result;
346
356
  },
347
357
  });
@@ -528,6 +538,9 @@ export async function purgeRetainedData(name, agentLabel, { actor, logger, sourc
528
538
  plugin: name,
529
539
  details: { agentLabel, route: retained.route, package: retained.package },
530
540
  });
541
+ // The last data of an uninstalled plugin: its identity ledger goes too.
542
+ const { dropIdentityLedgerIfNoData } = await import('./audience.js');
543
+ await dropIdentityLedgerIfNoData(name, logger);
531
544
  return { forgotten: retained };
532
545
  }
533
546
  if (!(await isActiveAgent(agentLabel))) {
@@ -613,7 +626,19 @@ export async function acknowledgeAgentPluginConfig(label, route, issuedAt) {
613
626
  // ---------------------------------------------------------------------------
614
627
 
615
628
  /**
616
- * Resolve a route to a forward target (see `agentForwarderPlugin`).
629
+ * Resolve a route to a forward target — of the admin forwarder
630
+ * (`agentForwarderPlugin`) and the user surface alike.
631
+ *
632
+ * While the hosting's health is `down` — the monitor's probes found nothing
633
+ * on the relay-loopback port that answers with the installation secret —
634
+ * this refuses with `down` and the health's reason (`tunnel` or `app`), so
635
+ * nothing of an administrator's, an agent's or a user's request reaches
636
+ * whatever holds that port: the admin forwarder answers as for a dead
637
+ * tunnel (the `tunnel:down` fallback, or `502` with the reason), the user
638
+ * surface `503`. Lamaste's own calls — the health probe, settings, hooks,
639
+ * the identity feed — do not come this way, so the monitor still finds the
640
+ * agent half when it is back and the hosting comes up again.
641
+ *
617
642
  * @param {string} route
618
643
  */
619
644
  export async function resolveForwardTarget(route) {
@@ -623,6 +648,9 @@ export async function resolveForwardTarget(route) {
623
648
  if (plugin.status !== 'enabled') return { kind: 'disabled' };
624
649
  const record = await ownHosting(plugin);
625
650
  if (!record || pluginHostingPhase(record) !== 'active') return { kind: 'not_hosted' };
651
+ if (record.health?.state === 'down') {
652
+ return { kind: 'down', reason: record.health.reason === 'app' ? 'app' : 'tunnel' };
653
+ }
626
654
  const tunnels = await readTunnels();
627
655
  const tunnel = tunnels.find((t) => t.id === record.tunnelId);
628
656
  const maxBodyMb = tunnel?.maxBodySizeMb ?? TUNNEL_MAX_BODY_MB.default;