@camstack/server 1.2.605 → 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 +0 -1
- package/dist/api/core/auth-audit.procedures.js +44 -0
- package/dist/api/core/auth-revoke-credentials.procedures.js +4 -78
- package/dist/api/core/auth-tokens.procedures.js +105 -72
- package/dist/api/core/auth.router.js +14 -280
- 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-visibility.js +2 -2
- package/dist/api/trpc/share-view-access.js +1 -1
- package/dist/api/trpc/trpc-error-principal.js +1 -2
- package/dist/api/trpc/trpc.context.js +16 -13
- package/dist/api/trpc/trpc.router.js +6 -1
- package/dist/api/upload-auth.js +13 -25
- package/dist/auth/addon-route-share-gate.js +2 -2
- package/dist/auth/credential-verifier.js +63 -104
- package/dist/auth/embed-auth-route.js +45 -0
- package/dist/auth/live-user.js +19 -0
- package/dist/auth/scope-escalation.js +77 -2
- package/dist/boot/boot-config.js +8 -1
- package/dist/core/addon/addon-registry.service.js +14 -20
- package/dist/core/auth/credential-token.service.js +9 -14
- package/dist/core/auth/handoff-code.service.js +1 -1
- package/dist/core/auth/record-kinds.js +7 -2
- package/dist/main.js +18 -48
- package/package.json +14 -14
- package/dist/api/core/auth-admin-tokens.procedures.js +0 -156
- package/dist/core/auth/share-token.service.js +0 -459
package/dist/boot/boot-config.js
CHANGED
|
@@ -43,6 +43,7 @@ exports.retiredSecretSources = retiredSecretSources;
|
|
|
43
43
|
exports.loadBootstrapConfig = loadBootstrapConfig;
|
|
44
44
|
exports.setupInfra = setupInfra;
|
|
45
45
|
const fs = __importStar(require("node:fs"));
|
|
46
|
+
const os = __importStar(require("node:os"));
|
|
46
47
|
const path = __importStar(require("node:path"));
|
|
47
48
|
const types_1 = require("@camstack/types");
|
|
48
49
|
const yaml = __importStar(require("js-yaml"));
|
|
@@ -241,7 +242,13 @@ async function setupInfra(_configPath, bootstrapConfig) {
|
|
|
241
242
|
// The certificate comes only from the admin UI (D726): the generated
|
|
242
243
|
// local CA → leaf (D227), or an operator upload served verbatim.
|
|
243
244
|
const extraSans = [...core.readExtraSans(dataPath)];
|
|
244
|
-
|
|
245
|
+
// A local root CREATED at this boot is named after the hub. The stored name (D861)
|
|
246
|
+
// lives in the settings database, which is not open yet, so this is the name the
|
|
247
|
+
// hub shows while unnamed: the hostname. An existing root is never renamed.
|
|
248
|
+
const boot = await core.prepareBootTls(dataPath, {
|
|
249
|
+
hubName: (0, types_1.shortHostname)(os.hostname()),
|
|
250
|
+
...(extraSans.length > 0 ? { extraSans } : {}),
|
|
251
|
+
});
|
|
245
252
|
if (boot.uploadedMissing) {
|
|
246
253
|
console.warn(`[Phase2c] WARN the uploaded TLS certificate is missing from ${path.join(dataPath, 'tls')} — serving the generated one. Upload it again in Network → Local access.`);
|
|
247
254
|
}
|
|
@@ -390,7 +390,6 @@ class AddonRegistryService {
|
|
|
390
390
|
integrationRegistry = null;
|
|
391
391
|
clusterNodeHistoryStore = null;
|
|
392
392
|
clusterAgentRecords = null;
|
|
393
|
-
shareTokenService = null;
|
|
394
393
|
credentialTokenService = null;
|
|
395
394
|
// Device architecture — in-memory registry of live IDevice instances.
|
|
396
395
|
// Persistence is owned by the `device-manager` capability addon.
|
|
@@ -1379,10 +1378,7 @@ class AddonRegistryService {
|
|
|
1379
1378
|
const kind = records.kind(system_1.clusterNodeHistoryRecordKind);
|
|
1380
1379
|
clusterStore.bindRecordsKind(() => kind);
|
|
1381
1380
|
}
|
|
1382
|
-
|
|
1383
|
-
this.shareTokenService = service;
|
|
1384
|
-
}
|
|
1385
|
-
/** The unified token store (D892), bound to its `core-auth` records with the share links. */
|
|
1381
|
+
/** The unified token store (D892), bound to its `core-auth` records. */
|
|
1386
1382
|
setCredentialTokenService(service) {
|
|
1387
1383
|
this.credentialTokenService = service;
|
|
1388
1384
|
}
|
|
@@ -1399,24 +1395,22 @@ class AddonRegistryService {
|
|
|
1399
1395
|
await clusterRecords.declare(system_1.clusterAgentRecordKinds);
|
|
1400
1396
|
this.clusterAgentRecords = clusterRecords;
|
|
1401
1397
|
this.bindClusterNodeHistoryRecordsKind();
|
|
1402
|
-
const
|
|
1403
|
-
if (
|
|
1398
|
+
const credentialTokens = this.credentialTokenService;
|
|
1399
|
+
if (credentialTokens !== null) {
|
|
1404
1400
|
const coreRecords = this.recordsForOwner('core-auth', this.secretsForOwner('core-auth'));
|
|
1401
|
+
// Every core-auth kind is declared, `share-view-tokens` included: its
|
|
1402
|
+
// pre-D895 rows stay on disk for the operator to drop, and nothing reads
|
|
1403
|
+
// them.
|
|
1405
1404
|
await coreRecords.declare(record_kinds_js_1.coreAuthRecordKinds);
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
// already refuses them, so this is housekeeping, never a gate.
|
|
1414
|
-
void credentialTokens.reap().catch((error) => {
|
|
1415
|
-
this.logger.warn('Token reap failed', {
|
|
1416
|
-
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
1417
|
-
});
|
|
1405
|
+
const tokenKind = coreRecords.kind(record_kinds_js_1.coreAuthRecordKinds['credential-tokens']);
|
|
1406
|
+
credentialTokens.bindKind(() => tokenKind);
|
|
1407
|
+
// Dead rows (expired or revoked > 30 days) go at boot; the verifier
|
|
1408
|
+
// already refuses them, so this is housekeeping, never a gate.
|
|
1409
|
+
void credentialTokens.reap().catch((error) => {
|
|
1410
|
+
this.logger.warn('Token reap failed', {
|
|
1411
|
+
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
1418
1412
|
});
|
|
1419
|
-
}
|
|
1413
|
+
});
|
|
1420
1414
|
}
|
|
1421
1415
|
}
|
|
1422
1416
|
/**
|
|
@@ -160,7 +160,10 @@ class CredentialTokenService {
|
|
|
160
160
|
return null;
|
|
161
161
|
const lastUsedAt = row.data.lastUsedAt ?? null;
|
|
162
162
|
if (lastUsedAt === null || at - lastUsedAt > TOUCH_EVERY_MS) {
|
|
163
|
-
|
|
163
|
+
// ONLY `lastUsedAt`, merged into the row as it is when the write lands.
|
|
164
|
+
// Writing back the row read above (un-awaited) could land after a
|
|
165
|
+
// revoke / an expiry move and undo it (security review 2026-10-21).
|
|
166
|
+
void kind.patch(row.id, { lastUsedAt: at }).catch(() => { });
|
|
164
167
|
}
|
|
165
168
|
return {
|
|
166
169
|
id: row.id,
|
|
@@ -199,11 +202,11 @@ class CredentialTokenService {
|
|
|
199
202
|
if (row === null || (row.data.revokedAt !== null && row.data.revokedAt !== undefined)) {
|
|
200
203
|
return false;
|
|
201
204
|
}
|
|
202
|
-
await this.
|
|
205
|
+
await this.kind().patch(id, { revokedAt: this.now() });
|
|
203
206
|
this.logger?.info('Token revoked', { meta: { tokenId: id, subjectId: row.data.subjectId } });
|
|
204
207
|
return true;
|
|
205
208
|
}
|
|
206
|
-
/** Move a live token's expiry (
|
|
209
|
+
/** Move a live token's expiry (`null`: never). `null` when not live. */
|
|
207
210
|
async setExpiry(id, expiresAt) {
|
|
208
211
|
const row = await this.kind().get(id);
|
|
209
212
|
if (row === null)
|
|
@@ -211,11 +214,10 @@ class CredentialTokenService {
|
|
|
211
214
|
const at = this.now();
|
|
212
215
|
const dead = (row.data.revokedAt !== null && row.data.revokedAt !== undefined) ||
|
|
213
216
|
(row.data.expiresAt !== null && row.data.expiresAt <= at);
|
|
214
|
-
if (dead || expiresAt <= at)
|
|
217
|
+
if (dead || (expiresAt !== null && expiresAt <= at))
|
|
215
218
|
return null;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return toRecord(id, next);
|
|
219
|
+
await this.kind().patch(id, { expiresAt }, { expiresAt });
|
|
220
|
+
return toRecord(id, { ...row.data, expiresAt });
|
|
219
221
|
}
|
|
220
222
|
/** Revoke every live token of `subjectId` (the user was deleted). */
|
|
221
223
|
async revokeAllFor(subjectId) {
|
|
@@ -239,12 +241,5 @@ class CredentialTokenService {
|
|
|
239
241
|
}
|
|
240
242
|
return deleted;
|
|
241
243
|
}
|
|
242
|
-
async write(id, body) {
|
|
243
|
-
await this.kind().put({
|
|
244
|
-
id,
|
|
245
|
-
...(body.expiresAt !== null ? { expiresAt: body.expiresAt } : {}),
|
|
246
|
-
data: body,
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
244
|
}
|
|
250
245
|
exports.CredentialTokenService = CredentialTokenService;
|
|
@@ -60,7 +60,7 @@ const types_1 = require("@camstack/types");
|
|
|
60
60
|
* prune-on-mint keeps the map from growing.
|
|
61
61
|
*/
|
|
62
62
|
const crypto = __importStar(require("node:crypto"));
|
|
63
|
-
/** Wire prefix — `chc_` = CamStack Handoff Code (
|
|
63
|
+
/** Wire prefix — `chc_` = CamStack Handoff Code (not a credential: it is redeemed once for a session). */
|
|
64
64
|
exports.HANDOFF_CODE_PREFIX = 'chc_';
|
|
65
65
|
exports.HANDOFF_CODE_TTL_MS = 60_000;
|
|
66
66
|
/** Hard bound on concurrently-pending codes (mint is auth-gated, so this
|
|
@@ -12,6 +12,11 @@ const ShareTokenScopeSchema = zod_1.z.object({
|
|
|
12
12
|
talk: zod_1.z.boolean().optional(),
|
|
13
13
|
});
|
|
14
14
|
exports.SHARE_VIEW_TOKENS_LEGACY = 'share_view_tokens';
|
|
15
|
+
/**
|
|
16
|
+
* The pre-D892 share-link rows. Nothing writes or reads them since D895 (a
|
|
17
|
+
* share link is a `credential-tokens` row); the kind stays declared so the
|
|
18
|
+
* storage layout is unchanged and the operator drops the rows by hand.
|
|
19
|
+
*/
|
|
15
20
|
const ShareViewTokenBodySchema = zod_1.z.object({
|
|
16
21
|
userId: zod_1.z.string(),
|
|
17
22
|
tokenHash: zod_1.z.string(),
|
|
@@ -41,14 +46,14 @@ exports.coreAuthRecordKinds = (0, types_1.defineRecordKinds)({
|
|
|
41
46
|
'share-view-tokens': {
|
|
42
47
|
schema: ShareViewTokenBodySchema,
|
|
43
48
|
class: 'config',
|
|
44
|
-
writeTrigger: '
|
|
49
|
+
writeTrigger: 'none since D895 — legacy rows kept on disk for the operator to drop',
|
|
45
50
|
device: 'none',
|
|
46
51
|
at: 'none',
|
|
47
52
|
indexes: [
|
|
48
53
|
{ name: 'hash', fields: ['tokenHash'], unique: true },
|
|
49
54
|
{ name: 'user', fields: ['userId'] },
|
|
50
55
|
],
|
|
51
|
-
retention: { none: '
|
|
56
|
+
retention: { none: 'operator drops the legacy rows by hand (D895)' },
|
|
52
57
|
adoptsFrom: exports.SHARE_VIEW_TOKENS_LEGACY,
|
|
53
58
|
},
|
|
54
59
|
'credential-tokens': {
|
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");
|
|
@@ -122,7 +124,6 @@ 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
126
|
const credential_token_service_js_1 = require("./core/auth/credential-token.service.js");
|
|
125
|
-
const share_token_service_1 = require("./core/auth/share-token.service");
|
|
126
127
|
const config_service_1 = require("./core/config/config.service");
|
|
127
128
|
const event_bus_service_1 = require("./core/events/event-bus.service");
|
|
128
129
|
const feature_service_1 = require("./core/feature/feature.service");
|
|
@@ -438,8 +439,8 @@ async function bootstrap() {
|
|
|
438
439
|
const sessionLifetimes = () => (0, session_lifetimes_js_1.readSessionLifetimes)((key) => config.get(key));
|
|
439
440
|
// Register addon upload route (multipart — must be registered before tRPC).
|
|
440
441
|
// We pass AddonRegistryService so the handler can resolve the
|
|
441
|
-
// `user-management` cap singleton at request time (the
|
|
442
|
-
//
|
|
442
|
+
// `user-management` cap singleton at request time (the credential
|
|
443
|
+
// verifier reads users and admin bits there — see upload-auth.ts).
|
|
443
444
|
try {
|
|
444
445
|
const uploadAuthService = app.get(auth_service_1.AuthService);
|
|
445
446
|
const uploadAddonBridge = app.get(addon_bridge_service_1.AddonBridgeService);
|
|
@@ -473,8 +474,8 @@ async function bootstrap() {
|
|
|
473
474
|
.getSingleton('local-network')),
|
|
474
475
|
});
|
|
475
476
|
console.log('[bootstrap] Server upload propagate route registered at POST /api/server-upload/propagate');
|
|
476
|
-
// Companion endpoint: /api/auth/whoami —
|
|
477
|
-
//
|
|
477
|
+
// Companion endpoint: /api/auth/whoami — verifies any hub credential
|
|
478
|
+
// through the one verifier, returns the resolved identity + scopes.
|
|
478
479
|
// Mirrors the addon-upload auth chain so the CLI can ping for
|
|
479
480
|
// token-still-valid without consuming a real cap.
|
|
480
481
|
await (0, auth_whoami_1.registerAuthWhoamiRoute)(fastify, uploadAuthService, uploadAddonRegistry);
|
|
@@ -596,17 +597,13 @@ async function bootstrap() {
|
|
|
596
597
|
// and the distributed metrics-provider capability — no manual wiring needed.
|
|
597
598
|
let trpcRegistered = false;
|
|
598
599
|
let appRouter = null;
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
const shareTokenService = new share_token_service_1.ShareTokenService(() => addonRegistry.getSettingsBackend(), loggingService.createLogger('share-tokens'));
|
|
603
|
-
addonRegistry.setShareTokenService(shareTokenService);
|
|
604
|
-
// The unified token store (D892): rows in the `core-auth` records, bound
|
|
605
|
-
// with the share links; the JWT is signed with the session signing key.
|
|
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.
|
|
606
603
|
const credentialTokenService = new credential_token_service_js_1.CredentialTokenService(app.get(auth_service_1.AuthService), loggingService.createLogger('tokens'));
|
|
607
604
|
addonRegistry.setCredentialTokenService(credentialTokenService);
|
|
608
605
|
// Every surface that verifies a bearer reads the same stores (D892).
|
|
609
|
-
(0, trpc_context_1.bindHubCredentialStores)({
|
|
606
|
+
(0, trpc_context_1.bindHubCredentialStores)({ tokenRows: credentialTokenService });
|
|
610
607
|
// One-time native-app login handoff codes (in-memory, 60s TTL, single-use) —
|
|
611
608
|
// minted by `auth.createHandoffCode`, redeemed by the viewer app via the
|
|
612
609
|
// public `auth.redeemHandoffCode`.
|
|
@@ -638,7 +635,6 @@ async function bootstrap() {
|
|
|
638
635
|
});
|
|
639
636
|
const buildRouter = () => (0, trpc_router_1.buildAppRouter)({
|
|
640
637
|
authService: app.get(auth_service_1.AuthService),
|
|
641
|
-
shareTokenService,
|
|
642
638
|
credentialTokenService,
|
|
643
639
|
handoffCodeService,
|
|
644
640
|
externalLinkIntents,
|
|
@@ -808,7 +804,7 @@ async function bootstrap() {
|
|
|
808
804
|
trpcOptions: {
|
|
809
805
|
router: appRouter,
|
|
810
806
|
createContext: async ({ req }) => {
|
|
811
|
-
const trpcCtx = await (0, trpc_context_1.createTrpcContext)(req, authService, addonRegistry,
|
|
807
|
+
const trpcCtx = await (0, trpc_context_1.createTrpcContext)(req, authService, addonRegistry,
|
|
812
808
|
// A bearer refused for NOT being an API credential never reaches a
|
|
813
809
|
// procedure, so `onError` below never sees it — the refusal would
|
|
814
810
|
// be silent without this. Same reason the addon gate logs (D103).
|
|
@@ -1047,37 +1043,11 @@ async function bootstrap() {
|
|
|
1047
1043
|
reply.setCookie(c.name, c.value, c.options);
|
|
1048
1044
|
return reply.send({ ok: true });
|
|
1049
1045
|
});
|
|
1050
|
-
// GET /api/embed-auth
|
|
1051
|
-
// cookie
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
// embed — page + assets then authenticate via the cookie. `next` is
|
|
1056
|
-
// restricted to the stream-broker embed prefix (no open redirect).
|
|
1057
|
-
fastify.get('/api/embed-auth', async (request, reply) => {
|
|
1058
|
-
const authHeader = request.headers.authorization;
|
|
1059
|
-
const token = authHeader?.startsWith('Bearer ')
|
|
1060
|
-
? authHeader.slice('Bearer '.length)
|
|
1061
|
-
: undefined;
|
|
1062
|
-
if (!token)
|
|
1063
|
-
return reply.status(401).send({ error: 'Bearer token required' });
|
|
1064
|
-
const next = request.query?.next ?? '';
|
|
1065
|
-
if (!(0, session_cookie_js_1.isEmbedRedirectTarget)(next))
|
|
1066
|
-
return reply.status(400).send({ error: 'invalid next' });
|
|
1067
|
-
let ttlSec;
|
|
1068
|
-
try {
|
|
1069
|
-
const payload = authService.verifyToken(token);
|
|
1070
|
-
const expSec = typeof payload.exp === 'number' ? payload.exp : 0;
|
|
1071
|
-
ttlSec = Math.max(0, expSec - Math.floor(Date.now() / 1000));
|
|
1072
|
-
}
|
|
1073
|
-
catch {
|
|
1074
|
-
return reply.status(401).send({ error: 'invalid token' });
|
|
1075
|
-
}
|
|
1076
|
-
const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, {
|
|
1077
|
-
secure: (0, session_cookie_js_1.sessionCookieIsSecure)(request.protocol, request.headers, spa_mount_1.FORWARDED_PREFIX_HEADER),
|
|
1078
|
-
});
|
|
1079
|
-
reply.setCookie(c.name, c.value, c.options);
|
|
1080
|
-
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),
|
|
1081
1051
|
});
|
|
1082
1052
|
// Every refusal on the addon gate is a request an operator will be asking
|
|
1083
1053
|
// about ("why did Home Assistant stop actuating?"). Its own scope so the
|
|
@@ -1086,7 +1056,6 @@ async function bootstrap() {
|
|
|
1086
1056
|
const addonVerifier = (0, trpc_context_1.hubCredentialVerifier)({
|
|
1087
1057
|
authService,
|
|
1088
1058
|
addonRegistry,
|
|
1089
|
-
shareTokens: shareTokenService,
|
|
1090
1059
|
logger: addonGateLogger,
|
|
1091
1060
|
});
|
|
1092
1061
|
// Addon HTTP API route catch-all: /addon/:addonId/*
|
|
@@ -1255,6 +1224,7 @@ async function bootstrap() {
|
|
|
1255
1224
|
(0, oauth2_routes_js_1.registerOauth2Routes)(fastify, {
|
|
1256
1225
|
getRegistry: () => capabilityRegistry,
|
|
1257
1226
|
verifyToken: (t) => authService.verifyToken(t),
|
|
1227
|
+
isLiveUser: (0, live_user_js_1.liveUserCheck)(capabilityRegistry),
|
|
1258
1228
|
// The hub's public endpoint from the `local-network` cap (the flagged
|
|
1259
1229
|
// address, else tunnel, then mesh, then LAN). `localhost` only when the
|
|
1260
1230
|
// palette names nothing — the dev case, where it is the truth.
|
|
@@ -1341,7 +1311,7 @@ async function bootstrap() {
|
|
|
1341
1311
|
// A termination here closes with 1006; the guard's close line names it.
|
|
1342
1312
|
keepAlive: { enabled: true, pingMs: 30_000, pongWaitMs: 10_000 },
|
|
1343
1313
|
createContext: async (opts) => {
|
|
1344
|
-
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'));
|
|
1345
1315
|
// The connection's identity, derived ONCE and reused for every
|
|
1346
1316
|
// operation on this socket. Frames that arrived while the bearer was
|
|
1347
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,156 +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 auth_tokens_procedures_js_1 = require("./auth-tokens.procedures.js");
|
|
19
|
-
const types_1 = require("@camstack/types");
|
|
20
|
-
const DAY_MS = 86_400_000;
|
|
21
|
-
exports.ADMIN_TOKEN_DEFAULT_DAYS = 7;
|
|
22
|
-
exports.ADMIN_TOKEN_MAX_DAYS = system_1.ADMIN_TOKEN_MAX_LIFETIME_MS / DAY_MS;
|
|
23
|
-
exports.AdminTokenSchema = zod_1.z.object({
|
|
24
|
-
id: zod_1.z.string(),
|
|
25
|
-
createdBy: zod_1.z.string(),
|
|
26
|
-
name: zod_1.z.string(),
|
|
27
|
-
tokenPrefix: zod_1.z.string(),
|
|
28
|
-
createdAt: zod_1.z.number(),
|
|
29
|
-
expiresAt: zod_1.z.number(),
|
|
30
|
-
lastUsedAt: zod_1.z.number().nullable(),
|
|
31
|
-
revokedAt: zod_1.z.number().nullable(),
|
|
32
|
-
/** A pre-D892 `csa_` token: valid until it expires, never minted again. */
|
|
33
|
-
legacy: zod_1.z.boolean().optional(),
|
|
34
|
-
});
|
|
35
|
-
/** A unified `'*'` token, in the admin-token listing's shape. */
|
|
36
|
-
function adminSummaryOf(record, expiresAt) {
|
|
37
|
-
return {
|
|
38
|
-
id: record.id,
|
|
39
|
-
createdBy: record.createdBy,
|
|
40
|
-
name: record.label,
|
|
41
|
-
tokenPrefix: `token:${record.id.slice(0, 8)}`,
|
|
42
|
-
createdAt: record.createdAt,
|
|
43
|
-
expiresAt,
|
|
44
|
-
lastUsedAt: record.lastUsedAt,
|
|
45
|
-
revokedAt: record.revokedAt,
|
|
46
|
-
legacy: false,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
exports.AuthAuditRecordSchema = zod_1.z.object({
|
|
50
|
-
id: zod_1.z.string(),
|
|
51
|
-
at: zod_1.z.number(),
|
|
52
|
-
action: zod_1.z.string(),
|
|
53
|
-
userId: zod_1.z.string().optional(),
|
|
54
|
-
ip: zod_1.z.string().optional(),
|
|
55
|
-
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(),
|
|
56
|
-
});
|
|
57
|
-
function requireInternal(registry) {
|
|
58
|
-
const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
|
|
59
|
-
if (internal === null) {
|
|
60
|
-
throw new server_1.TRPCError({
|
|
61
|
-
code: 'PRECONDITION_FAILED',
|
|
62
|
-
message: 'User management is not available',
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return internal;
|
|
66
|
-
}
|
|
67
|
-
function createAdminTokenProcedures(deps) {
|
|
68
|
-
return {
|
|
69
|
-
/**
|
|
70
|
-
* ADMIN — mint an admin token for an ops script. Requires a real
|
|
71
|
-
* signed-in session (an admin token cannot mint another) and a fresh
|
|
72
|
-
* re-authentication. Expiry mandatory: 1–90 days, default 7. The token
|
|
73
|
-
* is in this answer only.
|
|
74
|
-
*/
|
|
75
|
-
createAdminToken: trpc_middleware_js_1.adminProcedure
|
|
76
|
-
.input(zod_1.z.object({
|
|
77
|
-
name: zod_1.z.string().trim().min(1).max(80),
|
|
78
|
-
expiresInDays: zod_1.z
|
|
79
|
-
.number()
|
|
80
|
-
.int()
|
|
81
|
-
.min(1)
|
|
82
|
-
.max(exports.ADMIN_TOKEN_MAX_DAYS)
|
|
83
|
-
.default(exports.ADMIN_TOKEN_DEFAULT_DAYS),
|
|
84
|
-
reauthToken: auth_reauth_procedures_js_1.ReauthTokenInput.optional(),
|
|
85
|
-
}))
|
|
86
|
-
.output(zod_1.z.object({ token: zod_1.z.string(), record: exports.AdminTokenSchema }))
|
|
87
|
-
.mutation(async ({ ctx, input }) => {
|
|
88
|
-
if (ctx.user.interactive !== true) {
|
|
89
|
-
throw new server_1.TRPCError({
|
|
90
|
-
code: 'FORBIDDEN',
|
|
91
|
-
message: 'An admin token can only be created from a signed-in admin session',
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
// A thin wrapper for one release (D892): the token is a unified one
|
|
95
|
-
// holding `'*'`, minted — re-authentication included — by the one mint.
|
|
96
|
-
const minted = await (0, auth_tokens_procedures_js_1.mintFor)(deps, ctx, {
|
|
97
|
-
scopes: [types_1.ADMIN_SCOPE],
|
|
98
|
-
expiresIn: (0, auth_tokens_procedures_js_1.daysToSeconds)(input.expiresInDays),
|
|
99
|
-
label: input.name,
|
|
100
|
-
...(input.reauthToken !== undefined ? { reauthToken: input.reauthToken } : {}),
|
|
101
|
-
});
|
|
102
|
-
const expiresAt = minted.record.expiresAt ?? Date.now() + input.expiresInDays * DAY_MS;
|
|
103
|
-
return { token: minted.token, record: adminSummaryOf(minted.record, expiresAt) };
|
|
104
|
-
}),
|
|
105
|
-
/** ADMIN — every admin token not yet reaped (no secrets). */
|
|
106
|
-
listAdminTokens: trpc_middleware_js_1.adminProcedure
|
|
107
|
-
.input(zod_1.z.void())
|
|
108
|
-
.output(zod_1.z.array(exports.AdminTokenSchema))
|
|
109
|
-
.query(async () => {
|
|
110
|
-
const unified = (deps.credentialTokens === null ? [] : await deps.credentialTokens.list())
|
|
111
|
-
.filter((t) => (0, types_1.scopesGrantAdmin)(t.scopes) && t.expiresAt !== null)
|
|
112
|
-
.map((t) => adminSummaryOf(t, t.expiresAt ?? 0));
|
|
113
|
-
const legacy = (await requireInternal(deps.registry).adminTokens.list()).map((t) => ({
|
|
114
|
-
...t,
|
|
115
|
-
legacy: true,
|
|
116
|
-
}));
|
|
117
|
-
return [...unified, ...legacy];
|
|
118
|
-
}),
|
|
119
|
-
/** ADMIN — revoke one admin token; it stops working at once. */
|
|
120
|
-
revokeAdminToken: trpc_middleware_js_1.adminProcedure
|
|
121
|
-
.input(zod_1.z.object({ id: zod_1.z.string().min(1) }))
|
|
122
|
-
.output(zod_1.z.object({ success: zod_1.z.boolean() }))
|
|
123
|
-
.mutation(async ({ ctx, input }) => {
|
|
124
|
-
const internal = requireInternal(deps.registry);
|
|
125
|
-
const unified = deps.credentialTokens === null ? null : await deps.credentialTokens.get(input.id);
|
|
126
|
-
const success = unified !== null && deps.credentialTokens !== null
|
|
127
|
-
? await deps.credentialTokens.revoke(input.id)
|
|
128
|
-
: await internal.adminTokens.revoke(input.id);
|
|
129
|
-
if (success) {
|
|
130
|
-
const ip = deps.network.clientAddress(ctx.req).ip;
|
|
131
|
-
await internal.audit.record({
|
|
132
|
-
action: 'admin-token-revoked',
|
|
133
|
-
userId: ctx.user.id,
|
|
134
|
-
...(ip !== null ? { ip } : {}),
|
|
135
|
-
detail: { tokenId: input.id },
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
return { success };
|
|
139
|
-
}),
|
|
140
|
-
/** ADMIN — the authentication audit trail, newest first. */
|
|
141
|
-
listAuthAudit: trpc_middleware_js_1.adminProcedure
|
|
142
|
-
.input(zod_1.z
|
|
143
|
-
.object({
|
|
144
|
-
limit: zod_1.z.number().int().min(1).max(500).optional(),
|
|
145
|
-
userId: zod_1.z.string().min(1).optional(),
|
|
146
|
-
})
|
|
147
|
-
.optional())
|
|
148
|
-
.output(zod_1.z.array(exports.AuthAuditRecordSchema))
|
|
149
|
-
.query(async ({ input }) => [
|
|
150
|
-
...(await requireInternal(deps.registry).audit.list({
|
|
151
|
-
...(input?.limit !== undefined ? { limit: input.limit } : {}),
|
|
152
|
-
...(input?.userId !== undefined ? { userId: input.userId } : {}),
|
|
153
|
-
})),
|
|
154
|
-
]),
|
|
155
|
-
};
|
|
156
|
-
}
|