@camstack/server 1.2.603 → 1.2.605

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.
@@ -37,6 +37,7 @@ const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
37
37
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
38
38
  const auth_limits_js_1 = require("./auth-limits.js");
39
39
  const auth_admin_tokens_procedures_js_1 = require("./auth-admin-tokens.procedures.js");
40
+ const auth_tokens_procedures_js_1 = require("./auth-tokens.procedures.js");
40
41
  const auth_revoke_credentials_procedures_js_1 = require("./auth-revoke-credentials.procedures.js");
41
42
  const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
42
43
  const auth_device_cert_procedures_js_1 = require("./auth-device-cert.procedures.js");
@@ -199,6 +200,8 @@ const ShareTokenSummarySchema = zod_1.z.object({
199
200
  /** Derived convenience flag so listings surface infinite tokens clearly. */
200
201
  neverExpires: zod_1.z.boolean(),
201
202
  lastUsedAt: zod_1.z.number().optional(),
203
+ /** A pre-D892 `csv_` link: valid until it expires, never minted again. */
204
+ legacy: zod_1.z.boolean().optional(),
202
205
  });
203
206
  function toShareTokenSummary(record) {
204
207
  return {
@@ -219,9 +222,10 @@ function toShareTokenSummary(record) {
219
222
  * a leaked restricted token would otherwise widen its own reach.
220
223
  */
221
224
  function assertRealUserSession(user, what = 'Share-token management') {
222
- // An admin token is a script's credential, never a person's session: it
223
- // must not turn itself into a session (handoff codes) or mint links.
224
- if (user.isScoped || user.shareView || user.adminTokenId !== undefined) {
225
+ // A minted token (admin, scoped, share — legacy or unified) is a script's
226
+ // credential, never a person's: it must not turn itself into a session
227
+ // (handoff codes) or mint links. Provenance, not permission (D892).
228
+ if (user.userDelegated !== true) {
225
229
  throw new server_1.TRPCError({
226
230
  code: 'FORBIDDEN',
227
231
  message: `${what} requires a real user session`,
@@ -304,6 +308,15 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
304
308
  const credentialProcedure = makeRateLimitedProcedure(limiters.credential, network);
305
309
  /** Challenge-minting begin-legs — looser, they verify nothing. */
306
310
  const ceremonyProcedure = makeRateLimitedProcedure(limiters.ceremony, network);
311
+ const credentialTokens = options.credentialTokens ?? null;
312
+ const tokenDeps = { auth, registry, network, credentialTokens, shareTokens };
313
+ /** The unified share link behind `id`, or `null` (a legacy `csv_`, or none). */
314
+ const unifiedShare = async (id) => {
315
+ if (credentialTokens === null)
316
+ return null;
317
+ const record = await credentialTokens.get(id);
318
+ return record !== null && (0, types_2.shareViewGrantOf)(record.scopes) !== null ? record : null;
319
+ };
307
320
  const requireShareTokens = () => {
308
321
  if (!shareTokens) {
309
322
  throw new Error('Share tokens unavailable — service not wired on this node');
@@ -957,7 +970,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
957
970
  // hide surfaces from a single probe (F1 #5). Admins summarise to
958
971
  // "everything"; a scoped principal's selectors expand to counts.
959
972
  const fleet = ctx.deviceFleet?.() ?? [];
960
- const scope = (0, types_1.summarizeEffectiveScope)(ctx.user.scopes ?? [], fleet, ctx.user.isAdmin);
973
+ const scope = (0, types_1.summarizeEffectiveScope)((0, types_2.capabilityScopesOf)(ctx.user.scopes ?? []), fleet, ctx.user.isAdmin);
961
974
  return { ...ctx.user, scope };
962
975
  }),
963
976
  /**
@@ -1188,7 +1201,6 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1188
1201
  .output(zod_1.z.object({ id: zod_1.z.string(), token: zod_1.z.string(), expiresAt: zod_1.z.number().nullable() }))
1189
1202
  .mutation(async ({ input, ctx }) => {
1190
1203
  assertRealUserSession(ctx.user);
1191
- const service = requireShareTokens();
1192
1204
  // SECURITY (F0.4): a share token is a PRIVILEGE HANDOFF — it authorises
1193
1205
  // whoever holds the link to view its devices with no further auth. A
1194
1206
  // scoped (non-admin) minter must therefore not be able to mint for a
@@ -1202,7 +1214,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1202
1214
  ? input.scope
1203
1215
  : {
1204
1216
  ...input.scope,
1205
- deviceIds: (0, principal_visibility_js_1.filterViewableDeviceIds)(ctx.user.scopes ?? [], input.scope.deviceIds, ctx.deviceScopeLookup),
1217
+ deviceIds: (0, principal_visibility_js_1.filterViewableDeviceIds)((0, types_2.capabilityScopesOf)(ctx.user.scopes ?? []), input.scope.deviceIds, ctx.deviceScopeLookup),
1206
1218
  };
1207
1219
  if (scope.deviceIds.length === 0) {
1208
1220
  throw new server_1.TRPCError({
@@ -1210,7 +1222,20 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1210
1222
  message: 'None of the requested devices are within your scope — you cannot mint a share token for devices you cannot see',
1211
1223
  });
1212
1224
  }
1213
- const { token, record } = await service.create({
1225
+ // A thin wrapper for one release (D892): the link is a unified token
1226
+ // whose only scope is the share-view grant. Without the store (a
1227
+ // node that cannot mint) it is still a legacy `csv_`.
1228
+ if (credentialTokens !== null) {
1229
+ const ttl = input.ttlSec ?? share_token_service_js_1.SHARE_TOKEN_TTL_DEFAULT_SEC;
1230
+ const minted = await (0, auth_tokens_procedures_js_1.mintFor)(tokenDeps, ctx, {
1231
+ scopes: [{ type: 'share-view', grant: scope }],
1232
+ expiresIn: ttl,
1233
+ label: `share ${scope.kind}`,
1234
+ allowAccountLink: true,
1235
+ });
1236
+ return { id: minted.record.id, token: minted.token, expiresAt: minted.record.expiresAt };
1237
+ }
1238
+ const { token, record } = await requireShareTokens().create({
1214
1239
  userId: ctx.user.id,
1215
1240
  scope,
1216
1241
  ...(input.ttlSec !== undefined ? { ttlSec: input.ttlSec } : {}),
@@ -1230,6 +1255,23 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1230
1255
  .output(zod_1.z.object({ id: zod_1.z.string(), expiresAt: zod_1.z.number() }))
1231
1256
  .mutation(async ({ input, ctx }) => {
1232
1257
  assertRealUserSession(ctx.user);
1258
+ const unified = await unifiedShare(input.id);
1259
+ if (unified !== null && credentialTokens !== null) {
1260
+ if (!ctx.user.isAdmin && unified.subjectId !== ctx.user.id) {
1261
+ throw new server_1.TRPCError({
1262
+ code: 'FORBIDDEN',
1263
+ message: 'Only the token owner or an admin can extend a share token',
1264
+ });
1265
+ }
1266
+ if (unified.expiresAt === null) {
1267
+ throw (0, user_error_js_1.trpcUserError)('BAD_REQUEST', 'auth.share-never-expiring-extend', undefined, 'A never-expiring share token cannot be extended');
1268
+ }
1269
+ const moved = await credentialTokens.setExpiry(input.id, Date.now() + input.ttlSec * 1000);
1270
+ if (moved === null || moved.expiresAt === null) {
1271
+ throw new server_1.TRPCError({ code: 'NOT_FOUND', message: `Share token expired: ${input.id}` });
1272
+ }
1273
+ return { id: moved.id, expiresAt: moved.expiresAt };
1274
+ }
1233
1275
  const service = requireShareTokens();
1234
1276
  try {
1235
1277
  const record = await service.extend({
@@ -1259,6 +1301,16 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1259
1301
  .output(zod_1.z.object({ success: zod_1.z.boolean() }))
1260
1302
  .mutation(async ({ input, ctx }) => {
1261
1303
  assertRealUserSession(ctx.user);
1304
+ const unified = await unifiedShare(input.id);
1305
+ if (unified !== null && credentialTokens !== null) {
1306
+ if (!ctx.user.isAdmin && unified.subjectId !== ctx.user.id) {
1307
+ throw new server_1.TRPCError({
1308
+ code: 'FORBIDDEN',
1309
+ message: 'Only the token owner or an admin can revoke a share token',
1310
+ });
1311
+ }
1312
+ return { success: await credentialTokens.revoke(input.id) };
1313
+ }
1262
1314
  const service = requireShareTokens();
1263
1315
  try {
1264
1316
  const removed = await service.revoke({
@@ -1303,7 +1355,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1303
1355
  .output(zod_1.z.array(ShareTokenSummarySchema))
1304
1356
  .query(async ({ ctx, input }) => {
1305
1357
  assertRealUserSession(ctx.user);
1306
- const service = requireShareTokens();
1358
+ // The legacy `csv_` store, or — on a node with only the unified store —
1359
+ // none; with neither, the clear refusal below.
1360
+ const service = credentialTokens === null ? requireShareTokens() : shareTokens;
1307
1361
  const owner = input?.owner ?? (ctx.user.isAdmin ? 'all' : 'self');
1308
1362
  if (owner === 'all' && !ctx.user.isAdmin) {
1309
1363
  throw new server_1.TRPCError({
@@ -1311,8 +1365,36 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1311
1365
  message: "Listing every user's share links requires an admin",
1312
1366
  });
1313
1367
  }
1314
- const records = owner === 'all' ? await service.listAll() : await service.listForUser(ctx.user.id);
1315
- return records.map(toShareTokenSummary);
1368
+ const records = service === null
1369
+ ? []
1370
+ : owner === 'all'
1371
+ ? await service.listAll()
1372
+ : await service.listForUser(ctx.user.id);
1373
+ const unified = credentialTokens === null
1374
+ ? []
1375
+ : await credentialTokens.list(owner === 'all' ? undefined : ctx.user.id);
1376
+ const unifiedRows = unified.flatMap((record) => {
1377
+ const grant = (0, types_2.shareViewGrantOf)(record.scopes);
1378
+ if (grant === null || record.revokedAt !== null)
1379
+ return [];
1380
+ return [
1381
+ {
1382
+ id: record.id,
1383
+ userId: record.subjectId,
1384
+ tokenPrefix: `token:${record.id.slice(0, 8)}`,
1385
+ scope: grant,
1386
+ createdAt: record.createdAt,
1387
+ expiresAt: record.expiresAt,
1388
+ neverExpires: record.expiresAt === null,
1389
+ ...(record.lastUsedAt !== null ? { lastUsedAt: record.lastUsedAt } : {}),
1390
+ legacy: false,
1391
+ },
1392
+ ];
1393
+ });
1394
+ return [
1395
+ ...unifiedRows,
1396
+ ...records.map((record) => ({ ...toShareTokenSummary(record), legacy: true })),
1397
+ ];
1316
1398
  }),
1317
1399
  ...(0, auth_sessions_procedures_js_1.createSessionProcedures)({ registry, minter, credentialProcedure }),
1318
1400
  ...(0, auth_device_cert_procedures_js_1.createDeviceCertProcedures)({
@@ -1331,8 +1413,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1331
1413
  issueFor,
1332
1414
  }),
1333
1415
  ...(0, auth_reauth_procedures_js_1.createReauthProcedures)({ auth, registry, moleculer, limiters }),
1334
- ...(0, auth_admin_tokens_procedures_js_1.createAdminTokenProcedures)({ auth, registry, network }),
1335
- ...(0, auth_revoke_credentials_procedures_js_1.createRevokeCredentialsProcedures)({ registry, shareTokens, network }),
1416
+ ...(0, auth_admin_tokens_procedures_js_1.createAdminTokenProcedures)({ auth, registry, network, credentialTokens, shareTokens }),
1417
+ ...(0, auth_tokens_procedures_js_1.createTokenProcedures)({ auth, registry, network, credentialTokens, shareTokens }),
1418
+ ...(0, auth_revoke_credentials_procedures_js_1.createRevokeCredentialsProcedures)({ registry, shareTokens, network, credentialTokens }),
1336
1419
  // ── External sign-in identities (D782) ────────────────────────
1337
1420
  //
1338
1421
  // A signed-in user links an OIDC identity to THEIR OWN account. The
@@ -45,7 +45,7 @@ function createLiveEventsRouter(eb, ar) {
45
45
  // Share-view principals get a device-filtered stream: only events
46
46
  // attributable to an in-scope device are pushed (fail closed —
47
47
  // unattributable events are dropped).
48
- const shareScope = ctx.user.shareView?.scope ?? null;
48
+ const shareScope = (0, types_1.shareViewGrantOf)(ctx.user.scopes ?? []);
49
49
  // Non-share scoped tokens get the SAME projection keyed to what they may
50
50
  // view (F0.5); admins and broad category-view operators are unfiltered.
51
51
  const scopedKeep = shareScope
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authorizeProcedure = authorizeProcedure;
4
+ exports.authorizeAddonRoute = authorizeAddonRoute;
5
+ /**
6
+ * The ONE scope matcher (D892): every authorization decision about a verified
7
+ * principal is a function of `principal.scopes`, here. It answers for a tRPC
8
+ * procedure and for an addon's own HTTP surface (routes and data planes);
9
+ * neither asks what kind of credential the principal came from.
10
+ *
11
+ * Order, the same on both surfaces:
12
+ * 1. a `share-view` grant RESTRICTS the principal to its fail-closed
13
+ * perimeter — nothing else it holds is honoured;
14
+ * 2. `'*'` passes;
15
+ * 3. the capability / addon grants decide.
16
+ */
17
+ const system_1 = require("@camstack/system");
18
+ const types_1 = require("@camstack/types");
19
+ const addon_route_share_gate_js_1 = require("../../auth/addon-route-share-gate.js");
20
+ const scope_access_js_1 = require("./scope-access.js");
21
+ const share_view_access_js_1 = require("./share-view-access.js");
22
+ /**
23
+ * May a principal holding `scopes` call the tRPC procedure at `path`?
24
+ *
25
+ * A hand-written core route (`auth.me`, `system.info`) has no cap entry: the
26
+ * authentication already passed, and an explicit `adminProcedure` on top of it
27
+ * gates what is destructive. A share-view principal never reaches that
28
+ * pass-through — its allowlist is enumerated.
29
+ */
30
+ function authorizeProcedure(scopes, path, rawInput, lookup) {
31
+ const grant = (0, types_1.shareViewGrantOf)(scopes);
32
+ if (grant !== null) {
33
+ const result = (0, share_view_access_js_1.checkShareViewAccess)(grant, path, rawInput);
34
+ return result.ok ? { ok: true, mode: 'share', grant } : { ok: false, reason: result.reason };
35
+ }
36
+ if ((0, types_1.scopesGrantAdmin)(scopes))
37
+ return { ok: true, mode: 'all' };
38
+ const capability = (0, types_1.capabilityScopesOf)(scopes);
39
+ if (path in system_1.METHOD_ACCESS_MAP) {
40
+ const result = (0, scope_access_js_1.checkScopeAccess)(capability, path, rawInput, lookup);
41
+ if (!result.ok)
42
+ return { ok: false, reason: result.reason };
43
+ }
44
+ return { ok: true, mode: 'scoped', scopes: capability };
45
+ }
46
+ /** May a principal holding `scopes` reach `/addon/<addonId>/…` with this verb? */
47
+ function authorizeAddonRoute(scopes, target) {
48
+ const grant = (0, types_1.shareViewGrantOf)(scopes);
49
+ if (grant !== null) {
50
+ if (!(target.share ?? []).includes(grant.kind)) {
51
+ return {
52
+ ok: false,
53
+ status: 403,
54
+ error: 'Share token not accepted here',
55
+ reason: `share-view ${grant.kind} not accepted by ${target.addonId}`,
56
+ };
57
+ }
58
+ return {
59
+ ok: true,
60
+ shareHeaders: {
61
+ [addon_route_share_gate_js_1.SHARE_KIND_HEADER]: grant.kind,
62
+ [addon_route_share_gate_js_1.SHARE_DEVICES_HEADER]: grant.deviceIds.join(','),
63
+ },
64
+ };
65
+ }
66
+ if ((0, types_1.scopesGrantAdmin)(scopes))
67
+ return { ok: true, shareHeaders: null };
68
+ if (target.access === 'admin') {
69
+ return { ok: false, status: 403, error: 'Admin required', reason: 'admin route' };
70
+ }
71
+ const access = (0, types_1.methodAccessForHttpMethod)(target.method);
72
+ if ((0, types_1.scopesAllowAddonRoutes)(scopes, access) ||
73
+ (0, types_1.scopesAllowAddon)((0, types_1.capabilityScopesOf)(scopes), target.addonId, access)) {
74
+ return { ok: true, shareHeaders: null };
75
+ }
76
+ return {
77
+ ok: false,
78
+ status: 403,
79
+ error: 'Token scope mismatch',
80
+ reason: `holds no addon:${target.addonId}[${access}] grant`,
81
+ };
82
+ }
@@ -147,7 +147,7 @@ function projectListAllForPrincipal(data, isAdmin, scopes, fleet) {
147
147
  function scopedEventVisibility(user, lookup) {
148
148
  if (user.isAdmin)
149
149
  return null;
150
- const scopes = user.scopes ?? [];
150
+ const scopes = (0, types_1.capabilityScopesOf)(user.scopes ?? []);
151
151
  if (isBroadEventViewer(scopes))
152
152
  return null;
153
153
  return (evt) => {
@@ -29,12 +29,14 @@ function readCredentialIdentity(claims) {
29
29
  /**
30
30
  * Render a principal for one log line.
31
31
  *
32
- * Shapes, in the order they are distinguished:
32
+ * Shapes, in the order they are distinguished (the label comes from the
33
+ * principal's audit tag — attribution only, D892):
33
34
  *
34
35
  * anonymous
35
36
  * apocaliss92 (admin)
36
- * share:ab12cd34 (share-view)
37
- * scoped:1a2b3c4d (scoped-token)
37
+ * share:ab12cd34 (legacy-share-token)
38
+ * scoped:1a2b3c4d (legacy-scoped-token)
39
+ * token:ci (token)
38
40
  * apocaliss92 (oauth-access session=910a6179-…)
39
41
  * apocaliss92 (session)
40
42
  */
@@ -44,15 +46,14 @@ function describeTrpcPrincipal(user) {
44
46
  if (!user)
45
47
  return 'anonymous';
46
48
  const name = user.username || user.id || 'unknown';
47
- if (user.shareView)
48
- return `${name} (share-view)`;
49
- if (user.isAdmin)
49
+ if (user.auditTag === 'session' && user.isAdmin)
50
50
  return `${name} (admin)`;
51
51
  if (user.credential) {
52
52
  const session = user.oauthSessionId ? ` session=${user.oauthSessionId}` : '';
53
53
  return `${name} (${user.credential}${session})`;
54
54
  }
55
- if (user.isScoped)
56
- return `${name} (scoped-token)`;
57
- return `${name} (session)`;
55
+ if (user.auditTag !== undefined) {
56
+ return `${name} (${user.auditTag}${user.isAdmin && user.auditTag !== 'session' ? ' admin' : ''})`;
57
+ }
58
+ return user.isAdmin ? `${name} (admin)` : `${name} (session)`;
58
59
  }