@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.
@@ -17,7 +17,6 @@ async function resolveWhoami(deps, bearer) {
17
17
  scopes: principal.scopes,
18
18
  credentialId: principal.credentialId,
19
19
  credential: principal.auditTag,
20
- legacy: principal.legacy,
21
20
  };
22
21
  }
23
22
  async function registerAuthWhoamiRoute(fastify, authService, addonRegistry) {
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthAuditRecordSchema = void 0;
4
+ exports.createAuthAuditProcedures = createAuthAuditProcedures;
5
+ const server_1 = require("@trpc/server");
6
+ const zod_1 = require("zod");
7
+ const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
8
+ const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
9
+ exports.AuthAuditRecordSchema = zod_1.z.object({
10
+ id: zod_1.z.string(),
11
+ at: zod_1.z.number(),
12
+ action: zod_1.z.string(),
13
+ userId: zod_1.z.string().optional(),
14
+ ip: zod_1.z.string().optional(),
15
+ 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(),
16
+ });
17
+ function createAuthAuditProcedures(deps) {
18
+ return {
19
+ /** ADMIN — the authentication audit trail, newest first. */
20
+ listAuthAudit: trpc_middleware_js_1.adminProcedure
21
+ .input(zod_1.z
22
+ .object({
23
+ limit: zod_1.z.number().int().min(1).max(500).optional(),
24
+ userId: zod_1.z.string().min(1).optional(),
25
+ })
26
+ .optional())
27
+ .output(zod_1.z.array(exports.AuthAuditRecordSchema))
28
+ .query(async ({ input }) => {
29
+ const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
30
+ if (internal === null) {
31
+ throw new server_1.TRPCError({
32
+ code: 'PRECONDITION_FAILED',
33
+ message: 'User management is not available',
34
+ });
35
+ }
36
+ return [
37
+ ...(await internal.audit.list({
38
+ ...(input?.limit !== undefined ? { limit: input.limit } : {}),
39
+ ...(input?.userId !== undefined ? { userId: input.userId } : {}),
40
+ })),
41
+ ];
42
+ }),
43
+ };
44
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RevokeSkipReasonSchema = exports.RevocableCredentialKindSchema = exports.REVOKE_CREDENTIALS_MAX = void 0;
3
+ exports.RevokeSkipReasonSchema = exports.REVOKE_CREDENTIALS_MAX = void 0;
4
4
  exports.createRevokeCredentialsProcedures = createRevokeCredentialsProcedures;
5
5
  const server_1 = require("@trpc/server");
6
6
  const zod_1 = require("zod");
@@ -8,25 +8,9 @@ const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
8
8
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
9
9
  /** Upper bound of one bulk call — a page of a table, several times over. */
10
10
  exports.REVOKE_CREDENTIALS_MAX = 500;
11
- /** What a bulk revoke can target. Each has a per-row revoke on the server. */
12
- exports.RevocableCredentialKindSchema = zod_1.z.enum([
13
- 'token',
14
- 'share-link',
15
- 'admin-token',
16
- 'login-session',
17
- 'scoped-token',
18
- 'oauth-session',
19
- ]);
20
11
  exports.RevokeSkipReasonSchema = zod_1.z.enum(['current-session', 'not-active']);
21
12
  const RevokeCredentialsInputSchema = zod_1.z
22
13
  .object({
23
- /**
24
- * Deprecated (D892): revocation is by CREDENTIAL ID. Omitted, each id is
25
- * resolved against every store — unified tokens, login sessions, legacy
26
- * admin / share / scoped tokens, account links. Kept one release for the
27
- * clients that still send it; it then restricts the lookup to that store.
28
- */
29
- kind: exports.RevocableCredentialKindSchema.optional(),
30
14
  ids: zod_1.z.array(zod_1.z.string().min(1)).min(1).max(exports.REVOKE_CREDENTIALS_MAX),
31
15
  /** The count the operator confirmed. Must equal `ids.length`. */
32
16
  confirmCount: zod_1.z.number().int().min(1),
@@ -49,52 +33,6 @@ function assertSignedInAdminSession(user) {
49
33
  });
50
34
  }
51
35
  }
52
- function requireInternal(registry) {
53
- const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
54
- if (internal === null) {
55
- throw new server_1.TRPCError({
56
- code: 'PRECONDITION_FAILED',
57
- message: 'User management is not available',
58
- });
59
- }
60
- return internal;
61
- }
62
- /** The per-row revoke for `kind`, bound to the caller. `null` = revoked. */
63
- function revokerFor(kind, caller, deps) {
64
- if (kind === 'token') {
65
- const service = deps.credentialTokens ?? null;
66
- if (service === null) {
67
- throw new server_1.TRPCError({ code: 'PRECONDITION_FAILED', message: 'Tokens are not available' });
68
- }
69
- return async (id) => ((await service.revoke(id)) ? null : 'not-active');
70
- }
71
- if (kind === 'share-link') {
72
- const service = deps.shareTokens;
73
- if (service === null) {
74
- throw new server_1.TRPCError({ code: 'PRECONDITION_FAILED', message: 'Share links are not available' });
75
- }
76
- return async (id) => (await service.revoke({ id, callerUserId: caller.id, callerIsAdmin: true }))
77
- ? null
78
- : 'not-active';
79
- }
80
- const internal = requireInternal(deps.registry);
81
- if (kind === 'admin-token') {
82
- return async (id) => ((await internal.adminTokens.revoke(id)) ? null : 'not-active');
83
- }
84
- if (kind === 'scoped-token') {
85
- return async (id) => ((await internal.scopedTokens.revoke(id)) ? null : 'not-active');
86
- }
87
- if (kind === 'oauth-session') {
88
- return async (id) => ((await internal.oauthSessions.revoke(id)) ? null : 'not-active');
89
- }
90
- return async (id) => {
91
- if (id === caller.sessionId)
92
- return 'current-session';
93
- if (internal.sessions.check(id).state !== 'active')
94
- return 'not-active';
95
- return (await internal.sessions.revoke(id, 'revoked-by-admin')) ? null : 'not-active';
96
- };
97
- }
98
36
  /**
99
37
  * Revoke one credential BY ITS ID, whatever it is (D892). Every store is asked
100
38
  * in turn; ids are random UUIDs per store, so at most one knows it. The
@@ -113,16 +51,6 @@ function revokerById(caller, deps) {
113
51
  if (internal.sessions.check(id).state === 'active') {
114
52
  return (await internal.sessions.revoke(id, 'revoked-by-admin')) ? null : 'not-active';
115
53
  }
116
- if (await internal.adminTokens.revoke(id))
117
- return null;
118
- }
119
- if (deps.shareTokens !== null &&
120
- (await deps.shareTokens.revoke({ id, callerUserId: caller.id, callerIsAdmin: true }))) {
121
- return null;
122
- }
123
- if (internal !== null) {
124
- if (await internal.scopedTokens.revoke(id))
125
- return null;
126
54
  if (await internal.oauthSessions.revoke(id))
127
55
  return null;
128
56
  }
@@ -135,7 +63,7 @@ function auditIds(ids) {
135
63
  }
136
64
  function createRevokeCredentialsProcedures(deps) {
137
65
  return {
138
- /** ADMIN — revoke exactly `ids` of one kind; see the module header. */
66
+ /** ADMIN — revoke exactly `ids`, by credential id; see the module header. */
139
67
  revokeCredentials: trpc_middleware_js_1.adminProcedure
140
68
  .input(RevokeCredentialsInputSchema)
141
69
  .output(RevokeCredentialsOutputSchema)
@@ -147,9 +75,7 @@ function createRevokeCredentialsProcedures(deps) {
147
75
  message: `Confirmed ${input.confirmCount} but ${input.ids.length} were listed — nothing was revoked`,
148
76
  });
149
77
  }
150
- const revokeOne = input.kind === undefined
151
- ? revokerById(ctx.user, deps)
152
- : revokerFor(input.kind, ctx.user, deps);
78
+ const revokeOne = revokerById(ctx.user, deps);
153
79
  const revoked = [];
154
80
  const skipped = [];
155
81
  // Sequential on purpose: the stores are single-writer, and a partial
@@ -169,7 +95,7 @@ function createRevokeCredentialsProcedures(deps) {
169
95
  userId: ctx.user.id,
170
96
  ...(ip !== null ? { ip } : {}),
171
97
  detail: {
172
- kind: input.kind ?? 'by-id',
98
+ kind: 'by-id',
173
99
  requested: input.ids.length,
174
100
  revoked: revoked.length,
175
101
  skipped: skipped.length,
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TokenSummarySchema = exports.TokenFormatSchema = exports.MintTokenInputSchema = exports.TokenExpiresInSchema = exports.TOKEN_MAX_LIFETIME_SEC = void 0;
3
+ exports.TokenSummarySchema = exports.ExtendTokenInputSchema = exports.ListTokensInputSchema = exports.MintTokenInputSchema = exports.TokenExpiresInSchema = exports.TOKEN_MAX_LIFETIME_SEC = void 0;
4
4
  exports.summarizeToken = summarizeToken;
5
5
  exports.requireCredentialTokens = requireCredentialTokens;
6
6
  exports.mintFor = mintFor;
@@ -29,9 +29,24 @@ exports.MintTokenInputSchema = zod_1.z.object({
29
29
  label: zod_1.z.string().trim().min(1).max(80),
30
30
  /** Required when `scopes` holds `'*'`. */
31
31
  reauthToken: auth_reauth_procedures_js_1.ReauthTokenInput.optional(),
32
+ /**
33
+ * The user the token acts for — an admin only; omitted, the caller. Its
34
+ * scopes are cut to what that user holds at every verify (a token never
35
+ * outgrows its subject).
36
+ */
37
+ subjectId: zod_1.z.string().min(1).optional(),
38
+ });
39
+ exports.ListTokensInputSchema = zod_1.z
40
+ .object({
41
+ /** Only the tokens with exactly this label — how a client finds the one it minted. */
42
+ label: zod_1.z.string().trim().min(1).max(80).optional(),
43
+ })
44
+ .optional();
45
+ exports.ExtendTokenInputSchema = zod_1.z.object({
46
+ id: zod_1.z.string().min(1),
47
+ /** Seconds from now, or `'never'` (a lone share-view token only). */
48
+ expiresIn: exports.TokenExpiresInSchema,
32
49
  });
33
- /** Where a listed token comes from: the unified format, or a legacy table still honoured. */
34
- exports.TokenFormatSchema = zod_1.z.enum(['token', 'csa', 'cst', 'csv']);
35
50
  exports.TokenSummarySchema = zod_1.z.object({
36
51
  id: zod_1.z.string(),
37
52
  subjectId: zod_1.z.string(),
@@ -41,9 +56,6 @@ exports.TokenSummarySchema = zod_1.z.object({
41
56
  expiresAt: zod_1.z.number().nullable(),
42
57
  lastUsedAt: zod_1.z.number().nullable(),
43
58
  revokedAt: zod_1.z.number().nullable(),
44
- /** A pre-D892 token: valid until it expires, never minted again. */
45
- legacy: zod_1.z.boolean(),
46
- format: exports.TokenFormatSchema,
47
59
  });
48
60
  function summarizeToken(record) {
49
61
  return {
@@ -55,8 +67,6 @@ function summarizeToken(record) {
55
67
  expiresAt: record.expiresAt,
56
68
  lastUsedAt: record.lastUsedAt,
57
69
  revokedAt: record.revokedAt,
58
- legacy: false,
59
- format: 'token',
60
70
  };
61
71
  }
62
72
  function requireCredentialTokens(service) {
@@ -66,13 +76,34 @@ function requireCredentialTokens(service) {
66
76
  return service;
67
77
  }
68
78
  /**
69
- * The one mint, shared by `auth.mintToken` and every wrapper that kept its
70
- * route (`createAdminToken`, `createShareToken`). Refuses; never trims.
79
+ * May `user` mint (or extend) a token carrying `scopes`? A person's login
80
+ * session may mint anything within its reach; an account link only a lone
81
+ * share-view grant (D895 — the reach the share-link route always gave it);
82
+ * a minted token never.
83
+ */
84
+ function provenanceAllows(user, scopes) {
85
+ if (user.interactive === true)
86
+ return true;
87
+ return user.userDelegated === true && scopes.length === 1 && (0, types_1.shareViewGrantOf)(scopes) !== null;
88
+ }
89
+ /** The lifetime rules every mint and every extend obey. Throws a BAD_REQUEST. */
90
+ function assertLifetime(scopes, expiresIn) {
91
+ if (expiresIn === 'never' && (0, types_1.shareViewGrantOf)(scopes) === null) {
92
+ throw new server_1.TRPCError({
93
+ code: 'BAD_REQUEST',
94
+ message: 'Only a share link may be minted without an expiry',
95
+ });
96
+ }
97
+ }
98
+ function expiryOf(expiresIn) {
99
+ return expiresIn === 'never' ? null : Date.now() + expiresIn * 1000;
100
+ }
101
+ /**
102
+ * The one mint behind `auth.mintToken`. Refuses; never trims.
71
103
  */
72
104
  async function mintFor(deps, ctx, request) {
73
105
  const user = ctx.user;
74
- const provenanceOk = request.allowAccountLink === true ? user.userDelegated === true : user.interactive === true;
75
- if (!provenanceOk) {
106
+ if (!provenanceAllows(user, request.scopes)) {
76
107
  throw new server_1.TRPCError({
77
108
  code: 'FORBIDDEN',
78
109
  message: 'A token can only be minted from a signed-in session',
@@ -85,12 +116,7 @@ async function mintFor(deps, ctx, request) {
85
116
  message: 'A share-view grant must be the token’s only scope',
86
117
  });
87
118
  }
88
- if (request.expiresIn === 'never' && share === null) {
89
- throw new server_1.TRPCError({
90
- code: 'BAD_REQUEST',
91
- message: 'Only a share link may be minted without an expiry',
92
- });
93
- }
119
+ assertLifetime(request.scopes, request.expiresIn);
94
120
  const verdict = (0, scope_escalation_js_1.scopesWithin)(request.scopes, user.scopes ?? [], ctx.deviceScopeLookup);
95
121
  if (!verdict.ok) {
96
122
  throw new server_1.TRPCError({
@@ -105,13 +131,19 @@ async function mintFor(deps, ctx, request) {
105
131
  if (subjectId !== user.id && !user.isAdmin) {
106
132
  throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Only an admin mints for another user' });
107
133
  }
134
+ if (subjectId !== user.id && (0, types_1.scopesGrantAdmin)(request.scopes)) {
135
+ throw new server_1.TRPCError({
136
+ code: 'BAD_REQUEST',
137
+ message: "A '*' token acts for the admin who mints it, never for another user",
138
+ });
139
+ }
108
140
  const service = requireCredentialTokens(deps.credentialTokens);
109
141
  const minted = await service.mint({
110
142
  subjectId,
111
143
  createdBy: user.id,
112
144
  label: request.label,
113
145
  scopes: request.scopes,
114
- expiresAt: request.expiresIn === 'never' ? null : Date.now() + request.expiresIn * 1000,
146
+ expiresAt: expiryOf(request.expiresIn),
115
147
  });
116
148
  const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
117
149
  if (internal !== null) {
@@ -134,49 +166,6 @@ async function mintFor(deps, ctx, request) {
134
166
  }
135
167
  return minted;
136
168
  }
137
- async function legacyRows(deps, caller) {
138
- const out = [];
139
- const own = (subjectId) => caller.isAdmin || subjectId === caller.id;
140
- const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
141
- if (internal !== null && caller.isAdmin) {
142
- for (const row of await internal.adminTokens.list()) {
143
- out.push({
144
- id: row.id,
145
- subjectId: row.createdBy,
146
- label: row.name,
147
- scopes: ['*'],
148
- createdAt: row.createdAt,
149
- expiresAt: row.expiresAt,
150
- lastUsedAt: row.lastUsedAt,
151
- revokedAt: row.revokedAt,
152
- legacy: true,
153
- format: 'csa',
154
- });
155
- }
156
- }
157
- if (deps.shareTokens !== null) {
158
- const rows = caller.isAdmin
159
- ? await deps.shareTokens.listAll()
160
- : await deps.shareTokens.listForUser(caller.id);
161
- for (const row of rows) {
162
- if (!own(row.userId))
163
- continue;
164
- out.push({
165
- id: row.id,
166
- subjectId: row.userId,
167
- label: `share ${row.tokenPrefix}`,
168
- scopes: [{ type: 'share-view', grant: row.scope }],
169
- createdAt: row.createdAt,
170
- expiresAt: row.expiresAt,
171
- lastUsedAt: row.lastUsedAt ?? null,
172
- revokedAt: null,
173
- legacy: true,
174
- format: 'csv',
175
- });
176
- }
177
- }
178
- return out;
179
- }
180
169
  function createTokenProcedures(deps) {
181
170
  return {
182
171
  /** Issue a token carrying `scopes` (see the module header). Shown once. */
@@ -189,27 +178,65 @@ function createTokenProcedures(deps) {
189
178
  expiresIn: input.expiresIn,
190
179
  label: input.label,
191
180
  ...(input.reauthToken !== undefined ? { reauthToken: input.reauthToken } : {}),
181
+ ...(input.subjectId !== undefined ? { subjectId: input.subjectId } : {}),
192
182
  });
193
183
  return { token: minted.token, record: summarizeToken(minted.record) };
194
184
  }),
195
185
  /**
196
- * The caller's tokens (an admin: everyone's) — unified and legacy alike,
197
- * a legacy row flagged. No secret is ever listed. (The legacy `cst_`
198
- * rows are listed by `userManagement.listScopedTokens`, as before.)
186
+ * The caller's tokens (an admin: everyone's). No secret is ever listed.
199
187
  */
200
188
  listTokens: trpc_middleware_js_1.protectedProcedure
201
- .input(zod_1.z.void())
189
+ .input(exports.ListTokensInputSchema)
202
190
  .output(zod_1.z.array(exports.TokenSummarySchema))
203
- .query(async ({ ctx }) => {
191
+ .query(async ({ ctx, input }) => {
204
192
  if (ctx.user.userDelegated !== true) {
205
193
  throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Requires a signed-in user session' });
206
194
  }
207
- const unified = deps.credentialTokens === null
208
- ? []
209
- : await deps.credentialTokens.list(ctx.user.isAdmin ? undefined : ctx.user.id);
210
- return [...unified.map(summarizeToken), ...(await legacyRows(deps, ctx.user))];
195
+ const rows = await requireCredentialTokens(deps.credentialTokens).list(ctx.user.isAdmin ? undefined : ctx.user.id);
196
+ const label = input?.label;
197
+ // An account link manages share links only, so it lists only those.
198
+ const shareOnly = ctx.user.interactive !== true;
199
+ return rows
200
+ .filter((row) => label === undefined || row.label === label)
201
+ .filter((row) => !shareOnly || provenanceAllows(ctx.user, row.scopes))
202
+ .map(summarizeToken);
203
+ }),
204
+ /**
205
+ * Move one live token's expiry, in place: the same token keeps working
206
+ * with the new lifetime, so a client renews instead of minting another.
207
+ * The owner or an admin; the same provenance and lifetime rules as a mint
208
+ * (an account link renews only its share links; `'never'` only for one).
209
+ * Shortening is allowed; a revoked or expired token is NOT_FOUND.
210
+ */
211
+ extendToken: trpc_middleware_js_1.protectedProcedure
212
+ .input(exports.ExtendTokenInputSchema)
213
+ .output(exports.TokenSummarySchema)
214
+ .mutation(async ({ ctx, input }) => {
215
+ const service = requireCredentialTokens(deps.credentialTokens);
216
+ const record = await service.get(input.id);
217
+ if (record === null) {
218
+ throw new server_1.TRPCError({ code: 'NOT_FOUND', message: `Token not found: ${input.id}` });
219
+ }
220
+ if (!ctx.user.isAdmin && record.subjectId !== ctx.user.id) {
221
+ throw new server_1.TRPCError({
222
+ code: 'FORBIDDEN',
223
+ message: 'Only the owner or an admin extends a token',
224
+ });
225
+ }
226
+ if (!provenanceAllows(ctx.user, record.scopes)) {
227
+ throw new server_1.TRPCError({
228
+ code: 'FORBIDDEN',
229
+ message: 'A token can only be extended from a signed-in session',
230
+ });
231
+ }
232
+ assertLifetime(record.scopes, input.expiresIn);
233
+ const moved = await service.setExpiry(input.id, expiryOf(input.expiresIn));
234
+ if (moved === null) {
235
+ throw new server_1.TRPCError({ code: 'NOT_FOUND', message: `Token expired or revoked: ${input.id}` });
236
+ }
237
+ return summarizeToken(moved);
211
238
  }),
212
- /** Revoke one unified token: its owner or an admin. It stops working at once. */
239
+ /** Revoke one token: its owner or an admin. It stops working at once. */
213
240
  revokeToken: trpc_middleware_js_1.protectedProcedure
214
241
  .input(zod_1.z.object({ id: zod_1.z.string().min(1) }))
215
242
  .output(zod_1.z.object({ success: zod_1.z.boolean() }))
@@ -227,6 +254,12 @@ function createTokenProcedures(deps) {
227
254
  message: 'Only the owner or an admin revokes a token',
228
255
  });
229
256
  }
257
+ if (!provenanceAllows(ctx.user, record.scopes)) {
258
+ throw new server_1.TRPCError({
259
+ code: 'FORBIDDEN',
260
+ message: 'An account link revokes only its share links',
261
+ });
262
+ }
230
263
  const success = await service.revoke(input.id);
231
264
  const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
232
265
  if (success && internal !== null) {