@camstack/server 1.2.604 → 1.2.606

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 (38) hide show
  1. package/dist/api/auth-whoami.js +27 -43
  2. package/dist/api/core/auth-audit.procedures.js +44 -0
  3. package/dist/api/core/auth-device-cert.procedures.js +1 -1
  4. package/dist/api/core/auth-reauth.procedures.js +1 -1
  5. package/dist/api/core/auth-revoke-credentials.procedures.js +25 -52
  6. package/dist/api/core/auth-sessions.procedures.js +1 -1
  7. package/dist/api/core/auth-tokens.procedures.js +283 -0
  8. package/dist/api/core/auth.router.js +20 -203
  9. package/dist/api/core/live-events.router.js +1 -1
  10. package/dist/api/core/preferences.router.js +98 -0
  11. package/dist/api/oauth2/oauth2-routes.js +40 -15
  12. package/dist/api/trpc/device-config-secret-redaction.js +2 -2
  13. package/dist/api/trpc/generated-cap-mounts.js +0 -1
  14. package/dist/api/trpc/generated-cap-routers.js +2 -38
  15. package/dist/api/trpc/principal-authorization.js +82 -0
  16. package/dist/api/trpc/principal-visibility.js +3 -3
  17. package/dist/api/trpc/share-view-access.js +1 -1
  18. package/dist/api/trpc/trpc-error-principal.js +9 -9
  19. package/dist/api/trpc/trpc.context.js +87 -258
  20. package/dist/api/trpc/trpc.middleware.js +42 -83
  21. package/dist/api/trpc/trpc.router.js +7 -1
  22. package/dist/api/upload-auth.js +19 -54
  23. package/dist/auth/addon-route-auth.js +44 -0
  24. package/dist/auth/addon-route-share-gate.js +18 -16
  25. package/dist/auth/credential-verifier.js +236 -0
  26. package/dist/auth/embed-auth-route.js +45 -0
  27. package/dist/auth/live-user.js +19 -0
  28. package/dist/auth/scope-escalation.js +133 -0
  29. package/dist/boot/boot-config.js +8 -1
  30. package/dist/core/addon/addon-registry.service.js +18 -7
  31. package/dist/core/auth/credential-token.service.js +245 -0
  32. package/dist/core/auth/handoff-code.service.js +1 -1
  33. package/dist/core/auth/record-kinds.js +38 -2
  34. package/dist/main.js +65 -141
  35. package/package.json +14 -14
  36. package/dist/api/core/auth-admin-tokens.procedures.js +0 -138
  37. package/dist/auth/addon-route-jwt-gate.js +0 -59
  38. package/dist/core/auth/share-token.service.js +0 -459
@@ -10,7 +10,7 @@ exports.scopedEventVisibility = scopedEventVisibility;
10
10
  * Non-admin principal device visibility — the shared answer to "which cameras
11
11
  * may this scoped caller see?", reused by two hotfixes:
12
12
  *
13
- * - F0.4 `auth.createShareToken` — a scoped user must not mint a `csv_*`
13
+ * - F0.4 `auth.mintToken` (a share-view grant) — a scoped user must not mint a share link
14
14
  * share token for a device it cannot see. The requested deviceIds are
15
15
  * INTERSECTED with what the caller can view.
16
16
  * - F0.5 the event firehose (`live.onEvent`, `systemEvents.subscribe`, …) —
@@ -141,13 +141,13 @@ function projectListAllForPrincipal(data, isAdmin, scopes, fleet) {
141
141
  * view; an event with NO device identity is DROPPED (fail closed — "probably in
142
142
  * scope" is not a security argument).
143
143
  *
144
- * Share-view (`csv_*`) principals are NOT handled here — they keep their own,
144
+ * Share-view principals are NOT handled here — they keep their own,
145
145
  * stricter `liveEventInShareScope` filter, applied by the routers first.
146
146
  */
147
147
  function scopedEventVisibility(user, lookup) {
148
148
  if (user.isAdmin)
149
149
  return null;
150
- const scopes = user.scopes ?? [];
150
+ const scopes = (0, types_1.capabilityScopesOf)(user.scopes ?? []);
151
151
  if (isBroadEventViewer(scopes))
152
152
  return null;
153
153
  return (evt) => {
@@ -181,7 +181,7 @@ function projectListAllForShareScope(data, scope) {
181
181
  * The projection ENGINE: drop every `listAll` row whose id is not in
182
182
  * `allowed`, and project the survivors onto the whitelisted row.
183
183
  *
184
- * Shared by the `csv_*` share path ({@link projectListAllForShareScope}) and
184
+ * Shared by the share-link path ({@link projectListAllForShareScope}) and
185
185
  * the scoped-principal path (`projectListAllForPrincipal` in
186
186
  * `principal-visibility.ts`) so enumeration is cut ONE way. Two projectors
187
187
  * would be two chances to forget `config` — which carries the camera
@@ -29,12 +29,13 @@ function readCredentialIdentity(claims) {
29
29
  /**
30
30
  * Render a principal for one log line.
31
31
  *
32
- * Shapes, in the order they are distinguished:
32
+ * Shapes, in the order they are distinguished (the label comes from the
33
+ * principal's audit tag — attribution only, D892):
33
34
  *
34
35
  * anonymous
35
36
  * apocaliss92 (admin)
36
- * share:ab12cd34 (share-view)
37
- * scoped:1a2b3c4d (scoped-token)
37
+ * token:ci (token)
38
+ * token:ops (token admin)
38
39
  * apocaliss92 (oauth-access session=910a6179-…)
39
40
  * apocaliss92 (session)
40
41
  */
@@ -44,15 +45,14 @@ function describeTrpcPrincipal(user) {
44
45
  if (!user)
45
46
  return 'anonymous';
46
47
  const name = user.username || user.id || 'unknown';
47
- if (user.shareView)
48
- return `${name} (share-view)`;
49
- if (user.isAdmin)
48
+ if (user.auditTag === 'session' && user.isAdmin)
50
49
  return `${name} (admin)`;
51
50
  if (user.credential) {
52
51
  const session = user.oauthSessionId ? ` session=${user.oauthSessionId}` : '';
53
52
  return `${name} (${user.credential}${session})`;
54
53
  }
55
- if (user.isScoped)
56
- return `${name} (scoped-token)`;
57
- return `${name} (session)`;
54
+ if (user.auditTag !== undefined) {
55
+ return `${name} (${user.auditTag}${user.isAdmin && user.auditTag !== 'session' ? ' admin' : ''})`;
56
+ }
57
+ return user.isAdmin ? `${name} (admin)` : `${name} (session)`;
58
58
  }
@@ -1,56 +1,65 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentFromPrincipal = agentFromPrincipal;
4
+ exports.bindHubCredentialStores = bindHubCredentialStores;
5
+ exports.hubCredentialVerifier = hubCredentialVerifier;
6
+ exports.requestFacts = requestFacts;
3
7
  exports.createMeshTrpcContext = createMeshTrpcContext;
4
8
  exports.createTrpcContext = createTrpcContext;
5
9
  exports.createWsTrpcContext = createWsTrpcContext;
6
10
  const system_1 = require("@camstack/system");
7
- const types_1 = require("@camstack/types");
8
11
  const auth_network_policy_js_1 = require("../../auth/auth-network-policy.js");
9
- const share_token_service_js_1 = require("../../core/auth/share-token.service.js");
12
+ const credential_verifier_js_1 = require("../../auth/credential-verifier.js");
10
13
  const request_lang_js_1 = require("../../i18n/request-lang.js");
11
- const trpc_error_principal_js_1 = require("./trpc-error-principal.js");
14
+ /** The tRPC principal a verified credential becomes. */
15
+ function agentFromPrincipal(principal) {
16
+ return {
17
+ id: principal.subjectId,
18
+ username: principal.username,
19
+ isAdmin: principal.isAdmin,
20
+ permissions: { isAdmin: principal.isAdmin, allowedProviders: '*', allowedDevices: {} },
21
+ isApiKey: !principal.interactive,
22
+ scopes: principal.scopes,
23
+ credentialId: principal.credentialId,
24
+ auditTag: principal.auditTag,
25
+ interactive: principal.interactive,
26
+ userDelegated: principal.userDelegated,
27
+ ...(principal.sessionId !== undefined ? { sessionId: principal.sessionId } : {}),
28
+ ...(principal.oauthSessionId !== undefined ? { oauthSessionId: principal.oauthSessionId } : {}),
29
+ ...(principal.credential !== undefined ? { credential: principal.credential } : {}),
30
+ };
31
+ }
12
32
  /**
13
- * Live-scope refresh memo (see the JWT branch of `resolveUser`).
14
- *
15
- * The JWT embeds scopes at login, so an admin's scope edit used to reach a
16
- * live session only at the next login — a user granted cameras while logged
17
- * in saw NONE until they signed out and back in (2026-08-15, user report).
18
- * The store is therefore re-read per user, but memoized: a scope change
19
- * propagates within {@link SCOPE_REFRESH_TTL_MS} and a busy session costs at
20
- * most one store read per TTL window, not one per request.
33
+ * The live-scope memo of non-admin sessions, shared by every verifier this
34
+ * process builds (a request builds one; the memo must outlive it).
21
35
  */
22
- const SCOPE_REFRESH_TTL_MS = 30_000;
23
- const SCOPE_REFRESH_MISS_TTL_MS = 5_000;
24
- const liveScopeMemo = new Map();
25
- async function refreshScopesFromStore(userId, addonRegistry) {
26
- const cached = liveScopeMemo.get(userId);
27
- const now = Date.now();
28
- if (cached &&
29
- now - cached.at < (cached.scopes === null ? SCOPE_REFRESH_MISS_TTL_MS : SCOPE_REFRESH_TTL_MS)) {
30
- return cached.scopes;
31
- }
32
- let scopes = null;
33
- try {
34
- const userMgmt = addonRegistry.getCapabilityRegistry().getSingleton('user-management');
35
- const fresh = typeof userMgmt?.listUsers === 'function'
36
- ? (await userMgmt.listUsers()).find((u) => u.id === userId)
37
- : null;
38
- if (fresh)
39
- scopes = (0, types_1.normalizeTokenScopes)(fresh.scopes ?? []);
40
- }
41
- catch {
42
- scopes = null;
43
- }
44
- liveScopeMemo.set(userId, { at: now, scopes });
45
- // Bound the map: sessions come and go, entries are tiny, and a missed
46
- // sweep would grow it by one row per distinct user per TTL window.
47
- if (liveScopeMemo.size > 512) {
48
- for (const [key, entry] of liveScopeMemo) {
49
- if (now - entry.at > SCOPE_REFRESH_TTL_MS * 4)
50
- liveScopeMemo.delete(key);
51
- }
52
- }
53
- return scopes;
36
+ const hubScopeMemo = new Map();
37
+ /** The user-list memo, shared the same way (every token verify asks "does its subject exist?"). */
38
+ const hubUsersMemo = { entry: null };
39
+ const hubStores = { tokenRows: null };
40
+ function bindHubCredentialStores(stores) {
41
+ if (stores.tokenRows !== undefined)
42
+ hubStores.tokenRows = stores.tokenRows;
43
+ }
44
+ /**
45
+ * The hub's verifier over its own stores — the same one for tRPC and the
46
+ * addon routes. Documented type boundary: the registry hands the
47
+ * `user-management` provider back untyped; the one method the verifier reads
48
+ * (`listUsers`) is asserted here.
49
+ */
50
+ function hubCredentialVerifier(deps) {
51
+ const provider = () => deps.addonRegistry.getCapabilityRegistry().getSingleton('user-management');
52
+ return (0, credential_verifier_js_1.createCredentialVerifier)({
53
+ verifyJwt: (token) => deps.authService.verifyToken(token),
54
+ verifyCredentialJwt: (token) => deps.authService.verifyCredentialToken(token),
55
+ users: () => provider() ?? null,
56
+ internal: () => (0, system_1.getUserManagementInternal)(provider()),
57
+ tokenRows: () => deps.tokenRows ?? hubStores.tokenRows,
58
+ logger: deps.logger ?? null,
59
+ scopeMemo: hubScopeMemo,
60
+ usersMemo: hubUsersMemo,
61
+ ...(deps.deviceScopeLookup !== undefined ? { deviceScopeLookup: deps.deviceScopeLookup } : {}),
62
+ });
54
63
  }
55
64
  /** Read `req.query` if present (Fastify-only) without losing type safety. */
56
65
  function readQuery(req) {
@@ -62,7 +71,7 @@ function readQuery(req) {
62
71
  return { ...q };
63
72
  }
64
73
  /**
65
- * Extract a JWT token from an HTTP request.
74
+ * Extract a bearer from an HTTP request.
66
75
  * Priority: Authorization header → Fastify req.query → URL query string
67
76
  */
68
77
  function extractTokenFromRequest(req) {
@@ -89,6 +98,7 @@ function extractTokenFromRequest(req) {
89
98
  }
90
99
  return null;
91
100
  }
101
+ /** What the audit of an admin-capable token's use records about the request. */
92
102
  function requestFacts(req) {
93
103
  if (req === undefined)
94
104
  return {};
@@ -97,203 +107,9 @@ function requestFacts(req) {
97
107
  const path = typeof url === 'string' ? url.split('?')[0] : undefined;
98
108
  return { ...(path !== undefined ? { path } : {}), ...(ip !== null ? { ip } : {}) };
99
109
  }
100
- /**
101
- * An admin token (`csa_`): an ADMIN principal, attributed to the admin who
102
- * created it, every use audited. Validation (owner still exists and is still
103
- * an admin, not revoked, not expired) is local-auth's (`AdminTokenManager`).
104
- */
105
- async function resolveAdminToken(token, addonRegistry, facts) {
106
- const internal = (0, system_1.getUserManagementInternal)(addonRegistry.getCapabilityRegistry().getSingleton('user-management'));
107
- if (internal === null)
108
- return null;
109
- const record = await internal.adminTokens.validate(token).catch(() => null);
110
- if (record === null)
111
- return null;
112
- void internal.audit.record({
113
- action: 'admin-token-used',
114
- userId: record.createdBy,
115
- ...(facts.ip !== undefined ? { ip: facts.ip } : {}),
116
- detail: {
117
- tokenId: record.id,
118
- name: record.name,
119
- prefix: record.tokenPrefix,
120
- ...(facts.path !== undefined ? { path: facts.path } : {}),
121
- },
122
- });
123
- return {
124
- id: record.createdBy,
125
- username: `admin-token:${record.name}`,
126
- isAdmin: true,
127
- permissions: { isAdmin: true, allowedProviders: '*', allowedDevices: {} },
128
- isApiKey: true,
129
- credential: 'admin-token',
130
- adminTokenId: record.id,
131
- };
132
- }
133
- async function resolveUser(token, authService, addonRegistry, shareTokens = null, logger = null, facts = {}) {
134
- if (!token)
135
- return null;
136
- if (token.startsWith(system_1.ADMIN_TOKEN_PREFIX))
137
- return resolveAdminToken(token, addonRegistry, facts);
138
- // Share-token path (`csv_*`): resolve through the ShareTokenService.
139
- // The synthetic principal is maximally restricted — non-admin, no
140
- // scopes; the `shareView` grant routes it into the fail-closed
141
- // allowlist in `protectedProcedure` instead of the scope matcher.
142
- if (token.startsWith(share_token_service_js_1.SHARE_TOKEN_PREFIX)) {
143
- if (!shareTokens)
144
- return null;
145
- try {
146
- const record = await shareTokens.validate(token);
147
- if (!record)
148
- return null;
149
- return {
150
- id: record.userId,
151
- // Display label — `share:<prefix>` keeps audit logs readable
152
- // without exposing the token hash.
153
- username: `share:${record.tokenPrefix}`,
154
- isAdmin: false,
155
- permissions: {
156
- isAdmin: false,
157
- allowedProviders: '*',
158
- allowedDevices: {},
159
- },
160
- isApiKey: true,
161
- shareView: { tokenId: record.id, scope: record.scope },
162
- };
163
- }
164
- catch {
165
- return null;
166
- }
167
- }
168
- // Scoped-token path: hit the user-management cap. Synthetic user
169
- // with `isAdmin: false` so admin-gated procedures bounce while
170
- // protectedProcedure can still gate by scope match.
171
- if (token.startsWith('cst_')) {
172
- try {
173
- const userMgmt = addonRegistry.getCapabilityRegistry().getSingleton('user-management');
174
- if (!userMgmt)
175
- return null;
176
- const record = await userMgmt.validateScopedToken({ token });
177
- if (!record)
178
- return null;
179
- return {
180
- id: record.userId,
181
- // Display label — `scoped:<prefix>` makes audit logs read
182
- // naturally without exposing the token hash.
183
- username: `scoped:${record.tokenPrefix}`,
184
- isAdmin: false,
185
- permissions: {
186
- isAdmin: false,
187
- allowedProviders: '*',
188
- allowedDevices: {},
189
- },
190
- isApiKey: true,
191
- isScoped: true,
192
- // Migrate any v2 `device:targets` grant to a v3 selector at the
193
- // boundary so the enforcement matcher only ever sees v3.
194
- scopes: (0, types_1.normalizeTokenScopes)(record.scopes),
195
- };
196
- }
197
- catch {
198
- return null;
199
- }
200
- }
201
- // JWT path.
202
- try {
203
- const payload = authService.verifyToken(token);
204
- // Reject pre-v2 JWTs at the boundary. Tokens issued before the
205
- // role → isAdmin migration don't carry the `isAdmin` field, so
206
- // letting them through would degrade silently into "non-admin
207
- // with no scopes" → locked out of every cap. Returning null forces
208
- // the client to land on 401 → re-login, where it picks up a fresh
209
- // v2 token. No back-compat shim — the role enum is gone.
210
- if (typeof payload.isAdmin !== 'boolean') {
211
- return null;
212
- }
213
- // What the token IS decides both WHETHER it authenticates at all and
214
- // WHERE its scopes come from.
215
- const principal = (0, types_1.classifyBearerPrincipal)(payload);
216
- // ── The allowlist ────────────────────────────────────────────────
217
- // Every token this hub mints verifies under the same `auth.jwtSecret`, so
218
- // `verifyToken` succeeding means "we minted this", never "this is an API
219
- // credential" (D103). Exactly two kinds are credentials here:
220
- //
221
- // • `session` — no `kind` claim: `auth.login`'s JWT and the SSO-minted
222
- // session from `/api/auth/sso/finish`.
223
- // • `integration` — `provider: 'oauth-access'`: the account-link
224
- // credential a component (Home Assistant) calls `/trpc` with.
225
- //
226
- // Everything else is minted for ONE hand-off step and is refused: an
227
- // `oauth-code` is single-use, lives 60s and travels in a browser redirect
228
- // URL (history, referrers, proxy logs); an `oauth-refresh` belongs to
229
- // `POST /oauth/token`; an `oidc` login bridge belongs to
230
- // `?bridge=` on `/api/auth/sso/finish`; a `totp-challenge` is login leg 1.
231
- // `/addon/:addonId/*` has refused these since D103 — this closes the same
232
- // hole on the other surface.
233
- //
234
- // The TOTP login is unaffected BY CONSTRUCTION: `loginVerifyTotp` takes
235
- // the challenge as procedure INPUT on a public rate-limited procedure, and
236
- // neither the SDK (`login()` calls `setToken` only when
237
- // `!requiresSecondFactor`) nor the admin UI ever puts it in a header.
238
- if (principal.kind === 'not-a-credential') {
239
- logger?.warn('trpc: refused a bearer that is not an API credential', {
240
- meta: { reason: principal.reason, userId: payload.userId ?? 'unknown' },
241
- });
242
- return null;
243
- }
244
- // What the token says it IS. An `oauth-access` token is an account link,
245
- // and until this was carried onto the principal a refusal of one was
246
- // indistinguishable in the logs from a refusal of a browser session.
247
- const credential = (0, trpc_error_principal_js_1.readCredentialIdentity)(payload);
248
- return {
249
- id: payload.userId ?? 'unknown',
250
- username: payload.username ?? 'unknown',
251
- isAdmin: payload.isAdmin,
252
- permissions: {
253
- isAdmin: payload.isAdmin,
254
- allowedProviders: payload.allowedProviders,
255
- allowedDevices: payload.allowedDevices,
256
- },
257
- isApiKey: false,
258
- ...(principal.kind === 'session' && typeof payload.sid === 'string'
259
- ? { sessionId: payload.sid }
260
- : {}),
261
- // Scopes are baked into the JWT at login; the middleware uses
262
- // them to gate every call until the user re-logs. Normalised at the
263
- // boundary — a v2 `device:targets` JWT (issued before the v3 model)
264
- // migrates to a selector here, so a pre-v3 token keeps working.
265
- //
266
- // …but a login-time snapshot is exactly the bug: an admin editing a
267
- // user's scopes expects the change to APPLY, and the live session
268
- // kept gating on the old set until re-login (2026-08-15: a user
269
- // granted the whole fleet kept seeing zero cameras). For non-admin
270
- // SESSION principals the store's current scopes win when readable
271
- // (memoized — see above); the JWT's copy is the fallback when the
272
- // user-management cap is unreachable, so a store hiccup never
273
- // locks anybody out mid-request.
274
- //
275
- // `session` ONLY, and that word is the whole fix. An ACCOUNT-LINK token
276
- // is governed by the grant the operator consented to, not by the linking
277
- // user's record: it always carries `isAdmin: false` (`oauth-grants.ts`
278
- // → `baseClaims`) and its owner is normally an admin, whose record is
279
- // deliberately unscoped — so the refresh replaced a six-scope Home
280
- // Assistant grant with `[]` and the hub refused the component's first
281
- // call with `Have: (none)` (2026-08-19). The user store knows nothing
282
- // about a link and must not get a vote on one.
283
- ...(payload.scopes !== undefined ? { scopes: (0, types_1.normalizeTokenScopes)(payload.scopes) } : {}),
284
- ...(!payload.isAdmin && principal.kind === 'session'
285
- ? await (async () => {
286
- const fresh = await refreshScopesFromStore(payload.userId ?? 'unknown', addonRegistry);
287
- return fresh !== null ? { scopes: fresh } : {};
288
- })()
289
- : {}),
290
- ...(credential.credential !== undefined ? { credential: credential.credential } : {}),
291
- ...(credential.sessionId !== undefined ? { oauthSessionId: credential.sessionId } : {}),
292
- };
293
- }
294
- catch {
295
- return null;
296
- }
110
+ async function resolveUser(token, verifier, facts) {
111
+ const principal = await verifier.verify(token, facts);
112
+ return principal === null ? null : agentFromPrincipal(principal);
297
113
  }
298
114
  /**
299
115
  * Build the parent-chain walker for the scope-access matcher. Returns every
@@ -304,8 +120,7 @@ async function resolveUser(token, authService, addonRegistry, shareTokens = null
304
120
  * the device-manager's PERSISTED parentage (F0.6). It MUST NOT walk the hub
305
121
  * `DeviceRegistry` directly: that registry only holds hub-local devices, so it
306
122
  * is empty for every forked provider and the inheritance this function exists to
307
- * provide silently did nothing (0/6 providers inherited). The mirror is warmed
308
- * off the event path and read synchronously here.
123
+ * provide silently did nothing (0/6 providers inherited).
309
124
  */
310
125
  function makeDeviceScopeLookup(addonRegistry) {
311
126
  return {
@@ -321,15 +136,12 @@ function makeDeviceScopeLookup(addonRegistry) {
321
136
  * `principal` names WHO is calling (D802): `mesh` for the hub's own runners
322
137
  * (they reach the bridge over UDS, inside the hub's trust boundary), and
323
138
  * `node:<nodeId>` for another node — whose id the hub's mesh gate has bound
324
- * to its CERTIFICATE before the call got here (`createMeshAccessMiddleware`:
325
- * the TLS handshake chained to the mesh CA, the leaf CN is that id, D857).
139
+ * to its CERTIFICATE before the call got here (D857).
326
140
  *
327
- * Both principals are still admins: that keeps every `adminProcedure` an
328
- * agent's addons call working. What is NOT reachable from another node is
329
- * decided by the bridge itself (`REMOTE_DENIED_NAMESPACES`), and `repl` is not
330
- * bridged at all. Narrowing the remote principal to a per-method grant is the
331
- * next step once the mesh census has measured what each node calls (D802).
332
- * There is no HTTP request behind the call.
141
+ * Both principals hold `'*'`: that keeps every `adminProcedure` an agent's
142
+ * addons call working. What is NOT reachable from another node is decided by
143
+ * the bridge itself (`REMOTE_DENIED_NAMESPACES`), and `repl` is not bridged at
144
+ * all. There is no HTTP request behind the call.
333
145
  */
334
146
  function createMeshTrpcContext(principal = 'mesh') {
335
147
  const user = {
@@ -338,17 +150,26 @@ function createMeshTrpcContext(principal = 'mesh') {
338
150
  isAdmin: true,
339
151
  permissions: { isAdmin: true, allowedProviders: '*', allowedDevices: {} },
340
152
  isApiKey: true,
153
+ scopes: ['*'],
154
+ credentialId: principal,
341
155
  };
342
156
  return { user };
343
157
  }
344
158
  /** Context factory for HTTP tRPC requests (Fastify adapter). */
345
- async function createTrpcContext(req, authService, addonRegistry, shareTokens = null, logger = null) {
346
- const token = extractTokenFromRequest(req);
159
+ async function createTrpcContext(req, authService, addonRegistry, logger = null, tokenRows = null) {
160
+ const deviceScopeLookup = makeDeviceScopeLookup(addonRegistry);
161
+ const verifier = hubCredentialVerifier({
162
+ authService,
163
+ addonRegistry,
164
+ tokenRows,
165
+ logger,
166
+ deviceScopeLookup,
167
+ });
347
168
  return {
348
- user: await resolveUser(token, authService, addonRegistry, shareTokens, logger, requestFacts(req)),
169
+ user: await resolveUser(extractTokenFromRequest(req), verifier, requestFacts(req)),
349
170
  req,
350
171
  lang: (0, request_lang_js_1.resolveRequestLang)((0, request_lang_js_1.requestLangInputFrom)(req.headers, readQuery(req))),
351
- deviceScopeLookup: makeDeviceScopeLookup(addonRegistry),
172
+ deviceScopeLookup,
352
173
  deviceFleet: () => addonRegistry.getPersistedDeviceList(),
353
174
  };
354
175
  }
@@ -357,11 +178,19 @@ async function createTrpcContext(req, authService, addonRegistry, shareTokens =
357
178
  * Token is sent via tRPC connectionParams (a JSON message sent right after
358
179
  * the WS handshake), which is more reliable than query params through proxies.
359
180
  */
360
- async function createWsTrpcContext(opts, authService, addonRegistry, shareTokens = null, logger = null) {
181
+ async function createWsTrpcContext(opts, authService, addonRegistry, logger = null, tokenRows = null) {
361
182
  // 1. connectionParams.token (sent by BackendClient's createWSClient)
362
183
  const paramToken = opts.info.connectionParams?.['token'];
363
184
  const token = (typeof paramToken === 'string' ? paramToken : null) ?? extractTokenFromRequest(opts.req);
364
- const user = await resolveUser(token, authService, addonRegistry, shareTokens, logger, requestFacts(opts.req));
185
+ const deviceScopeLookup = makeDeviceScopeLookup(addonRegistry);
186
+ const verifier = hubCredentialVerifier({
187
+ authService,
188
+ addonRegistry,
189
+ tokenRows,
190
+ logger,
191
+ deviceScopeLookup,
192
+ });
193
+ const user = await resolveUser(token, verifier, requestFacts(opts.req));
365
194
  const sessionId = user?.sessionId;
366
195
  return {
367
196
  user,
@@ -379,7 +208,7 @@ async function createWsTrpcContext(opts, authService, addonRegistry, shareTokens
379
208
  }) === null,
380
209
  }
381
210
  : {}),
382
- deviceScopeLookup: makeDeviceScopeLookup(addonRegistry),
211
+ deviceScopeLookup,
383
212
  deviceFleet: () => addonRegistry.getPersistedDeviceList(),
384
213
  };
385
214
  }