@camstack/server 1.2.133 → 1.2.135
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.
- package/dist/api/trpc/trpc.context.js +51 -9
- package/dist/main.js +6 -2
- package/package.json +9 -9
|
@@ -93,10 +93,11 @@ function extractTokenFromRequest(req) {
|
|
|
93
93
|
* its REST auth chain).
|
|
94
94
|
*
|
|
95
95
|
* Returns `null` for: missing token, malformed JWT, unknown scoped
|
|
96
|
-
* token
|
|
97
|
-
*
|
|
96
|
+
* token, and a JWT that is not an ACCESS CREDENTIAL (see the classifier
|
|
97
|
+
* branch below). Caller (protectedProcedure) decides the failure
|
|
98
|
+
* response (typically UNAUTHORIZED).
|
|
98
99
|
*/
|
|
99
|
-
async function resolveUser(token, authService, addonRegistry, shareTokens = null) {
|
|
100
|
+
async function resolveUser(token, authService, addonRegistry, shareTokens = null, logger = null) {
|
|
100
101
|
if (!token)
|
|
101
102
|
return null;
|
|
102
103
|
// Share-token path (`csv_*`): resolve through the ShareTokenService.
|
|
@@ -174,6 +175,38 @@ async function resolveUser(token, authService, addonRegistry, shareTokens = null
|
|
|
174
175
|
if (typeof payload.isAdmin !== 'boolean') {
|
|
175
176
|
return null;
|
|
176
177
|
}
|
|
178
|
+
// What the token IS decides both WHETHER it authenticates at all and
|
|
179
|
+
// WHERE its scopes come from.
|
|
180
|
+
const principal = (0, types_1.classifyBearerPrincipal)(payload);
|
|
181
|
+
// ── The allowlist ────────────────────────────────────────────────
|
|
182
|
+
// Every token this hub mints verifies under the same `auth.jwtSecret`, so
|
|
183
|
+
// `verifyToken` succeeding means "we minted this", never "this is an API
|
|
184
|
+
// credential" (D103). Exactly two kinds are credentials here:
|
|
185
|
+
//
|
|
186
|
+
// • `session` — no `kind` claim: `auth.login`'s JWT, the SSO-minted
|
|
187
|
+
// session from `/api/auth/sso/finish`, a `type: 'service'` agent
|
|
188
|
+
// token, a `type: 'api_key'` token.
|
|
189
|
+
// • `integration` — `provider: 'oauth-access'`: the account-link
|
|
190
|
+
// credential a component (Home Assistant) calls `/trpc` with.
|
|
191
|
+
//
|
|
192
|
+
// Everything else is minted for ONE hand-off step and is refused: an
|
|
193
|
+
// `oauth-code` is single-use, lives 60s and travels in a browser redirect
|
|
194
|
+
// URL (history, referrers, proxy logs); an `oauth-refresh` belongs to
|
|
195
|
+
// `POST /oauth/token`; an `oidc`/`magic-link` bridge belongs to
|
|
196
|
+
// `?bridge=` on `/api/auth/sso/finish`; a `totp-challenge` is login leg 1.
|
|
197
|
+
// `/addon/:addonId/*` has refused these since D103 — this closes the same
|
|
198
|
+
// hole on the other surface.
|
|
199
|
+
//
|
|
200
|
+
// The TOTP login is unaffected BY CONSTRUCTION: `loginVerifyTotp` takes
|
|
201
|
+
// the challenge as procedure INPUT on a public rate-limited procedure, and
|
|
202
|
+
// neither the SDK (`login()` calls `setToken` only when
|
|
203
|
+
// `!requiresSecondFactor`) nor the admin UI ever puts it in a header.
|
|
204
|
+
if (principal.kind === 'not-a-credential') {
|
|
205
|
+
logger?.warn('trpc: refused a bearer that is not an API credential', {
|
|
206
|
+
meta: { reason: principal.reason, userId: payload.userId ?? payload.keyId ?? 'unknown' },
|
|
207
|
+
});
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
177
210
|
// What the token says it IS. An `oauth-access` token is an account link,
|
|
178
211
|
// and until this was carried onto the principal a refusal of one was
|
|
179
212
|
// indistinguishable in the logs from a refusal of a browser session.
|
|
@@ -198,12 +231,21 @@ async function resolveUser(token, authService, addonRegistry, shareTokens = null
|
|
|
198
231
|
// user's scopes expects the change to APPLY, and the live session
|
|
199
232
|
// kept gating on the old set until re-login (2026-08-15: a user
|
|
200
233
|
// granted the whole fleet kept seeing zero cameras). For non-admin
|
|
201
|
-
//
|
|
234
|
+
// SESSION principals the store's current scopes win when readable
|
|
202
235
|
// (memoized — see above); the JWT's copy is the fallback when the
|
|
203
236
|
// user-management cap is unreachable, so a store hiccup never
|
|
204
237
|
// locks anybody out mid-request.
|
|
238
|
+
//
|
|
239
|
+
// `session` ONLY, and that word is the whole fix. An ACCOUNT-LINK token
|
|
240
|
+
// is governed by the grant the operator consented to, not by the linking
|
|
241
|
+
// user's record: it always carries `isAdmin: false` (`oauth-grants.ts`
|
|
242
|
+
// → `baseClaims`) and its owner is normally an admin, whose record is
|
|
243
|
+
// deliberately unscoped — so the refresh replaced a six-scope Home
|
|
244
|
+
// Assistant grant with `[]` and the hub refused the component's first
|
|
245
|
+
// call with `Have: (none)` (2026-08-19). The user store knows nothing
|
|
246
|
+
// about a link and must not get a vote on one.
|
|
205
247
|
...(payload.scopes !== undefined ? { scopes: (0, types_1.normalizeTokenScopes)(payload.scopes) } : {}),
|
|
206
|
-
...(!payload.isAdmin
|
|
248
|
+
...(!payload.isAdmin && principal.kind === 'session'
|
|
207
249
|
? await (async () => {
|
|
208
250
|
const fresh = await refreshScopesFromStore(payload.userId ?? payload.keyId ?? 'unknown', addonRegistry);
|
|
209
251
|
return fresh !== null ? { scopes: fresh } : {};
|
|
@@ -257,10 +299,10 @@ function createMeshTrpcContext() {
|
|
|
257
299
|
return { user };
|
|
258
300
|
}
|
|
259
301
|
/** Context factory for HTTP tRPC requests (Fastify adapter). */
|
|
260
|
-
async function createTrpcContext(req, authService, addonRegistry, shareTokens = null) {
|
|
302
|
+
async function createTrpcContext(req, authService, addonRegistry, shareTokens = null, logger = null) {
|
|
261
303
|
const token = extractTokenFromRequest(req);
|
|
262
304
|
return {
|
|
263
|
-
user: await resolveUser(token, authService, addonRegistry, shareTokens),
|
|
305
|
+
user: await resolveUser(token, authService, addonRegistry, shareTokens, logger),
|
|
264
306
|
req,
|
|
265
307
|
deviceScopeLookup: makeDeviceScopeLookup(addonRegistry),
|
|
266
308
|
deviceFleet: () => addonRegistry.getPersistedDeviceList(),
|
|
@@ -271,11 +313,11 @@ async function createTrpcContext(req, authService, addonRegistry, shareTokens =
|
|
|
271
313
|
* Token is sent via tRPC connectionParams (a JSON message sent right after
|
|
272
314
|
* the WS handshake), which is more reliable than query params through proxies.
|
|
273
315
|
*/
|
|
274
|
-
async function createWsTrpcContext(opts, authService, addonRegistry, shareTokens = null) {
|
|
316
|
+
async function createWsTrpcContext(opts, authService, addonRegistry, shareTokens = null, logger = null) {
|
|
275
317
|
// 1. connectionParams.token (sent by BackendClient's createWSClient)
|
|
276
318
|
const paramToken = opts.info.connectionParams?.['token'];
|
|
277
319
|
const token = (typeof paramToken === 'string' ? paramToken : null) ?? extractTokenFromRequest(opts.req);
|
|
278
|
-
const user = await resolveUser(token, authService, addonRegistry, shareTokens);
|
|
320
|
+
const user = await resolveUser(token, authService, addonRegistry, shareTokens, logger);
|
|
279
321
|
return {
|
|
280
322
|
user,
|
|
281
323
|
req: opts.req,
|
package/dist/main.js
CHANGED
|
@@ -504,7 +504,11 @@ async function bootstrap() {
|
|
|
504
504
|
prefix: '/trpc',
|
|
505
505
|
trpcOptions: {
|
|
506
506
|
router: appRouter,
|
|
507
|
-
createContext: ({ req }) => (0, trpc_context_1.createTrpcContext)(req, authService, addonRegistry, shareTokenService
|
|
507
|
+
createContext: ({ req }) => (0, trpc_context_1.createTrpcContext)(req, authService, addonRegistry, shareTokenService,
|
|
508
|
+
// A bearer refused for NOT being an API credential never reaches a
|
|
509
|
+
// procedure, so `onError` below never sees it — the refusal would
|
|
510
|
+
// be silent without this. Same reason `gateAddonJwt` logs (D103).
|
|
511
|
+
app.get(logging_service_1.LoggingService).createLogger('tRPC')),
|
|
508
512
|
onError: ({ path: trpcPath, error, ctx, }) => {
|
|
509
513
|
const trpcLogger = app.get(logging_service_1.LoggingService).createLogger('tRPC');
|
|
510
514
|
trpcLogger.warn('tRPC error', {
|
|
@@ -1065,7 +1069,7 @@ async function bootstrap() {
|
|
|
1065
1069
|
(0, ws_1.applyWSSHandler)({
|
|
1066
1070
|
wss,
|
|
1067
1071
|
router: appRouter,
|
|
1068
|
-
createContext: (opts) => (0, trpc_context_1.createWsTrpcContext)(opts, authService, addonRegistry, shareTokenService),
|
|
1072
|
+
createContext: (opts) => (0, trpc_context_1.createWsTrpcContext)(opts, authService, addonRegistry, shareTokenService, app.get(logging_service_1.LoggingService).createLogger('tRPC:ws')),
|
|
1069
1073
|
onError: ({ path: trpcPath, error, ctx, }) => {
|
|
1070
1074
|
const trpcLogger = app.get(logging_service_1.LoggingService).createLogger('tRPC:ws');
|
|
1071
1075
|
trpcLogger.warn('tRPC error', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.135",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
37
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
38
|
-
"@camstack/addon-auth": "1.2.
|
|
36
|
+
"@camstack/addon-admin-ui": "1.2.83",
|
|
37
|
+
"@camstack/addon-agent-ui": "1.2.24",
|
|
38
|
+
"@camstack/addon-auth": "1.2.26",
|
|
39
39
|
"@camstack/addon-decoder-nodeav": "1.2.22",
|
|
40
40
|
"@camstack/addon-notifiers": "1.2.27",
|
|
41
|
-
"@camstack/addon-pipeline": "1.2.
|
|
42
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
43
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
41
|
+
"@camstack/addon-pipeline": "1.2.98",
|
|
42
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.83",
|
|
43
|
+
"@camstack/addon-post-analysis": "1.2.99",
|
|
44
44
|
"@camstack/sdk": "1.2.25",
|
|
45
45
|
"@camstack/shm-ring": "1.1.22",
|
|
46
46
|
"@camstack/system": "1.2.106",
|
|
47
|
-
"@camstack/types": "1.2.
|
|
48
|
-
"@camstack/ui-library": "1.2.
|
|
47
|
+
"@camstack/types": "1.2.90",
|
|
48
|
+
"@camstack/ui-library": "1.2.62",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|
|
51
51
|
"@fastify/cors": "^11.2.0",
|