@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.
- package/dist/api/auth-whoami.js +27 -43
- package/dist/api/core/auth-audit.procedures.js +44 -0
- package/dist/api/core/auth-device-cert.procedures.js +1 -1
- package/dist/api/core/auth-reauth.procedures.js +1 -1
- package/dist/api/core/auth-revoke-credentials.procedures.js +25 -52
- package/dist/api/core/auth-sessions.procedures.js +1 -1
- package/dist/api/core/auth-tokens.procedures.js +283 -0
- package/dist/api/core/auth.router.js +20 -203
- package/dist/api/core/live-events.router.js +1 -1
- package/dist/api/core/preferences.router.js +98 -0
- package/dist/api/oauth2/oauth2-routes.js +40 -15
- package/dist/api/trpc/device-config-secret-redaction.js +2 -2
- package/dist/api/trpc/generated-cap-mounts.js +0 -1
- package/dist/api/trpc/generated-cap-routers.js +2 -38
- package/dist/api/trpc/principal-authorization.js +82 -0
- package/dist/api/trpc/principal-visibility.js +3 -3
- package/dist/api/trpc/share-view-access.js +1 -1
- package/dist/api/trpc/trpc-error-principal.js +9 -9
- package/dist/api/trpc/trpc.context.js +87 -258
- package/dist/api/trpc/trpc.middleware.js +42 -83
- package/dist/api/trpc/trpc.router.js +7 -1
- package/dist/api/upload-auth.js +19 -54
- package/dist/auth/addon-route-auth.js +44 -0
- package/dist/auth/addon-route-share-gate.js +18 -16
- package/dist/auth/credential-verifier.js +236 -0
- package/dist/auth/embed-auth-route.js +45 -0
- package/dist/auth/live-user.js +19 -0
- package/dist/auth/scope-escalation.js +133 -0
- package/dist/boot/boot-config.js +8 -1
- package/dist/core/addon/addon-registry.service.js +18 -7
- package/dist/core/auth/credential-token.service.js +245 -0
- package/dist/core/auth/handoff-code.service.js +1 -1
- package/dist/core/auth/record-kinds.js +38 -2
- package/dist/main.js +65 -141
- package/package.json +14 -14
- package/dist/api/core/auth-admin-tokens.procedures.js +0 -138
- package/dist/auth/addon-route-jwt-gate.js +0 -59
- 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
|
|
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
|
|
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
|
|
441
|
-
//
|
|
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 —
|
|
476
|
-
//
|
|
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
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
const
|
|
602
|
-
addonRegistry.
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
1043
|
-
// cookie
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
42
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
43
|
-
"@camstack/addon-auth": "1.2.
|
|
44
|
-
"@camstack/addon-decoder-nodeav": "1.2.
|
|
45
|
-
"@camstack/addon-notifiers": "1.2.
|
|
46
|
-
"@camstack/addon-pipeline": "1.2.
|
|
47
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
48
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
49
|
-
"@camstack/develop": "0.1.
|
|
50
|
-
"@camstack/sdk": "1.2.
|
|
51
|
-
"@camstack/system": "1.2.
|
|
52
|
-
"@camstack/types": "1.2.
|
|
53
|
-
"@camstack/ui-library": "1.2.
|
|
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
|
-
}
|