@camstack/server 1.2.134 → 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 +40 -11
- package/dist/main.js +6 -2
- package/package.json +2 -2
|
@@ -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,14 +175,42 @@ 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.
|
|
180
213
|
const credential = (0, trpc_error_principal_js_1.readCredentialIdentity)(payload);
|
|
181
|
-
// What the token IS decides WHERE its scopes come from. A `session` JWT is
|
|
182
|
-
// governed by its user's record; anything else carries its own grant and
|
|
183
|
-
// must keep it — see the live-scope note on the spread below.
|
|
184
|
-
const principal = (0, types_1.classifyBearerPrincipal)(payload);
|
|
185
214
|
return {
|
|
186
215
|
id: payload.userId ?? payload.keyId ?? 'unknown',
|
|
187
216
|
username: payload.username ?? 'unknown',
|
|
@@ -270,10 +299,10 @@ function createMeshTrpcContext() {
|
|
|
270
299
|
return { user };
|
|
271
300
|
}
|
|
272
301
|
/** Context factory for HTTP tRPC requests (Fastify adapter). */
|
|
273
|
-
async function createTrpcContext(req, authService, addonRegistry, shareTokens = null) {
|
|
302
|
+
async function createTrpcContext(req, authService, addonRegistry, shareTokens = null, logger = null) {
|
|
274
303
|
const token = extractTokenFromRequest(req);
|
|
275
304
|
return {
|
|
276
|
-
user: await resolveUser(token, authService, addonRegistry, shareTokens),
|
|
305
|
+
user: await resolveUser(token, authService, addonRegistry, shareTokens, logger),
|
|
277
306
|
req,
|
|
278
307
|
deviceScopeLookup: makeDeviceScopeLookup(addonRegistry),
|
|
279
308
|
deviceFleet: () => addonRegistry.getPersistedDeviceList(),
|
|
@@ -284,11 +313,11 @@ async function createTrpcContext(req, authService, addonRegistry, shareTokens =
|
|
|
284
313
|
* Token is sent via tRPC connectionParams (a JSON message sent right after
|
|
285
314
|
* the WS handshake), which is more reliable than query params through proxies.
|
|
286
315
|
*/
|
|
287
|
-
async function createWsTrpcContext(opts, authService, addonRegistry, shareTokens = null) {
|
|
316
|
+
async function createWsTrpcContext(opts, authService, addonRegistry, shareTokens = null, logger = null) {
|
|
288
317
|
// 1. connectionParams.token (sent by BackendClient's createWSClient)
|
|
289
318
|
const paramToken = opts.info.connectionParams?.['token'];
|
|
290
319
|
const token = (typeof paramToken === 'string' ? paramToken : null) ?? extractTokenFromRequest(opts.req);
|
|
291
|
-
const user = await resolveUser(token, authService, addonRegistry, shareTokens);
|
|
320
|
+
const user = await resolveUser(token, authService, addonRegistry, shareTokens, logger);
|
|
292
321
|
return {
|
|
293
322
|
user,
|
|
294
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@camstack/addon-notifiers": "1.2.27",
|
|
41
41
|
"@camstack/addon-pipeline": "1.2.98",
|
|
42
42
|
"@camstack/addon-pipeline-orchestrator": "1.2.83",
|
|
43
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
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",
|