@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.
@@ -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
- const boot = await core.prepareBootTls(dataPath, extraSans.length > 0 ? { extraSans } : undefined);
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
- setShareTokenService(service) {
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 shareTokens = this.shareTokenService;
1403
- if (shareTokens !== null) {
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 shareKind = coreRecords.kind(record_kinds_js_1.coreAuthRecordKinds['share-view-tokens']);
1407
- shareTokens.bindShareTokensKind(() => shareKind);
1408
- const credentialTokens = this.credentialTokenService;
1409
- if (credentialTokens !== null) {
1410
- const tokenKind = coreRecords.kind(record_kinds_js_1.coreAuthRecordKinds['credential-tokens']);
1411
- credentialTokens.bindKind(() => tokenKind);
1412
- // Dead rows (expired or revoked > 30 days) go at boot; the verifier
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
- void this.write(row.id, { ...row.data, lastUsedAt: at }).catch(() => { });
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.write(id, { ...row.data, revokedAt: this.now() });
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 (a share link's slide). `null` when not live. */
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
- const next = { ...row.data, expiresAt };
217
- await this.write(id, next);
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 (cf. `cst_`/`csv_`). */
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: 'operator mints a share link; validate touches lastUsedAt',
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: 'revoke only; expiry enforced at validate' },
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 only working
442
- // 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).
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 — validates JWT or cst_*
477
- // 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.
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
- // Share-view (`csv_*`) tokens — hub-resident mint/verify/revoke over the
600
- // settings backend (lazy getter: the backend lands after the sqlite-storage
601
- // builtin registers; the service resolves it per call).
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)({ shareTokens: shareTokenService, tokenRows: credentialTokenService });
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, shareTokenService,
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?next=<embed-path> — establish the browser session
1051
- // cookie from a Bearer token, then 302 to the embed. A native WebView can
1052
- // pass `Authorization` only on the INITIAL navigation, not on the page's
1053
- // asset sub-requests; the data-plane `authenticated` gate reads the cookie,
1054
- // so we mint it here (mirroring POST /api/auth/session) and bounce to the
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, 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'));
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.605",
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.441",
42
- "@camstack/addon-agent-ui": "1.2.247",
43
- "@camstack/addon-auth": "1.2.152",
44
- "@camstack/addon-decoder-nodeav": "1.2.126",
45
- "@camstack/addon-notifiers": "1.2.604",
46
- "@camstack/addon-pipeline": "1.2.604",
47
- "@camstack/addon-pipeline-orchestrator": "1.2.605",
48
- "@camstack/addon-post-analysis": "1.2.610",
49
- "@camstack/develop": "0.1.16",
50
- "@camstack/sdk": "1.2.136",
51
- "@camstack/system": "1.2.455",
52
- "@camstack/types": "1.2.386",
53
- "@camstack/ui-library": "1.2.303",
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
- }