@camstack/server 1.2.605 → 1.2.607

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 (35) hide show
  1. package/dist/api/auth-whoami.js +0 -1
  2. package/dist/api/core/auth-audit.procedures.js +44 -0
  3. package/dist/api/core/auth-revoke-credentials.procedures.js +4 -78
  4. package/dist/api/core/auth-tokens.procedures.js +105 -72
  5. package/dist/api/core/auth.router.js +14 -280
  6. package/dist/api/core/cap-providers.js +38 -1
  7. package/dist/api/core/integration-secrets.js +22 -1
  8. package/dist/api/core/logs.router.js +219 -49
  9. package/dist/api/core/preferences.router.js +98 -0
  10. package/dist/api/oauth2/oauth2-routes.js +40 -15
  11. package/dist/api/trpc/device-config-secret-redaction.js +2 -2
  12. package/dist/api/trpc/generated-cap-mounts.js +0 -1
  13. package/dist/api/trpc/generated-cap-routers.js +53 -42
  14. package/dist/api/trpc/principal-visibility.js +2 -2
  15. package/dist/api/trpc/share-view-access.js +1 -1
  16. package/dist/api/trpc/trpc-error-principal.js +1 -2
  17. package/dist/api/trpc/trpc.context.js +16 -13
  18. package/dist/api/trpc/trpc.router.js +7 -2
  19. package/dist/api/upload-auth.js +13 -25
  20. package/dist/auth/addon-route-share-gate.js +2 -2
  21. package/dist/auth/credential-verifier.js +63 -104
  22. package/dist/auth/embed-auth-route.js +45 -0
  23. package/dist/auth/live-user.js +19 -0
  24. package/dist/auth/scope-escalation.js +77 -2
  25. package/dist/boot/boot-config.js +8 -1
  26. package/dist/core/addon/addon-registry.service.js +14 -20
  27. package/dist/core/auth/credential-token.service.js +9 -14
  28. package/dist/core/auth/handoff-code.service.js +1 -1
  29. package/dist/core/auth/record-kinds.js +7 -2
  30. package/dist/core/logging/log-history.js +119 -0
  31. package/dist/core/logging/logging.service.js +38 -1
  32. package/dist/main.js +18 -48
  33. package/package.json +14 -14
  34. package/dist/api/core/auth-admin-tokens.procedures.js +0 -156
  35. package/dist/core/auth/share-token.service.js +0 -459
@@ -11,7 +11,9 @@ exports.createLogsRouter = createLogsRouter;
11
11
  */
12
12
  const zod_1 = require("zod");
13
13
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
14
- const types_1 = require("@camstack/types");
14
+ const node_crypto_1 = require("node:crypto");
15
+ const system_1 = require("@camstack/system");
16
+ const log_history_js_1 = require("../../core/logging/log-history.js");
15
17
  const LogLevelSchema = zod_1.z.enum(['debug', 'info', 'warn', 'error']);
16
18
  const LogTagsSchema = zod_1.z.object({
17
19
  agentId: zod_1.z.string().optional(),
@@ -39,6 +41,8 @@ const LogTagsSchema = zod_1.z.object({
39
41
  });
40
42
  /** Wire shape of a log entry — timestamp is serialised to ISO string by tRPC. */
41
43
  const LogEntrySchema = zod_1.z.object({
44
+ /** `<bootId>.<seq>` — identical live and on disk; the client's dedupe key (D898). */
45
+ id: zod_1.z.string().optional(),
42
46
  timestamp: zod_1.z.string(),
43
47
  level: zod_1.z.string(),
44
48
  scope: zod_1.z.string().optional(),
@@ -53,63 +57,214 @@ const LogEntrySchema = zod_1.z.object({
53
57
  meta: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
54
58
  });
55
59
  const LogEntryArraySchema = zod_1.z.array(LogEntrySchema);
56
- function createLogsRouter(logging) {
60
+ /** Wire shape of a history page (D898). */
61
+ const LogPageSchema = zod_1.z.object({
62
+ entries: LogEntryArraySchema,
63
+ /** Pass back as `before` to load the next OLDER page; null = nothing older. */
64
+ nextBefore: zod_1.z.string().nullable(),
65
+ nextAfter: zod_1.z.string().nullable(),
66
+ /** Bytes the source scanned for this slice — the "N MB scanned" indicator. */
67
+ scannedBytes: zod_1.z.number().int().nonnegative(),
68
+ /**
69
+ * True when the source has nothing older. False with fewer than `limit`
70
+ * entries = the slice's time budget ran out first; ask again with
71
+ * `nextBefore` to keep searching.
72
+ */
73
+ done: zod_1.z.boolean(),
74
+ cancelled: zod_1.z.boolean().optional(),
75
+ /** The source that answered — `ring` when the requested one cannot read. */
76
+ source: zod_1.z.string(),
77
+ });
78
+ const LogSourceSchema = zod_1.z.object({
79
+ addonId: zod_1.z.string(),
80
+ readable: zod_1.z.boolean(),
81
+ reason: zod_1.z.string().nullable(),
82
+ isDefault: zod_1.z.boolean(),
83
+ retentionDays: zod_1.z.number().nullable(),
84
+ filters: zod_1.z.object({
85
+ deviceId: zod_1.z.boolean(),
86
+ addonId: zod_1.z.boolean(),
87
+ nodeId: zod_1.z.boolean(),
88
+ tags: zod_1.z.boolean(),
89
+ level: zod_1.z.boolean(),
90
+ text: zod_1.z.boolean(),
91
+ timeRange: zod_1.z.boolean(),
92
+ }),
93
+ });
94
+ /** `tags` (the LogStream scope) → the read contract's indexed fields + the rest. */
95
+ function readFiltersFromTags(tags) {
96
+ if (tags === undefined)
97
+ return {};
98
+ const rest = {};
99
+ let deviceId;
100
+ let addonId;
101
+ let nodeId;
102
+ for (const [key, raw] of Object.entries(tags)) {
103
+ if (raw === undefined)
104
+ continue;
105
+ if (key === 'deviceId') {
106
+ const n = typeof raw === 'number' ? raw : Number(raw);
107
+ if (Number.isInteger(n))
108
+ deviceId = n;
109
+ continue;
110
+ }
111
+ if (key === 'addonId' && typeof raw === 'string') {
112
+ addonId = raw;
113
+ continue;
114
+ }
115
+ if (key === 'agentId' && typeof raw === 'string') {
116
+ nodeId = raw;
117
+ continue;
118
+ }
119
+ if (key === 'containerDeviceId') {
120
+ const n = typeof raw === 'number' ? raw : Number(raw);
121
+ rest[key] = Number.isInteger(n) ? n : String(raw);
122
+ continue;
123
+ }
124
+ rest[key] = raw;
125
+ }
126
+ return {
127
+ ...(deviceId !== undefined ? { deviceId } : {}),
128
+ ...(addonId !== undefined ? { fromAddonId: addonId } : {}),
129
+ ...(nodeId !== undefined ? { fromNodeId: nodeId } : {}),
130
+ ...(Object.keys(rest).length > 0 ? { tags: rest } : {}),
131
+ };
132
+ }
133
+ function toWire(entry) {
134
+ return {
135
+ ...(entry.id !== undefined ? { id: entry.id } : {}),
136
+ timestamp: new Date(entry.timestamp).toISOString(),
137
+ level: entry.level,
138
+ ...(entry.scope !== undefined ? { scope: entry.scope } : {}),
139
+ message: entry.message,
140
+ ...(entry.tags !== undefined
141
+ ? {
142
+ tags: Object.fromEntries(Object.entries(entry.tags).map(([k, v]) => [k, String(v)])),
143
+ }
144
+ : {}),
145
+ ...(entry.meta !== undefined ? { meta: entry.meta } : {}),
146
+ };
147
+ }
148
+ /** Hub-local providers only: an agent's own files hold lines the hub already has. */
149
+ function hubLocalDestinations(registry) {
150
+ if (registry === null)
151
+ return [];
152
+ return registry
153
+ .getCollectionEntries('log-destination')
154
+ .filter(([addonId]) => !addonId.includes('@'));
155
+ }
156
+ const NOT_READABLE = {
157
+ readable: false,
158
+ reason: 'This destination only ships lines',
159
+ retentionDays: null,
160
+ filters: {
161
+ deviceId: false,
162
+ addonId: false,
163
+ nodeId: false,
164
+ tags: false,
165
+ level: false,
166
+ text: false,
167
+ timeRange: false,
168
+ },
169
+ };
170
+ function createLogsRouter(logging, capabilityRegistry = null) {
57
171
  return (0, trpc_middleware_js_1.trpcRouter)({
172
+ /**
173
+ * One page of history (D898): newest first, from the source the client
174
+ * picked (default the hub's rotated files), every filter applied by the
175
+ * source BEFORE the limit. Page older with `before: nextBefore`. A slice
176
+ * is time-bounded — `done: false` with a short page means "still
177
+ * searching", and aborting the request cancels the slice in flight.
178
+ */
58
179
  query: trpc_middleware_js_1.adminProcedure
59
180
  .input(zod_1.z.object({
181
+ /** A read-capable log-destination addonId, or `ring`. Default `winston-logging`. */
182
+ source: zod_1.z.string().min(1).max(200).optional(),
60
183
  level: LogLevelSchema.optional(),
61
184
  since: zod_1.z.number().optional(),
62
185
  until: zod_1.z.number().optional(),
63
- limit: zod_1.z.number().optional(),
186
+ limit: zod_1.z.number().int().min(1).max(1000).optional(),
64
187
  tags: LogTagsSchema.optional(),
188
+ /** Case-insensitive substring over message, scope, tags and meta. */
189
+ text: zod_1.z.string().max(200).optional(),
190
+ before: zod_1.z.string().min(1).max(1024).optional(),
191
+ after: zod_1.z.string().min(1).max(1024).optional(),
192
+ budgetMs: zod_1.z.number().int().min(50).max(10_000).optional(),
65
193
  }))
66
- .output(LogEntryArraySchema)
67
- .query(({ input, ctx }) => {
68
- const filter = { limit: input.limit ?? 100 };
69
- if (input.level)
70
- filter.level = input.level;
71
- if (input.since !== undefined)
72
- filter.since = new Date(input.since);
73
- if (input.until !== undefined)
74
- filter.until = new Date(input.until);
75
- if (input.tags)
76
- filter.tags = input.tags;
77
- const entries = logging.query(filter);
78
- const filtered = (() => {
79
- if (!ctx.user.isAdmin) {
80
- const ap = ctx.user.permissions?.allowedProviders;
81
- if (ap && ap !== '*') {
82
- const allowed = ap;
83
- return entries.filter((e) => {
84
- const addonId = e.tags?.addonId;
85
- if (typeof addonId !== 'string')
86
- return false;
87
- return allowed.some((p) => {
88
- const bare = (0, types_1.bareAddonId)(p);
89
- return addonId === bare || addonId.startsWith(bare);
90
- });
91
- });
92
- }
194
+ .output(LogPageSchema)
195
+ .query(async ({ input, signal }) => {
196
+ const sourceId = input.source ?? log_history_js_1.DEFAULT_LOG_SOURCE;
197
+ const provider = sourceId === log_history_js_1.RING_LOG_SOURCE
198
+ ? null
199
+ : (hubLocalDestinations(capabilityRegistry).find(([id]) => id === sourceId)?.[1] ??
200
+ null);
201
+ const requestId = (0, node_crypto_1.randomUUID)();
202
+ const text = input.text?.trim();
203
+ const query = {
204
+ requestId,
205
+ ...readFiltersFromTags(input.tags),
206
+ ...(input.level !== undefined ? { level: input.level } : {}),
207
+ ...(text !== undefined && text.length > 0 ? { text } : {}),
208
+ ...(input.since !== undefined ? { since: input.since } : {}),
209
+ ...(input.until !== undefined ? { until: input.until } : {}),
210
+ ...(input.before !== undefined ? { before: input.before } : {}),
211
+ ...(input.after !== undefined ? { after: input.after } : {}),
212
+ ...(input.budgetMs !== undefined ? { budgetMs: input.budgetMs } : {}),
213
+ limit: input.limit ?? log_history_js_1.DEFAULT_PAGE_LIMIT,
214
+ };
215
+ const onAbort = () => {
216
+ void provider?.cancelRead?.({ requestId }).catch(() => undefined);
217
+ };
218
+ signal?.addEventListener('abort', onAbort, { once: true });
219
+ try {
220
+ const page = await (0, log_history_js_1.readLogHistory)({
221
+ query,
222
+ source: sourceId,
223
+ tail: logging.query({}),
224
+ provider,
225
+ });
226
+ return {
227
+ entries: page.entries.map(toWire),
228
+ nextBefore: page.nextBefore,
229
+ nextAfter: page.nextAfter,
230
+ scannedBytes: page.scannedBytes,
231
+ done: page.done,
232
+ ...(page.cancelled !== undefined ? { cancelled: page.cancelled } : {}),
233
+ source: page.source,
234
+ };
235
+ }
236
+ finally {
237
+ signal?.removeEventListener('abort', onAbort);
238
+ }
239
+ }),
240
+ /**
241
+ * The history sources the Logs page can offer: every hub-local
242
+ * log-destination with what it can read. A destination without
243
+ * `describeRead` is listed as not readable, never omitted — "ships only"
244
+ * is an answer, "not installed" is a different one.
245
+ */
246
+ sources: trpc_middleware_js_1.adminProcedure.output(zod_1.z.array(LogSourceSchema)).query(async () => {
247
+ const out = [];
248
+ for (const [addonId, dest] of hubLocalDestinations(capabilityRegistry)) {
249
+ const isDefault = addonId === log_history_js_1.DEFAULT_LOG_SOURCE;
250
+ if (dest.describeRead === undefined) {
251
+ out.push({ addonId, isDefault, ...NOT_READABLE });
252
+ continue;
253
+ }
254
+ try {
255
+ const caps = await dest.describeRead();
256
+ out.push({ addonId, isDefault, ...caps });
93
257
  }
94
- return entries;
95
- })();
96
- return filtered.map((e) => ({
97
- timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : String(e.timestamp),
98
- level: e.level,
99
- ...(e.scope !== undefined ? { scope: e.scope } : {}),
100
- message: e.message,
101
- // Tags carry mixed primitive types (numeric deviceId is common —
102
- // see kernel/capability-registry.ts logging calls). The wire
103
- // schema is Record<string,string>, so stringify here. Mirrors
104
- // the same normalisation applied in `subscribe` below.
105
- tags: e.tags
106
- ? Object.fromEntries(Object.entries(e.tags).map(([k, v]) => [
107
- k,
108
- v === undefined ? undefined : String(v),
109
- ]))
110
- : undefined,
111
- ...(e.meta !== undefined ? { meta: e.meta } : {}),
112
- }));
258
+ catch (error) {
259
+ out.push({
260
+ addonId,
261
+ isDefault,
262
+ ...NOT_READABLE,
263
+ reason: error instanceof Error ? error.message : String(error),
264
+ });
265
+ }
266
+ }
267
+ return out;
113
268
  }),
114
269
  /**
115
270
  * Drop matching entries from the in-memory ring buffer. Powers
@@ -149,13 +304,27 @@ function createLogsRouter(logging) {
149
304
  .input(zod_1.z.object({
150
305
  level: LogLevelSchema.optional(),
151
306
  tags: LogTagsSchema.optional(),
307
+ /** Same text filter as `query`, applied before the push (D898). */
308
+ text: zod_1.z.string().max(200).optional(),
152
309
  }))
153
310
  .subscription(({ input }) => {
311
+ const needle = input.text?.trim().toLowerCase() ?? '';
154
312
  return (0, trpc_middleware_js_1.iterableSubscription)((push) => {
155
313
  return logging.subscribe({
156
314
  level: input.level,
157
315
  tags: input.tags,
158
316
  }, (entry) => {
317
+ if (needle.length > 0 &&
318
+ !(0, system_1.lineContainsText)({
319
+ timestamp: 0,
320
+ level: entry.level,
321
+ message: entry.message,
322
+ scope: entry.scope,
323
+ tags: entry.tags,
324
+ meta: entry.meta,
325
+ }, needle)) {
326
+ return;
327
+ }
159
328
  const stringifiedTags = entry.tags
160
329
  ? Object.fromEntries(Object.entries(entry.tags).map(([k, v]) => [
161
330
  k,
@@ -163,6 +332,7 @@ function createLogsRouter(logging) {
163
332
  ]))
164
333
  : undefined;
165
334
  push({
335
+ ...(entry.id !== undefined ? { id: entry.id } : {}),
166
336
  timestamp: entry.timestamp instanceof Date
167
337
  ? entry.timestamp.toISOString()
168
338
  : String(entry.timestamp),
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferencesFromRegistry = preferencesFromRegistry;
4
+ exports.preferencesOwnerOf = preferencesOwnerOf;
5
+ exports.createPreferencesRouter = createPreferencesRouter;
6
+ /**
7
+ * Account preferences — fixed core API (D894), not a capability.
8
+ *
9
+ * get the caller's rows (all, or the namespaces asked)
10
+ * put write one namespace; `baseRevision` turns it into a
11
+ * compare-and-set that answers `conflict` with the current row
12
+ * delete remove one namespace
13
+ * changedSince the cheap poll: only rows the client does not hold yet
14
+ *
15
+ * There is no `userId` input anywhere: every call acts on the PRINCIPAL's own
16
+ * rows, admin included — an admin cannot read another user's preferences
17
+ * through this router, and nothing else serves them. The principal must hold
18
+ * `preferences-self` (every person's session does; a unified token only when
19
+ * minted with it), so a share link, a legacy token, an account link and the
20
+ * hub's own mesh principal are refused.
21
+ *
22
+ * Sync is by polling `changedSince`: there is no per-user push channel to a
23
+ * user's sessions, and the event bus is fleet-wide telemetry (D8).
24
+ *
25
+ * The rows live in `local-auth` (it owns the users, and a user's delete wipes
26
+ * them), reached through the hub-internal half of `user-management`.
27
+ */
28
+ const system_1 = require("@camstack/system");
29
+ const types_1 = require("@camstack/types");
30
+ const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
31
+ const user_error_js_1 = require("../trpc/user-error.js");
32
+ /** The store behind the hub's in-process `user-management` provider. */
33
+ function preferencesFromRegistry(registry) {
34
+ return () => registry === null
35
+ ? null
36
+ : ((0, system_1.getUserManagementInternal)(registry.getSingleton('user-management'))?.preferences ?? null);
37
+ }
38
+ /** The user whose rows this call may touch: the principal itself, or a refusal. */
39
+ function preferencesOwnerOf(principal) {
40
+ if (!(0, types_1.scopesAllowOwnPreferences)(principal.scopes ?? [])) {
41
+ throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'preferences.forbidden', undefined, 'This credential cannot reach account preferences');
42
+ }
43
+ return principal.id;
44
+ }
45
+ function storeOf(deps) {
46
+ const store = deps.preferences();
47
+ if (store === null) {
48
+ throw (0, user_error_js_1.trpcUserError)('PRECONDITION_FAILED', 'preferences.unavailable', undefined, 'Account preferences are not available');
49
+ }
50
+ return store;
51
+ }
52
+ function limitRefusal(error) {
53
+ const reason = (0, system_1.preferencesLimitReasonOf)(error);
54
+ if (reason === 'value-too-large') {
55
+ throw (0, user_error_js_1.trpcUserError)('PAYLOAD_TOO_LARGE', 'preferences.value-too-large', undefined, 'This preference value is too large');
56
+ }
57
+ if (reason === 'too-many-namespaces') {
58
+ throw (0, user_error_js_1.trpcUserError)('BAD_REQUEST', 'preferences.too-many-namespaces', undefined, 'Too many preference namespaces for this account');
59
+ }
60
+ throw error;
61
+ }
62
+ function createPreferencesRouter(deps) {
63
+ return (0, trpc_middleware_js_1.trpcRouter)({
64
+ get: trpc_middleware_js_1.protectedProcedure
65
+ .input(types_1.PreferencesGetInputSchema)
66
+ .output(types_1.PreferencesGetOutputSchema)
67
+ .query(async ({ ctx, input }) => {
68
+ const owner = preferencesOwnerOf(ctx.user);
69
+ return { rows: [...(await storeOf(deps).get(owner, input.namespaces))] };
70
+ }),
71
+ put: trpc_middleware_js_1.protectedProcedure
72
+ .input(types_1.PreferencesPutInputSchema)
73
+ .output(types_1.PreferencesPutOutputSchema)
74
+ .mutation(async ({ ctx, input }) => {
75
+ const owner = preferencesOwnerOf(ctx.user);
76
+ try {
77
+ return await storeOf(deps).put(owner, input);
78
+ }
79
+ catch (error) {
80
+ return limitRefusal(error);
81
+ }
82
+ }),
83
+ delete: trpc_middleware_js_1.protectedProcedure
84
+ .input(types_1.PreferencesDeleteInputSchema)
85
+ .output(types_1.PreferencesDeleteOutputSchema)
86
+ .mutation(async ({ ctx, input }) => {
87
+ const owner = preferencesOwnerOf(ctx.user);
88
+ return { deleted: await storeOf(deps).delete(owner, input.namespace) };
89
+ }),
90
+ changedSince: trpc_middleware_js_1.protectedProcedure
91
+ .input(types_1.PreferencesChangedSinceInputSchema)
92
+ .output(types_1.PreferencesChangedSinceOutputSchema)
93
+ .query(async ({ ctx, input }) => {
94
+ const owner = preferencesOwnerOf(ctx.user);
95
+ return storeOf(deps).changedSince(owner, input.revisionByNamespace);
96
+ }),
97
+ });
98
+ }
@@ -269,6 +269,35 @@ function answerRefusal(request, reply, status, body, reason, integration) {
269
269
  }
270
270
  return reply.status(status).send(body);
271
271
  }
272
+ /**
273
+ * Who consents. ONLY a person's login session: a session-grade JWT (no
274
+ * `kind` — not a bridge, a challenge or a minted token) naming a user that
275
+ * still exists. Anything else this hub signed — a share link, a script's
276
+ * token, an account link — is refused, because a consent mints an account
277
+ * link with the integration's whole grant (security review 2026-10-21: a
278
+ * share link set as the cookie reached `consent=allow` and came back with a
279
+ * fleet-wide device grant for `userId: ''`).
280
+ */
281
+ async function consentingUserOf(deps, cookie) {
282
+ let payload;
283
+ try {
284
+ payload = deps.verifyToken(cookie);
285
+ }
286
+ catch {
287
+ return { ok: false, reason: 'session-invalid' };
288
+ }
289
+ if (typeof payload !== 'object' || payload === null || !(0, session_cookie_js_1.isSessionGradeJwtPayload)(payload)) {
290
+ return { ok: false, reason: 'not-a-login-session' };
291
+ }
292
+ const userId = Reflect.get(payload, 'userId');
293
+ const username = Reflect.get(payload, 'username');
294
+ if (typeof userId !== 'string' || userId === '') {
295
+ return { ok: false, reason: 'not-a-login-session' };
296
+ }
297
+ if (!(await deps.isLiveUser(userId)))
298
+ return { ok: false, reason: 'user-gone' };
299
+ return { ok: true, user: { userId, username: typeof username === 'string' ? username : '' } };
300
+ }
272
301
  /**
273
302
  * An authorize call turned away for want of a usable session.
274
303
  *
@@ -330,13 +359,11 @@ function registerOauth2Routes(fastify, deps) {
330
359
  if (!cookie) {
331
360
  return refuseUnauthenticated(request, reply, deps.logger, call, 'no-session-cookie');
332
361
  }
333
- let tokenInfo;
334
- try {
335
- tokenInfo = deps.verifyToken(cookie);
336
- }
337
- catch {
338
- return refuseUnauthenticated(request, reply, deps.logger, call, 'session-invalid');
362
+ const consenting = await consentingUserOf(deps, cookie);
363
+ if (!consenting.ok) {
364
+ return refuseUnauthenticated(request, reply, deps.logger, call, consenting.reason);
339
365
  }
366
+ const tokenInfo = consenting.user;
340
367
  const registry = deps.getRegistry();
341
368
  if (!registry) {
342
369
  return answerRefusal(request, reply, 503, { error: 'service_unavailable' }, 'unavailable');
@@ -367,7 +394,7 @@ function registerOauth2Routes(fastify, deps) {
367
394
  acceptLanguage: request.headers['accept-language'],
368
395
  }),
369
396
  displayName: descriptor.displayName,
370
- username: tokenInfo.username ?? '',
397
+ username: tokenInfo.username,
371
398
  scopeSummary: summariseScopes(descriptor.requestedScopes),
372
399
  // Shown, not merely validated. A `redirect_uri` on a private host is
373
400
  // admitted by pattern rather than by registration, so the operator is
@@ -428,13 +455,11 @@ function registerOauth2Routes(fastify, deps) {
428
455
  if (!cookie) {
429
456
  return refuseUnauthenticated(request, reply, deps.logger, call, 'no-session-cookie');
430
457
  }
431
- let tokenInfo;
432
- try {
433
- tokenInfo = deps.verifyToken(cookie);
434
- }
435
- catch {
436
- return refuseUnauthenticated(request, reply, deps.logger, call, 'session-invalid');
458
+ const consenting = await consentingUserOf(deps, cookie);
459
+ if (!consenting.ok) {
460
+ return refuseUnauthenticated(request, reply, deps.logger, call, consenting.reason);
437
461
  }
462
+ const tokenInfo = consenting.user;
438
463
  const registry = deps.getRegistry();
439
464
  if (!registry) {
440
465
  return answerRefusal(request, reply, 503, { error: 'service_unavailable' }, 'unavailable');
@@ -478,8 +503,8 @@ function registerOauth2Routes(fastify, deps) {
478
503
  }
479
504
  const { code } = await userMgmt.oauthIssueCode({
480
505
  integrationId: v.integration,
481
- userId: tokenInfo.userId ?? '',
482
- username: tokenInfo.username ?? '',
506
+ userId: tokenInfo.userId,
507
+ username: tokenInfo.username,
483
508
  scopes: descriptor.requestedScopes,
484
509
  redirectUri: v.redirectUri,
485
510
  // Prefer the integration's own public origin (e.g. the operator-selected
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Those methods are declared `auth: 'protected'` (the default in
12
12
  * `capability-definition.ts`), which is NOT admin — it is "authenticated, and
13
- * scope-matched". Measured against the live hub: a `cst_*` scoped token
13
+ * scope-matched". Measured against the live hub: a scoped token
14
14
  * carrying nothing but `category:system` + `view` — a READ-ONLY grant, the one
15
15
  * the deploy runbook mints for CI — read `deviceManager.getDevice` and received
16
16
  * `config.password` verbatim, and `deviceManager.listAll` answered with a
@@ -31,7 +31,7 @@
31
31
  * write-only for admins too is a bigger, UI-side change; it is not needed to
32
32
  * close this hole and would regress the form.
33
33
  *
34
- * Share-view (`csv_*`) principals never reach this code — they are gated
34
+ * Share-view principals never reach this code — they are gated
35
35
  * earlier by the fail-closed allowlist in `share-view-access.ts`, which does
36
36
  * not expose `getDevice` at all and projects `listAll` down to seven presentation
37
37
  * fields. This module is the gate for the tier BETWEEN a share link and an admin.
@@ -314,7 +314,6 @@ function mountAllCaps(services) {
314
314
  ...first,
315
315
  listProfileKinds: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listProfileKinds'),
316
316
  listProfiles: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listProfiles'),
317
- listModels: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listModels'),
318
317
  getDefaults: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'getDefaults'),
319
318
  getUsage: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'getUsage'),
320
319
  listModelCatalog: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listModelCatalog'),
@@ -2959,6 +2959,16 @@ function createCapRouter_deviceHistory(getProvider, createRemoteProxy) {
2959
2959
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2960
2960
  return p.queryNode(methodInput);
2961
2961
  }),
2962
+ queryMany: trpc_middleware_js_1.adminProcedure
2963
+ .input(types_50.deviceHistoryCapability.methods.queryMany.input.loose())
2964
+ .output(types_50.deviceHistoryCapability.methods.queryMany.output)
2965
+ .query(async ({ input, ctx }) => {
2966
+ const parsedInput = ctx.req !== undefined ? (0, types_3.withoutCallerStamp)(input) : input;
2967
+ const { nodeId, ...methodInput } = parsedInput;
2968
+ const p = resolveProvider('device-history', nodeId, () => getProvider(ctx), createRemoteProxy);
2969
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2970
+ return p.queryMany(methodInput);
2971
+ }),
2962
2972
  declareSeries: trpc_middleware_js_1.adminProcedure
2963
2973
  .input(types_50.deviceHistoryCapability.methods.declareSeries.input.loose())
2964
2974
  .output(types_50.deviceHistoryCapability.methods.declareSeries.output)
@@ -5300,10 +5310,10 @@ function createCapRouter_llm(getProvider, _createRemoteProxy) {
5300
5310
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5301
5311
  return p.testProfile(input);
5302
5312
  }),
5303
- listModels: trpc_middleware_js_1.protectedProcedure
5313
+ listModels: trpc_middleware_js_1.adminProcedure
5304
5314
  .input(types_69.llmCapability.methods.listModels.input.loose())
5305
5315
  .output(types_69.llmCapability.methods.listModels.output)
5306
- .query(async ({ input, ctx }) => {
5316
+ .mutation(async ({ input, ctx }) => {
5307
5317
  const p = requireCapProvider('llm', () => getProvider(ctx));
5308
5318
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5309
5319
  return p.listModels(input);
@@ -5760,14 +5770,51 @@ function createCapRouter_logDestination(getProvider, createRemoteProxy) {
5760
5770
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5761
5771
  return p.write(methodInput);
5762
5772
  }),
5763
- query: trpc_middleware_js_1.adminProcedure
5764
- .input(types_75.logDestinationCapability.methods.query.input.loose())
5765
- .output(types_75.logDestinationCapability.methods.query.output)
5773
+ describeRead: trpc_middleware_js_1.adminProcedure
5774
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().optional(), addonId: zod_1.z.string().optional() }).optional())
5775
+ .output(types_75.logDestinationCapability.methods.describeRead.output)
5776
+ .query(async ({ input, ctx }) => {
5777
+ const p = resolveProvider('log-destination', input?.nodeId, () => getProvider(ctx, input?.addonId), createRemoteProxy);
5778
+ const fn = p.describeRead;
5779
+ if (typeof fn !== 'function') {
5780
+ throw new server_1.TRPCError({
5781
+ code: 'NOT_IMPLEMENTED',
5782
+ message: 'Capability "log-destination" provider does not implement "describeRead"',
5783
+ });
5784
+ }
5785
+ return fn.call(p);
5786
+ }),
5787
+ read: trpc_middleware_js_1.adminProcedure
5788
+ .input(types_75.logDestinationCapability.methods.read.input.loose())
5789
+ .output(types_75.logDestinationCapability.methods.read.output)
5766
5790
  .query(async ({ input, ctx }) => {
5767
5791
  const { nodeId, addonId, ...methodInput } = input;
5768
5792
  const p = resolveProvider('log-destination', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
5793
+ const fn = p.read;
5794
+ if (typeof fn !== 'function') {
5795
+ throw new server_1.TRPCError({
5796
+ code: 'NOT_IMPLEMENTED',
5797
+ message: 'Capability "log-destination" provider does not implement "read"',
5798
+ });
5799
+ }
5769
5800
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5770
- return p.query(methodInput);
5801
+ return fn.call(p, methodInput);
5802
+ }),
5803
+ cancelRead: trpc_middleware_js_1.adminProcedure
5804
+ .input(types_75.logDestinationCapability.methods.cancelRead.input.loose())
5805
+ .output(types_75.logDestinationCapability.methods.cancelRead.output)
5806
+ .mutation(async ({ input, ctx }) => {
5807
+ const { nodeId, addonId, ...methodInput } = input;
5808
+ const p = resolveProvider('log-destination', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
5809
+ const fn = p.cancelRead;
5810
+ if (typeof fn !== 'function') {
5811
+ throw new server_1.TRPCError({
5812
+ code: 'NOT_IMPLEMENTED',
5813
+ message: 'Capability "log-destination" provider does not implement "cancelRead"',
5814
+ });
5815
+ }
5816
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5817
+ return fn.call(p, methodInput);
5771
5818
  }),
5772
5819
  });
5773
5820
  }
@@ -12446,42 +12493,6 @@ function createCapRouter_userManagement(getProvider, createRemoteProxy) {
12446
12493
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
12447
12494
  return p.revokeApiKey(methodInput);
12448
12495
  }),
12449
- createScopedToken: trpc_middleware_js_1.adminProcedure
12450
- .input(types_142.userManagementCapability.methods.createScopedToken.input.loose())
12451
- .output(types_142.userManagementCapability.methods.createScopedToken.output)
12452
- .mutation(async ({ input, ctx }) => {
12453
- const { nodeId, ...methodInput } = input;
12454
- const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
12455
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
12456
- return p.createScopedToken(methodInput);
12457
- }),
12458
- revokeScopedToken: trpc_middleware_js_1.adminProcedure
12459
- .input(types_142.userManagementCapability.methods.revokeScopedToken.input.loose())
12460
- .output(types_142.userManagementCapability.methods.revokeScopedToken.output)
12461
- .mutation(async ({ input, ctx }) => {
12462
- const { nodeId, ...methodInput } = input;
12463
- const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
12464
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
12465
- return p.revokeScopedToken(methodInput);
12466
- }),
12467
- validateScopedToken: trpc_middleware_js_1.protectedProcedure
12468
- .input(types_142.userManagementCapability.methods.validateScopedToken.input.loose())
12469
- .output(types_142.userManagementCapability.methods.validateScopedToken.output)
12470
- .query(async ({ input, ctx }) => {
12471
- const { nodeId, ...methodInput } = input;
12472
- const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
12473
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
12474
- return p.validateScopedToken(methodInput);
12475
- }),
12476
- listScopedTokens: trpc_middleware_js_1.adminProcedure
12477
- .input(types_142.userManagementCapability.methods.listScopedTokens.input.loose())
12478
- .output(types_142.userManagementCapability.methods.listScopedTokens.output)
12479
- .query(async ({ input, ctx }) => {
12480
- const { nodeId, ...methodInput } = input;
12481
- const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
12482
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
12483
- return p.listScopedTokens(methodInput);
12484
- }),
12485
12496
  setupTotp: trpc_middleware_js_1.adminProcedure
12486
12497
  .input(types_142.userManagementCapability.methods.setupTotp.input.loose())
12487
12498
  .output(types_142.userManagementCapability.methods.setupTotp.output)