@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
package/dist/main.js CHANGED
@@ -71,6 +71,8 @@ const runner_boot_check_1 = require("./core/runner-health/runner-boot-check");
71
71
  const hub_cluster_url_1 = require("./api/hub-cluster-url");
72
72
  const model_distributor_js_1 = require("./api/model-distributor.js");
73
73
  const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
74
+ const embed_auth_route_js_1 = require("./auth/embed-auth-route.js");
75
+ const live_user_js_1 = require("./auth/live-user.js");
74
76
  const server_upload_1 = require("./api/server-upload");
75
77
  const server_upload_propagate_1 = require("./api/server-upload-propagate");
76
78
  const sse_no_compression_js_1 = require("./api/sse-no-compression.js");
@@ -88,7 +90,7 @@ const trpc_error_device_tags_1 = require("./api/trpc/trpc-error-device-tags");
88
90
  const trpc_error_principal_1 = require("./api/trpc/trpc-error-principal");
89
91
  const ws_request_census_1 = require("./api/trpc/ws-request-census");
90
92
  const ws_slow_consumer_guard_js_1 = require("./api/trpc/ws-slow-consumer-guard.js");
91
- const addon_route_jwt_gate_js_1 = require("./auth/addon-route-jwt-gate.js");
93
+ const addon_route_auth_js_1 = require("./auth/addon-route-auth.js");
92
94
  const addon_route_share_gate_js_1 = require("./auth/addon-route-share-gate.js");
93
95
  const auth_network_policy_js_1 = require("./auth/auth-network-policy.js");
94
96
  const hub_sessions_js_1 = require("./auth/hub-sessions.js");
@@ -121,7 +123,7 @@ const agent_registry_service_1 = require("./core/agent/agent-registry.service");
121
123
  const well_known_app_links_1 = require("./core/app-links/well-known-app-links");
122
124
  const auth_service_1 = require("./core/auth/auth.service");
123
125
  const handoff_code_service_1 = require("./core/auth/handoff-code.service");
124
- const share_token_service_1 = require("./core/auth/share-token.service");
126
+ const credential_token_service_js_1 = require("./core/auth/credential-token.service.js");
125
127
  const config_service_1 = require("./core/config/config.service");
126
128
  const event_bus_service_1 = require("./core/events/event-bus.service");
127
129
  const feature_service_1 = require("./core/feature/feature.service");
@@ -437,8 +439,8 @@ async function bootstrap() {
437
439
  const sessionLifetimes = () => (0, session_lifetimes_js_1.readSessionLifetimes)((key) => config.get(key));
438
440
  // Register addon upload route (multipart — must be registered before tRPC).
439
441
  // We pass AddonRegistryService so the handler can resolve the
440
- // `user-management` cap singleton at request time (the only working
441
- // path to validate `cst_*` scoped tokens — see addon-upload.ts).
442
+ // `user-management` cap singleton at request time (the credential
443
+ // verifier reads users and admin bits there — see upload-auth.ts).
442
444
  try {
443
445
  const uploadAuthService = app.get(auth_service_1.AuthService);
444
446
  const uploadAddonBridge = app.get(addon_bridge_service_1.AddonBridgeService);
@@ -472,8 +474,8 @@ async function bootstrap() {
472
474
  .getSingleton('local-network')),
473
475
  });
474
476
  console.log('[bootstrap] Server upload propagate route registered at POST /api/server-upload/propagate');
475
- // Companion endpoint: /api/auth/whoami — validates JWT or cst_*
476
- // scoped tokens, returns the resolved identity + scope summary.
477
+ // Companion endpoint: /api/auth/whoami — verifies any hub credential
478
+ // through the one verifier, returns the resolved identity + scopes.
477
479
  // Mirrors the addon-upload auth chain so the CLI can ping for
478
480
  // token-still-valid without consuming a real cap.
479
481
  await (0, auth_whoami_1.registerAuthWhoamiRoute)(fastify, uploadAuthService, uploadAddonRegistry);
@@ -595,11 +597,13 @@ async function bootstrap() {
595
597
  // and the distributed metrics-provider capability — no manual wiring needed.
596
598
  let trpcRegistered = false;
597
599
  let appRouter = null;
598
- // Share-view (`csv_*`) tokens — hub-resident mint/verify/revoke over the
599
- // settings backend (lazy getter: the backend lands after the sqlite-storage
600
- // builtin registers; the service resolves it per call).
601
- const shareTokenService = new share_token_service_1.ShareTokenService(() => addonRegistry.getSettingsBackend(), loggingService.createLogger('share-tokens'));
602
- addonRegistry.setShareTokenService(shareTokenService);
600
+ // The one token store (D892, D895): rows in the `core-auth` records — admin
601
+ // tokens, scoped tokens and share links alike; the JWT is signed with the
602
+ // session signing key.
603
+ const credentialTokenService = new credential_token_service_js_1.CredentialTokenService(app.get(auth_service_1.AuthService), loggingService.createLogger('tokens'));
604
+ addonRegistry.setCredentialTokenService(credentialTokenService);
605
+ // Every surface that verifies a bearer reads the same stores (D892).
606
+ (0, trpc_context_1.bindHubCredentialStores)({ tokenRows: credentialTokenService });
603
607
  // One-time native-app login handoff codes (in-memory, 60s TTL, single-use) —
604
608
  // minted by `auth.createHandoffCode`, redeemed by the viewer app via the
605
609
  // public `auth.redeemHandoffCode`.
@@ -631,7 +635,7 @@ async function bootstrap() {
631
635
  });
632
636
  const buildRouter = () => (0, trpc_router_1.buildAppRouter)({
633
637
  authService: app.get(auth_service_1.AuthService),
634
- shareTokenService,
638
+ credentialTokenService,
635
639
  handoffCodeService,
636
640
  externalLinkIntents,
637
641
  nativeLink: {
@@ -800,10 +804,10 @@ async function bootstrap() {
800
804
  trpcOptions: {
801
805
  router: appRouter,
802
806
  createContext: async ({ req }) => {
803
- const trpcCtx = await (0, trpc_context_1.createTrpcContext)(req, authService, addonRegistry, shareTokenService,
807
+ const trpcCtx = await (0, trpc_context_1.createTrpcContext)(req, authService, addonRegistry,
804
808
  // A bearer refused for NOT being an API credential never reaches a
805
809
  // procedure, so `onError` below never sees it — the refusal would
806
- // be silent without this. Same reason `gateAddonJwt` logs (D103).
810
+ // be silent without this. Same reason the addon gate logs (D103).
807
811
  app.get(logging_service_1.LoggingService).createLogger('tRPC'));
808
812
  // WHO called, counted here because this is the first point at which
809
813
  // a principal exists AND the only one tRPC runs exactly once per
@@ -1039,42 +1043,21 @@ async function bootstrap() {
1039
1043
  reply.setCookie(c.name, c.value, c.options);
1040
1044
  return reply.send({ ok: true });
1041
1045
  });
1042
- // GET /api/embed-auth?next=<embed-path> — establish the browser session
1043
- // cookie from a Bearer token, then 302 to the embed. A native WebView can
1044
- // pass `Authorization` only on the INITIAL navigation, not on the page's
1045
- // asset sub-requests; the data-plane `authenticated` gate reads the cookie,
1046
- // so we mint it here (mirroring POST /api/auth/session) and bounce to the
1047
- // embed — page + assets then authenticate via the cookie. `next` is
1048
- // restricted to the stream-broker embed prefix (no open redirect).
1049
- fastify.get('/api/embed-auth', async (request, reply) => {
1050
- const authHeader = request.headers.authorization;
1051
- const token = authHeader?.startsWith('Bearer ')
1052
- ? authHeader.slice('Bearer '.length)
1053
- : undefined;
1054
- if (!token)
1055
- return reply.status(401).send({ error: 'Bearer token required' });
1056
- const next = request.query?.next ?? '';
1057
- if (!(0, session_cookie_js_1.isEmbedRedirectTarget)(next))
1058
- return reply.status(400).send({ error: 'invalid next' });
1059
- let ttlSec;
1060
- try {
1061
- const payload = authService.verifyToken(token);
1062
- const expSec = typeof payload.exp === 'number' ? payload.exp : 0;
1063
- ttlSec = Math.max(0, expSec - Math.floor(Date.now() / 1000));
1064
- }
1065
- catch {
1066
- return reply.status(401).send({ error: 'invalid token' });
1067
- }
1068
- const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, {
1069
- secure: (0, session_cookie_js_1.sessionCookieIsSecure)(request.protocol, request.headers, spa_mount_1.FORWARDED_PREFIX_HEADER),
1070
- });
1071
- reply.setCookie(c.name, c.value, c.options);
1072
- return reply.redirect(next);
1046
+ // GET /api/embed-auth — a person's login session becomes the embed's
1047
+ // cookie (`auth/embed-auth-route.ts`); nothing else this hub signed does.
1048
+ (0, embed_auth_route_js_1.registerEmbedAuthRoute)(fastify, {
1049
+ verifyToken: (token) => authService.verifyToken(token),
1050
+ isLiveUser: (0, live_user_js_1.liveUserCheck)(capabilityRegistry),
1073
1051
  });
1074
1052
  // Every refusal on the addon gate is a request an operator will be asking
1075
1053
  // about ("why did Home Assistant stop actuating?"). Its own scope so the
1076
1054
  // reason is greppable rather than buried in the bootstrap logger.
1077
1055
  const addonGateLogger = app.get(logging_service_1.LoggingService).createLogger('addon-route-auth');
1056
+ const addonVerifier = (0, trpc_context_1.hubCredentialVerifier)({
1057
+ authService,
1058
+ addonRegistry,
1059
+ logger: addonGateLogger,
1060
+ });
1078
1061
  // Addon HTTP API route catch-all: /addon/:addonId/*
1079
1062
  // Only handles non-GET or routes that actually exist in the addon route registry.
1080
1063
  // GET requests that don't match an addon route are SPA pages — handled by the /* fallback.
@@ -1113,40 +1096,20 @@ async function bootstrap() {
1113
1096
  return reply.status(401).send({ error: 'Unauthorized' });
1114
1097
  }
1115
1098
  const token = authHeader ? authHeader.replace('Bearer ', '') : cookieToken;
1116
- if (token.startsWith('csv_')) {
1117
- // A share link (`csv_*`): admitted only by a plane that opted in
1118
- // to its scope kind, and the scope's devices travel in headers the
1119
- // HUB sets — whatever the client sent under those names is dropped
1120
- // (see `addon-route-share-gate.ts`).
1121
- const record = await shareTokenService.validate(token);
1122
- const gate = (0, addon_route_share_gate_js_1.gateAddonShareToken)({ record, endpoint: dpMatch.endpoint });
1123
- if (!gate.ok)
1124
- return reply.status(gate.status).send({ error: gate.error });
1125
- shareHeaders = gate.headers;
1126
- }
1127
- else if (token.startsWith('cst_')) {
1128
- const userMgmt = capabilityRegistry?.getSingleton('user-management');
1129
- if (!userMgmt)
1130
- return reply.status(503).send({ error: 'User management not available' });
1131
- const scopedToken = await userMgmt.validateScopedToken({ token });
1132
- const scopeOk = scopedToken?.scopes.some((scope) => scope.type === 'addon' && scope.target === addonId);
1133
- if (!scopedToken || !scopeOk) {
1134
- return reply.status(403).send({ error: 'Token scope mismatch' });
1135
- }
1136
- }
1137
- else {
1138
- let payload;
1139
- try {
1140
- payload = authService.verifyToken(token);
1141
- }
1142
- catch {
1143
- return reply.status(401).send({ error: 'Invalid token' });
1144
- }
1145
- const gate = (0, addon_route_jwt_gate_js_1.gateAddonJwt)({ payload, addonId, method, access }, addonGateLogger);
1146
- if (!gate.ok) {
1147
- return reply.status(gate.status).send({ error: gate.error });
1148
- }
1149
- }
1099
+ // The ONE verifier and the ONE scope matcher, as on tRPC (D892). A
1100
+ // share principal is admitted only by a plane that opted in to its
1101
+ // kind, and its devices travel in headers the HUB sets — whatever
1102
+ // the client sent under those names is dropped below.
1103
+ const auth = await (0, addon_route_auth_js_1.authorizeAddonBearer)({
1104
+ bearer: token,
1105
+ verifier: addonVerifier,
1106
+ target: { addonId, method, access, share: dpMatch.endpoint.share ?? [] },
1107
+ facts: (0, trpc_context_1.requestFacts)(request),
1108
+ logger: addonGateLogger,
1109
+ });
1110
+ if (!auth.ok)
1111
+ return reply.status(auth.status).send({ error: auth.error });
1112
+ shareHeaders = auth.shareHeaders;
1150
1113
  }
1151
1114
  const qIdx = request.url.indexOf('?');
1152
1115
  const queryString = qIdx >= 0 ? request.url.slice(qIdx) : '';
@@ -1221,67 +1184,27 @@ async function bootstrap() {
1221
1184
  return reply.status(401).send({ error: 'Unauthorized' });
1222
1185
  }
1223
1186
  const token = authHeader ? authHeader.replace('Bearer ', '') : cookieToken;
1224
- if (token.startsWith('cst_')) {
1225
- const userMgmt = capabilityRegistry?.getSingleton('user-management');
1226
- if (!userMgmt)
1227
- return reply.status(503).send({ error: 'User management not available' });
1228
- const scopedToken = await userMgmt.validateScopedToken({ token });
1229
- if (!scopedToken) {
1230
- return reply.status(401).send({ error: 'Invalid token' });
1231
- }
1232
- // v2 model: scoped tokens grant by cap-category/cap-name/addon.
1233
- // For addon-route REST endpoints we match the `addon` scope
1234
- // type only — generic route-prefix scopes were dropped with
1235
- // the caps-only refactor.
1236
- const scopeMatch = scopedToken.scopes.some((scope) => {
1237
- return scope.type === 'addon' && scope.target === addonId;
1238
- });
1239
- if (!scopeMatch) {
1240
- return reply.status(403).send({ error: 'Token scope mismatch' });
1241
- }
1242
- // Build addon request with scoped token context
1243
- const addonRequest = {
1244
- params: match.params,
1245
- query,
1246
- body: request.body,
1247
- headers,
1248
- scopedToken: {
1249
- id: scopedToken.id,
1250
- userId: scopedToken.userId,
1251
- scopes: scopedToken.scopes,
1252
- },
1253
- };
1254
- const addonReply = buildAddonReply(reply);
1255
- return match.route.handler(addonRequest, addonReply);
1256
- }
1257
- else {
1258
- let payload;
1259
- try {
1260
- payload = authService.verifyToken(token);
1261
- }
1262
- catch {
1263
- return reply.status(401).send({ error: 'Invalid token' });
1264
- }
1265
- // NOTE: the handler call used to sit INSIDE this try, so a throw from
1266
- // an addon's own route handler was reported as `401 Invalid token`.
1267
- const gate = (0, addon_route_jwt_gate_js_1.gateAddonJwt)({ payload, addonId, method, access: match.route.access }, addonGateLogger);
1268
- if (!gate.ok) {
1269
- return reply.status(gate.status).send({ error: gate.error });
1270
- }
1271
- const addonRequest = {
1272
- params: match.params,
1273
- query,
1274
- body: request.body,
1275
- headers,
1276
- user: {
1277
- id: payload.userId ?? 'unknown',
1278
- username: payload.username ?? 'unknown',
1279
- isAdmin: payload.isAdmin,
1280
- },
1281
- };
1282
- const addonReply = buildAddonReply(reply);
1283
- return match.route.handler(addonRequest, addonReply);
1284
- }
1187
+ // The ONE verifier and the ONE scope matcher, as on tRPC (D892).
1188
+ const auth = await (0, addon_route_auth_js_1.authorizeAddonBearer)({
1189
+ bearer: token,
1190
+ verifier: addonVerifier,
1191
+ target: { addonId, method, access: match.route.access },
1192
+ facts: (0, trpc_context_1.requestFacts)(request),
1193
+ logger: addonGateLogger,
1194
+ });
1195
+ if (!auth.ok)
1196
+ return reply.status(auth.status).send({ error: auth.error });
1197
+ // NOTE: the handler call used to sit INSIDE the JWT try, so a throw
1198
+ // from an addon's own route handler was reported as `401 Invalid token`.
1199
+ const addonRequest = {
1200
+ params: match.params,
1201
+ query,
1202
+ body: request.body,
1203
+ headers,
1204
+ ...(0, addon_route_auth_js_1.addonRouteCaller)(auth.principal),
1205
+ };
1206
+ const addonReply = buildAddonReply(reply);
1207
+ return match.route.handler(addonRequest, addonReply);
1285
1208
  }
1286
1209
  // Public route — no auth required
1287
1210
  const addonRequest = {
@@ -1301,6 +1224,7 @@ async function bootstrap() {
1301
1224
  (0, oauth2_routes_js_1.registerOauth2Routes)(fastify, {
1302
1225
  getRegistry: () => capabilityRegistry,
1303
1226
  verifyToken: (t) => authService.verifyToken(t),
1227
+ isLiveUser: (0, live_user_js_1.liveUserCheck)(capabilityRegistry),
1304
1228
  // The hub's public endpoint from the `local-network` cap (the flagged
1305
1229
  // address, else tunnel, then mesh, then LAN). `localhost` only when the
1306
1230
  // palette names nothing — the dev case, where it is the truth.
@@ -1387,7 +1311,7 @@ async function bootstrap() {
1387
1311
  // A termination here closes with 1006; the guard's close line names it.
1388
1312
  keepAlive: { enabled: true, pingMs: 30_000, pongWaitMs: 10_000 },
1389
1313
  createContext: async (opts) => {
1390
- const wsCtx = await (0, trpc_context_1.createWsTrpcContext)(opts, authService, addonRegistry, shareTokenService, app.get(logging_service_1.LoggingService).createLogger('tRPC:ws'));
1314
+ const wsCtx = await (0, trpc_context_1.createWsTrpcContext)(opts, authService, addonRegistry, app.get(logging_service_1.LoggingService).createLogger('tRPC:ws'));
1391
1315
  // The connection's identity, derived ONCE and reused for every
1392
1316
  // operation on this socket. Frames that arrived while the bearer was
1393
1317
  // still resolving are held by the session and released here.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.604",
3
+ "version": "1.2.606",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,19 +38,19 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.440",
42
- "@camstack/addon-agent-ui": "1.2.246",
43
- "@camstack/addon-auth": "1.2.151",
44
- "@camstack/addon-decoder-nodeav": "1.2.125",
45
- "@camstack/addon-notifiers": "1.2.603",
46
- "@camstack/addon-pipeline": "1.2.603",
47
- "@camstack/addon-pipeline-orchestrator": "1.2.604",
48
- "@camstack/addon-post-analysis": "1.2.609",
49
- "@camstack/develop": "0.1.15",
50
- "@camstack/sdk": "1.2.135",
51
- "@camstack/system": "1.2.454",
52
- "@camstack/types": "1.2.385",
53
- "@camstack/ui-library": "1.2.302",
41
+ "@camstack/addon-admin-ui": "1.2.442",
42
+ "@camstack/addon-agent-ui": "1.2.248",
43
+ "@camstack/addon-auth": "1.2.153",
44
+ "@camstack/addon-decoder-nodeav": "1.2.127",
45
+ "@camstack/addon-notifiers": "1.2.605",
46
+ "@camstack/addon-pipeline": "1.2.605",
47
+ "@camstack/addon-pipeline-orchestrator": "1.2.606",
48
+ "@camstack/addon-post-analysis": "1.2.611",
49
+ "@camstack/develop": "0.1.17",
50
+ "@camstack/sdk": "1.2.137",
51
+ "@camstack/system": "1.2.456",
52
+ "@camstack/types": "1.2.387",
53
+ "@camstack/ui-library": "1.2.304",
54
54
  "@fastify/compress": "^9.0.0",
55
55
  "@fastify/cookie": "^11.0.2",
56
56
  "@fastify/cors": "^11.2.0",
@@ -1,138 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AuthAuditRecordSchema = exports.AdminTokenSchema = exports.ADMIN_TOKEN_MAX_DAYS = exports.ADMIN_TOKEN_DEFAULT_DAYS = void 0;
4
- exports.createAdminTokenProcedures = createAdminTokenProcedures;
5
- /**
6
- * Admin tokens (`csa_`) and the auth audit trail, as the `auth` router serves
7
- * them. The token store and its rules are local-auth's
8
- * (`admin-token-manager.ts`); this layer adds who may mint one: an ADMIN, in a
9
- * real signed-in session (never a token minting a token), right after
10
- * re-authenticating. Every create / use / revoke is audited.
11
- */
12
- const system_1 = require("@camstack/system");
13
- const server_1 = require("@trpc/server");
14
- const zod_1 = require("zod");
15
- const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
16
- const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
17
- const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
18
- const DAY_MS = 86_400_000;
19
- exports.ADMIN_TOKEN_DEFAULT_DAYS = 7;
20
- exports.ADMIN_TOKEN_MAX_DAYS = system_1.ADMIN_TOKEN_MAX_LIFETIME_MS / DAY_MS;
21
- exports.AdminTokenSchema = zod_1.z.object({
22
- id: zod_1.z.string(),
23
- createdBy: zod_1.z.string(),
24
- name: zod_1.z.string(),
25
- tokenPrefix: zod_1.z.string(),
26
- createdAt: zod_1.z.number(),
27
- expiresAt: zod_1.z.number(),
28
- lastUsedAt: zod_1.z.number().nullable(),
29
- revokedAt: zod_1.z.number().nullable(),
30
- });
31
- exports.AuthAuditRecordSchema = zod_1.z.object({
32
- id: zod_1.z.string(),
33
- at: zod_1.z.number(),
34
- action: zod_1.z.string(),
35
- userId: zod_1.z.string().optional(),
36
- ip: zod_1.z.string().optional(),
37
- detail: zod_1.z.record(zod_1.z.string(), zod_1.z.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean()])).optional(),
38
- });
39
- function requireInternal(registry) {
40
- const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
41
- if (internal === null) {
42
- throw new server_1.TRPCError({
43
- code: 'PRECONDITION_FAILED',
44
- message: 'User management is not available',
45
- });
46
- }
47
- return internal;
48
- }
49
- function createAdminTokenProcedures(deps) {
50
- return {
51
- /**
52
- * ADMIN — mint an admin token for an ops script. Requires a real
53
- * signed-in session (an admin token cannot mint another) and a fresh
54
- * re-authentication. Expiry mandatory: 1–90 days, default 7. The token
55
- * is in this answer only.
56
- */
57
- createAdminToken: trpc_middleware_js_1.adminProcedure
58
- .input(zod_1.z.object({
59
- name: zod_1.z.string().trim().min(1).max(80),
60
- expiresInDays: zod_1.z
61
- .number()
62
- .int()
63
- .min(1)
64
- .max(exports.ADMIN_TOKEN_MAX_DAYS)
65
- .default(exports.ADMIN_TOKEN_DEFAULT_DAYS),
66
- reauthToken: auth_reauth_procedures_js_1.ReauthTokenInput.optional(),
67
- }))
68
- .output(zod_1.z.object({ token: zod_1.z.string(), record: exports.AdminTokenSchema }))
69
- .mutation(async ({ ctx, input }) => {
70
- if (ctx.user.isScoped === true ||
71
- ctx.user.shareView !== undefined ||
72
- ctx.user.credential !== undefined) {
73
- throw new server_1.TRPCError({
74
- code: 'FORBIDDEN',
75
- message: 'An admin token can only be created from a signed-in admin session',
76
- });
77
- }
78
- (0, auth_reauth_procedures_js_1.assertReauthenticated)(deps.auth, ctx.user, input.reauthToken);
79
- const internal = requireInternal(deps.registry);
80
- const created = await internal.adminTokens.create({
81
- createdBy: ctx.user.id,
82
- name: input.name,
83
- expiresAt: Date.now() + input.expiresInDays * DAY_MS,
84
- });
85
- const ip = deps.network.clientAddress(ctx.req).ip;
86
- await internal.audit.record({
87
- action: 'admin-token-created',
88
- userId: ctx.user.id,
89
- ...(ip !== null ? { ip } : {}),
90
- detail: {
91
- tokenId: created.record.id,
92
- name: created.record.name,
93
- prefix: created.record.tokenPrefix,
94
- expiresAt: created.record.expiresAt,
95
- },
96
- });
97
- return created;
98
- }),
99
- /** ADMIN — every admin token not yet reaped (no secrets). */
100
- listAdminTokens: trpc_middleware_js_1.adminProcedure
101
- .input(zod_1.z.void())
102
- .output(zod_1.z.array(exports.AdminTokenSchema))
103
- .query(async () => [...(await requireInternal(deps.registry).adminTokens.list())]),
104
- /** ADMIN — revoke one admin token; it stops working at once. */
105
- revokeAdminToken: trpc_middleware_js_1.adminProcedure
106
- .input(zod_1.z.object({ id: zod_1.z.string().min(1) }))
107
- .output(zod_1.z.object({ success: zod_1.z.boolean() }))
108
- .mutation(async ({ ctx, input }) => {
109
- const internal = requireInternal(deps.registry);
110
- const success = await internal.adminTokens.revoke(input.id);
111
- if (success) {
112
- const ip = deps.network.clientAddress(ctx.req).ip;
113
- await internal.audit.record({
114
- action: 'admin-token-revoked',
115
- userId: ctx.user.id,
116
- ...(ip !== null ? { ip } : {}),
117
- detail: { tokenId: input.id },
118
- });
119
- }
120
- return { success };
121
- }),
122
- /** ADMIN — the authentication audit trail, newest first. */
123
- listAuthAudit: trpc_middleware_js_1.adminProcedure
124
- .input(zod_1.z
125
- .object({
126
- limit: zod_1.z.number().int().min(1).max(500).optional(),
127
- userId: zod_1.z.string().min(1).optional(),
128
- })
129
- .optional())
130
- .output(zod_1.z.array(exports.AuthAuditRecordSchema))
131
- .query(async ({ input }) => [
132
- ...(await requireInternal(deps.registry).audit.list({
133
- ...(input?.limit !== undefined ? { limit: input.limit } : {}),
134
- ...(input?.userId !== undefined ? { userId: input.userId } : {}),
135
- })),
136
- ]),
137
- };
138
- }
@@ -1,59 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.gateAddonJwt = gateAddonJwt;
4
- /**
5
- * The JWT half of the `/addon/:addonId/*` gate — shared by the HTTP data-plane
6
- * branch and the addon-route branch so there is exactly ONE answer to "may this
7
- * bearer reach this addon".
8
- *
9
- * Before this existed the two branches disagreed with themselves and with the
10
- * `cst_` branch: a scoped token needed an `addon:<id>` grant, while ANY valid
11
- * hub JWT passed on nothing but a signature check. Measured on the live hub
12
- * 2026-08-09, that let an `oauth-access` token minted for the `export-alexa`
13
- * account link drive `POST /addon/homeassistant-export/command` — PTZ, reboot,
14
- * the per-camera switches and notification snooze. A 60-second authorization
15
- * CODE and a 30-day refresh token passed the same way. See D103.
16
- */
17
- const types_1 = require("@camstack/types");
18
- function readScopes(raw) {
19
- return Array.isArray(raw) ? raw : [];
20
- }
21
- /**
22
- * Decide, and REPORT. A refusal always returns its reason to the log — a branch
23
- * that drops a request silently reads as "never happened", and this one drops
24
- * requests an operator will be trying to explain.
25
- *
26
- * Order is deliberate. `Admin required` is answered before the scope check so
27
- * an admin-only route keeps the response it already had; nothing that is not a
28
- * session or an `oauth-access` token gets that far at all.
29
- */
30
- function gateAddonJwt(input, logger) {
31
- const principal = (0, types_1.classifyBearerPrincipal)(input.payload);
32
- const isAdmin = input.payload.isAdmin === true;
33
- if (principal.kind === 'not-a-credential') {
34
- logger.warn('addon route: refused a bearer that is not an API credential', {
35
- meta: { addonId: input.addonId, method: input.method, reason: principal.reason },
36
- });
37
- return { ok: false, status: 401, error: 'Invalid token' };
38
- }
39
- if (input.access === 'admin' && !isAdmin) {
40
- logger.warn('addon route: refused a non-admin bearer on an admin route', {
41
- meta: { addonId: input.addonId, method: input.method, principal: principal.kind },
42
- });
43
- return { ok: false, status: 403, error: 'Admin required' };
44
- }
45
- const verdict = (0, types_1.principalMayReachAddon)({
46
- principal,
47
- scopes: readScopes(input.payload.scopes),
48
- isAdmin,
49
- addonId: input.addonId,
50
- method: input.method,
51
- });
52
- if (!verdict.allowed) {
53
- logger.warn('addon route: refused an integration token outside its declared grant', {
54
- meta: { addonId: input.addonId, method: input.method, reason: verdict.reason },
55
- });
56
- return { ok: false, status: 403, error: 'Token scope mismatch' };
57
- }
58
- return { ok: true };
59
- }