@lamalibre/create-lamaste 4.4.0 → 4.5.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 (150) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +200 -16
  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/03-architecture/installer-flow.md +2 -2
  28. package/vendor/docs/03-architecture/installer.md +4 -4
  29. package/vendor/docs/03-architecture/management-flow.md +3 -1
  30. package/vendor/docs/03-architecture/nginx-configuration.md +165 -18
  31. package/vendor/docs/03-architecture/overview.md +13 -17
  32. package/vendor/docs/03-architecture/panel-client.md +6 -6
  33. package/vendor/docs/03-architecture/panel-server.md +3 -2
  34. package/vendor/docs/03-architecture/state-management.md +4 -8
  35. package/vendor/docs/03-architecture/system-overview.md +1 -1
  36. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +292 -23
  37. package/vendor/docs/04-api-reference/app-host.md +3 -3
  38. package/vendor/docs/04-api-reference/gatekeeper.md +60 -5
  39. package/vendor/docs/04-api-reference/lifecycle-hooks.md +15 -13
  40. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  41. package/vendor/docs/04-api-reference/registry-and-oidc.md +6 -4
  42. package/vendor/docs/04-api-reference/tunnels.md +67 -29
  43. package/vendor/docs/04-api-reference/users.md +241 -45
  44. package/vendor/docs/05-operations/backup-and-restore.md +35 -26
  45. package/vendor/docs/05-operations/monitoring.md +39 -0
  46. package/vendor/docs/05-operations/uninstalling.md +2 -1
  47. package/vendor/docs/05-operations/upgrades.md +15 -6
  48. package/vendor/docs/06-reference/config-files.md +83 -189
  49. package/vendor/docs/06-reference/glossary.md +3 -3
  50. package/vendor/docs/06-reference/installer-flags.md +13 -13
  51. package/vendor/docs/06-reference/ports-and-services.md +25 -26
  52. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  53. package/vendor/docs/gatekeeper-design.md +10 -8
  54. package/vendor/docs/identity-directory-design.md +48 -14
  55. package/vendor/docs/package.json +1 -1
  56. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  57. package/vendor/gatekeeper/dist/index.d.ts +2 -1
  58. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  59. package/vendor/gatekeeper/dist/index.js +2 -0
  60. package/vendor/gatekeeper/dist/index.js.map +1 -1
  61. package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
  62. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  63. package/vendor/gatekeeper/dist/lib/grants.js +6 -1
  64. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  65. package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
  66. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/lib/groups.js +5 -1
  68. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
  69. package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
  70. package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
  71. package/vendor/gatekeeper/dist/lib/principals.js +67 -0
  72. package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
  73. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +13 -0
  74. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
  75. package/vendor/gatekeeper/dist/lib/tunnels.js +68 -0
  76. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
  77. package/vendor/gatekeeper/dist/lib/types.d.ts +19 -0
  78. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  79. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  80. package/vendor/gatekeeper/dist/server/index.js +22 -48
  81. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  82. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
  83. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  84. package/vendor/gatekeeper/dist/server/routes/authz.js +250 -103
  85. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  86. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  87. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
  88. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  89. package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
  90. package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
  91. package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
  92. package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
  93. package/vendor/gatekeeper/package.json +2 -2
  94. package/vendor/server/bin/lamaste-server.js +15 -1
  95. package/vendor/server/package.json +2 -2
  96. package/vendor/server/src/commands/plugin-identity.js +335 -0
  97. package/vendor/server/src/commands/plugins.js +99 -11
  98. package/vendor/server/src/commands/registry-credential.js +2 -1
  99. package/vendor/server/src/commands/tunnels.js +102 -2
  100. package/vendor/server/src/commands/uninstall.js +3 -1
  101. package/vendor/server/src/commands/users.js +358 -0
  102. package/vendor/server/src/plugin-approval.js +107 -5
  103. package/vendor/server/src/plugin-identity.js +382 -0
  104. package/vendor/server-ui/dist/assets/index-D-fer5ef.css +1 -0
  105. package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +84 -0
  106. package/vendor/server-ui/dist/index.html +2 -2
  107. package/vendor/serverd/package.json +2 -2
  108. package/vendor/serverd/src/index.js +25 -1
  109. package/vendor/serverd/src/lib/audience.js +576 -0
  110. package/vendor/serverd/src/lib/authelia.js +78 -7
  111. package/vendor/serverd/src/lib/config.js +68 -43
  112. package/vendor/serverd/src/lib/directory.js +1113 -0
  113. package/vendor/serverd/src/lib/entitlements.js +517 -0
  114. package/vendor/serverd/src/lib/gatekeeper-client.js +215 -10
  115. package/vendor/serverd/src/lib/hook-pipelines.js +13 -0
  116. package/vendor/serverd/src/lib/identity-sync.js +905 -0
  117. package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
  118. package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
  119. package/vendor/serverd/src/lib/mtls.js +6 -2
  120. package/vendor/serverd/src/lib/nginx.js +279 -48
  121. package/vendor/serverd/src/lib/plugin-hooks.js +98 -45
  122. package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
  123. package/vendor/serverd/src/lib/plugin-lifecycle.js +100 -9
  124. package/vendor/serverd/src/lib/plugin-packages.js +30 -22
  125. package/vendor/serverd/src/lib/plugins.js +94 -15
  126. package/vendor/serverd/src/lib/registry-credential.js +46 -6
  127. package/vendor/serverd/src/lib/request-logging.js +19 -0
  128. package/vendor/serverd/src/lib/retained-data.js +50 -4
  129. package/vendor/serverd/src/lib/state.js +18 -0
  130. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  131. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  132. package/vendor/serverd/src/lib/user-caller.js +67 -0
  133. package/vendor/serverd/src/lib/user-surface.js +733 -0
  134. package/vendor/serverd/src/lib/vhost-reconcile.js +136 -34
  135. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  136. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  137. package/vendor/serverd/src/routes/invite.js +53 -12
  138. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  139. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  140. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  141. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  142. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  143. package/vendor/serverd/src/routes/management/tunnels.js +98 -2
  144. package/vendor/serverd/src/routes/management/users.js +290 -208
  145. package/vendor/serverd/src/routes/management.js +4 -0
  146. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  147. package/vendor/serverd/src/routes/user-access.js +17 -4
  148. package/vendor/serverd/src/routes/user-surface.js +69 -0
  149. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  150. package/vendor/server-ui/dist/assets/index-CESyyK8V.js +0 -84
@@ -2,12 +2,14 @@
2
2
  * Tiny gatekeeper admin-API client for callers other than the
3
3
  * /api/gatekeeper/* proxy routes.
4
4
  *
5
- * Reads the shared secret on first use (cached for the process lifetime)
6
- * and forwards a request to http://127.0.0.1:9294/api/...
5
+ * Reads the shared secret on first use (cached for the process lifetime
6
+ * once found) and forwards a request to http://127.0.0.1:9294/api/...
7
7
  *
8
8
  * Used by the user-delete route to cascade-revoke grants belonging to a
9
- * deleted Authelia user, and by other internal callers that need to talk
10
- * to gatekeeper without going through the panel's own mTLS layer.
9
+ * deleted Authelia user, by the plugin audiences (grants and groups), the
10
+ * entitlements and the erasure of a removed user, and by other internal
11
+ * callers that need to talk to gatekeeper without going through the
12
+ * panel's own mTLS layer.
11
13
  */
12
14
 
13
15
  import { readFile } from 'node:fs/promises';
@@ -16,17 +18,31 @@ import { getConfigPath } from './config.js';
16
18
 
17
19
  const GATEKEEPER_BASE = 'http://127.0.0.1:9294';
18
20
  let cachedSecret = null;
19
- let secretLoaded = false;
21
+
22
+ /** Replaces the HTTP exchange with Gatekeeper (tests: there is no Gatekeeper). */
23
+ let transportOverride = null;
24
+
25
+ /**
26
+ * Answer every Gatekeeper request with `transport(method, apiPath, body)`
27
+ * — a {@link GatekeeperResult} — instead of calling Gatekeeper (tests);
28
+ * `null` calls Gatekeeper again.
29
+ *
30
+ * @param {((method: string, apiPath: string, body?: unknown) => Promise<GatekeeperResult>) | null} transport
31
+ */
32
+ export function setGatekeeperClientForTests(transport) {
33
+ transportOverride = transport;
34
+ }
20
35
 
21
36
  /**
22
37
  * Load the gatekeeper API secret. Supports both the new JSON envelope
23
38
  * (`gatekeeper-secret.json`) written by the gatekeeper service and the
24
39
  * legacy plain-text file (`gatekeeper-secret`). Returns null if neither
25
- * exists — callers must treat this as "gatekeeper not installed".
40
+ * exists — callers must treat this as "gatekeeper not installed (yet)".
41
+ * Only a secret that was found is kept: Gatekeeper writes it when it first
42
+ * starts, which may be after the panel's first call.
26
43
  */
27
44
  async function loadSecret() {
28
- if (secretLoaded) return cachedSecret;
29
- secretLoaded = true;
45
+ if (cachedSecret) return cachedSecret;
30
46
 
31
47
  const configDir = path.dirname(getConfigPath());
32
48
 
@@ -53,8 +69,7 @@ async function loadSecret() {
53
69
  // No secret available
54
70
  }
55
71
 
56
- cachedSecret = null;
57
- return cachedSecret;
72
+ return null;
58
73
  }
59
74
 
60
75
  /**
@@ -78,6 +93,8 @@ async function loadSecret() {
78
93
  * @returns {Promise<GatekeeperResult>}
79
94
  */
80
95
  export async function gatekeeperRequest(method, apiPath, body) {
96
+ if (transportOverride) return transportOverride(method, apiPath, body);
97
+
81
98
  const secret = await loadSecret();
82
99
  if (!secret) {
83
100
  return {
@@ -133,3 +150,191 @@ export async function gatekeeperRequest(method, apiPath, body) {
133
150
 
134
151
  return { ok: true, statusCode: response.status, data };
135
152
  }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Typed calls
156
+ // ---------------------------------------------------------------------------
157
+
158
+ /**
159
+ * A grant as Gatekeeper lists it.
160
+ *
161
+ * @typedef {object} GatekeeperGrant
162
+ * @property {string} grantId
163
+ * @property {'user' | 'group'} principalType
164
+ * @property {string} principalId
165
+ * @property {string} resourceType
166
+ * @property {string} resourceId
167
+ * @property {Record<string, unknown>} context
168
+ * @property {boolean} used
169
+ * @property {string} createdAt
170
+ */
171
+
172
+ /**
173
+ * A Lamaste group as Gatekeeper lists it.
174
+ *
175
+ * @typedef {object} GatekeeperGroup
176
+ * @property {string} name
177
+ * @property {string} description
178
+ * @property {string[]} members usernames
179
+ */
180
+
181
+ function isRecord(value) {
182
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
183
+ }
184
+
185
+ /** A failed call as a result, for an answer that is not the shape Gatekeeper gives. */
186
+ function malformed(what) {
187
+ return {
188
+ ok: false,
189
+ statusCode: 502,
190
+ error: `gatekeeper answered ${what} in an unexpected shape`,
191
+ reason: 'http-error',
192
+ };
193
+ }
194
+
195
+ function grantOf(raw) {
196
+ if (
197
+ !isRecord(raw) ||
198
+ typeof raw.grantId !== 'string' ||
199
+ (raw.principalType !== 'user' && raw.principalType !== 'group') ||
200
+ typeof raw.principalId !== 'string' ||
201
+ typeof raw.resourceType !== 'string' ||
202
+ typeof raw.resourceId !== 'string'
203
+ ) {
204
+ return null;
205
+ }
206
+ return {
207
+ grantId: raw.grantId,
208
+ principalType: raw.principalType,
209
+ principalId: raw.principalId,
210
+ resourceType: raw.resourceType,
211
+ resourceId: raw.resourceId,
212
+ context: isRecord(raw.context) ? raw.context : {},
213
+ used: raw.used === true,
214
+ createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : '',
215
+ };
216
+ }
217
+
218
+ function groupOf(raw) {
219
+ if (!isRecord(raw) || typeof raw.name !== 'string' || !Array.isArray(raw.members)) return null;
220
+ return {
221
+ name: raw.name,
222
+ description: typeof raw.description === 'string' ? raw.description : '',
223
+ members: raw.members.filter((m) => typeof m === 'string'),
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Every grant on plugin `route` (`resourceType: 'plugin'`, `resourceId:
229
+ * <route>`) — with roles or without.
230
+ *
231
+ * @param {string} route
232
+ * @returns {Promise<{ ok: true, grants: GatekeeperGrant[] } | Extract<GatekeeperResult, { ok: false }>>}
233
+ */
234
+ export async function listPluginGrants(route) {
235
+ const qs = new URLSearchParams({ resourceType: 'plugin', resourceId: route }).toString();
236
+ const result = await gatekeeperRequest('GET', `/api/grants?${qs}`);
237
+ if (!result.ok) return result;
238
+ if (!isRecord(result.data) || !Array.isArray(result.data.grants)) return malformed('its grants');
239
+ const grants = result.data.grants.map(grantOf);
240
+ if (grants.some((g) => g === null)) return malformed('its grants');
241
+ // Gatekeeper filters by the query; a grant on anything else is not this plugin's.
242
+ return {
243
+ ok: true,
244
+ grants: grants.filter((g) => g.resourceType === 'plugin' && g.resourceId === route),
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Every Lamaste group with its members.
250
+ *
251
+ * @returns {Promise<{ ok: true, groups: GatekeeperGroup[] } | Extract<GatekeeperResult, { ok: false }>>}
252
+ */
253
+ export async function listGatekeeperGroups() {
254
+ const result = await gatekeeperRequest('GET', '/api/groups');
255
+ if (!result.ok) return result;
256
+ if (!isRecord(result.data) || !Array.isArray(result.data.groups)) return malformed('its groups');
257
+ const groups = result.data.groups.map(groupOf);
258
+ if (groups.some((g) => g === null)) return malformed('its groups');
259
+ return { ok: true, groups };
260
+ }
261
+
262
+ /**
263
+ * Create a grant. Gatekeeper refuses a duplicate (409: same principal,
264
+ * resource and context JSON) and a grant beyond its limit (503).
265
+ *
266
+ * @param {{ principalType: 'user' | 'group', principalId: string, resourceType: string, resourceId: string, context: Record<string, unknown> }} grant
267
+ * @returns {Promise<{ ok: true, grant: GatekeeperGrant } | Extract<GatekeeperResult, { ok: false }>>}
268
+ */
269
+ export async function createGatekeeperGrant(grant) {
270
+ const result = await gatekeeperRequest('POST', '/api/grants', grant);
271
+ if (!result.ok) return result;
272
+ const created = isRecord(result.data) ? grantOf(result.data.grant) : null;
273
+ return created ? { ok: true, grant: created } : malformed('the grant it created');
274
+ }
275
+
276
+ /**
277
+ * Revoke one grant.
278
+ *
279
+ * @param {string} grantId
280
+ * @returns {Promise<GatekeeperResult>}
281
+ */
282
+ export function revokeGatekeeperGrant(grantId) {
283
+ return gatekeeperRequest('DELETE', `/api/grants/${encodeURIComponent(grantId)}`);
284
+ }
285
+
286
+ /**
287
+ * Remove everything Gatekeeper holds under `username` — every grant naming
288
+ * the user (any resource type) and its membership of every group — in one
289
+ * transaction. Idempotent. The username goes in the body (a URL is what
290
+ * request logs keep).
291
+ *
292
+ * @param {string} username
293
+ * @returns {Promise<{ ok: true, grantsRemoved: number, groupsLeft: number } | Extract<GatekeeperResult, { ok: false }>>}
294
+ */
295
+ export async function removeUserPrincipal(username) {
296
+ const result = await gatekeeperRequest('POST', '/api/principals/user/remove', { username });
297
+ if (!result.ok) return result;
298
+ const data = isRecord(result.data) ? result.data : {};
299
+ if (
300
+ !Number.isSafeInteger(data.grantsRemoved) ||
301
+ data.grantsRemoved < 0 ||
302
+ !Number.isSafeInteger(data.groupsLeft) ||
303
+ data.groupsLeft < 0
304
+ ) {
305
+ return malformed('the principal removal');
306
+ }
307
+ return { ok: true, grantsRemoved: data.grantsRemoved, groupsLeft: data.groupsLeft };
308
+ }
309
+
310
+ /**
311
+ * Drop Gatekeeper's cached session verdicts. Authelia ends every session
312
+ * when users.yml changes; without this a cached verdict would keep a
313
+ * disabled or deleted user in for up to the cache lifetime (30 s).
314
+ *
315
+ * @returns {Promise<{ ok: true } | Extract<GatekeeperResult, { ok: false }>>}
316
+ */
317
+ export async function bustGatekeeperSessions() {
318
+ const result = await gatekeeperRequest('POST', '/api/cache/bust');
319
+ return result.ok ? { ok: true } : result;
320
+ }
321
+
322
+ /**
323
+ * Delete the access-request log rows of `username` (logged before
324
+ * `before`, an ISO 8601 time, when given) — part of erasing a removed user.
325
+ * The username goes in the body (a URL is what request logs keep).
326
+ *
327
+ * @param {string} username
328
+ * @param {{ before?: string }} [opts]
329
+ * @returns {Promise<{ ok: true, removed: number } | Extract<GatekeeperResult, { ok: false }>>}
330
+ */
331
+ export async function purgeAccessLogForUser(username, { before } = {}) {
332
+ const result = await gatekeeperRequest('POST', '/api/access-log/purge', {
333
+ username,
334
+ ...(before ? { before } : {}),
335
+ });
336
+ if (!result.ok) return result;
337
+ const removed = isRecord(result.data) ? result.data.removed : undefined;
338
+ if (!Number.isSafeInteger(removed) || removed < 0) return malformed('the purge');
339
+ return { ok: true, removed };
340
+ }
@@ -83,6 +83,8 @@ import { forgetRetainedData, recordRetainedData } from './retained-data.js';
83
83
  import { runRelayHook } from './relay-hooks.js';
84
84
  import { recordAuditBestEffort } from './audit.js';
85
85
  import { deleteSettingsCache, relayHookSettings } from './plugin-settings.js';
86
+ import { forgetIdentityHalf } from './identity-sync.js';
87
+ import { refreshUserSurfaceVhosts } from './user-surface.js';
86
88
  import {
87
89
  tunnelNginxDeps,
88
90
  tunnelCertbotDeps,
@@ -620,6 +622,9 @@ export async function runPurgeRetainedData(op, { retained, logger }) {
620
622
  });
621
623
  assertRemoved(result, 'Purging the retained data');
622
624
  await forgetRetainedData(retained.agentLabel, retained.route);
625
+ // The last data of an uninstalled plugin: its identity ledger goes too.
626
+ const { dropIdentityLedgerIfNoData } = await import('./audience.js');
627
+ await dropIdentityLedgerIfNoData(retained.plugin, logger);
623
628
  });
624
629
  await recordAuditBestEffort(
625
630
  {
@@ -836,6 +841,14 @@ export async function runUninstallPipeline(op, ctx, { force = false } = {}) {
836
841
  await deleteSettingsCache(ctx.plugin.name).catch((err) =>
837
842
  op.warn(`Could not drop the cached settings: ${err.message}`),
838
843
  );
844
+ // So does what the relay knew of the half's identity feed: the next
845
+ // hosting's half answers protocol 1 before users reach it, and replays.
846
+ await forgetIdentityHalf(ctx.plugin.name).catch((err) =>
847
+ op.warn(`Could not forget the agent half's identity feed: ${err.message}`),
848
+ );
849
+ // Its user surface's locations no longer take the internal tunnel's
850
+ // body limit (best effort; the panel answers 503 until it is hosted).
851
+ await refreshUserSurfaceVhosts(ctx.route, ctx.logger);
839
852
  }
840
853
  await recordAuditBestEffort(
841
854
  {