@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.
@@ -1,55 +1,40 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveWhoami = resolveWhoami;
3
4
  exports.registerAuthWhoamiRoute = registerAuthWhoamiRoute;
5
+ const trpc_context_js_1 = require("./trpc/trpc.context.js");
6
+ /** The answer for `bearer`, or `null` when it authenticates nobody. */
7
+ async function resolveWhoami(deps, bearer) {
8
+ const principal = await (0, trpc_context_js_1.hubCredentialVerifier)(deps).verify(bearer);
9
+ if (principal === null)
10
+ return null;
11
+ return {
12
+ ok: true,
13
+ kind: principal.userDelegated ? 'jwt' : 'scoped',
14
+ userId: principal.subjectId,
15
+ username: principal.username,
16
+ isAdmin: principal.isAdmin,
17
+ scopes: principal.scopes,
18
+ credentialId: principal.credentialId,
19
+ credential: principal.auditTag,
20
+ legacy: principal.legacy,
21
+ };
22
+ }
4
23
  async function registerAuthWhoamiRoute(fastify, authService, addonRegistry) {
5
24
  fastify.get('/api/auth/whoami', async (request, reply) => {
6
25
  const authHeader = request.headers.authorization;
7
26
  if (!authHeader) {
8
- return reply.status(401).send({ ok: false, error: 'No Authorization header' });
27
+ const refused = { ok: false, error: 'No Authorization header' };
28
+ return reply.status(401).send(refused);
9
29
  }
10
- const token = authHeader.replace('Bearer ', '');
11
- // JWT fast path — same logic as addon-upload auth chain.
12
- try {
13
- const payload = authService.verifyToken(token);
14
- const ok = {
15
- ok: true,
16
- kind: 'jwt',
17
- userId: payload.userId ?? 'unknown',
18
- username: payload.username ?? 'unknown',
19
- isAdmin: payload.isAdmin,
30
+ const answer = await resolveWhoami({ authService, addonRegistry }, authHeader.replace('Bearer ', ''));
31
+ if (answer === null) {
32
+ const refused = {
33
+ ok: false,
34
+ error: 'Token not recognised (revoked, expired, or never issued)',
20
35
  };
21
- return reply.send(ok);
22
- }
23
- catch {
24
- // Not a JWT — try scoped token via cap registry.
25
- }
26
- try {
27
- const capRegistry = addonRegistry.getCapabilityRegistry();
28
- const userMgmt = capRegistry.getSingleton('user-management');
29
- if (!userMgmt) {
30
- return reply.status(503).send({ ok: false, error: 'user-management cap not mounted' });
31
- }
32
- const record = await userMgmt.validateScopedToken({ token });
33
- if (!record) {
34
- return reply
35
- .status(401)
36
- .send({ ok: false, error: 'Token not recognised (revoked, expired, or never issued)' });
37
- }
38
- const ok = {
39
- ok: true,
40
- kind: 'scoped',
41
- userId: record.userId,
42
- username: record.userId,
43
- tokenPrefix: record.tokenPrefix,
44
- scopes: record.scopes,
45
- expiresAt: record.expiresAt ?? null,
46
- createdAt: record.createdAt,
47
- };
48
- return reply.send(ok);
49
- }
50
- catch (err) {
51
- const msg = err instanceof Error ? err.message : String(err);
52
- return reply.status(500).send({ ok: false, error: `Validation failed: ${msg}` });
36
+ return reply.status(401).send(refused);
53
37
  }
38
+ return reply.send(answer);
54
39
  });
55
40
  }
@@ -15,6 +15,8 @@ const zod_1 = require("zod");
15
15
  const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
16
16
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
17
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");
18
20
  const DAY_MS = 86_400_000;
19
21
  exports.ADMIN_TOKEN_DEFAULT_DAYS = 7;
20
22
  exports.ADMIN_TOKEN_MAX_DAYS = system_1.ADMIN_TOKEN_MAX_LIFETIME_MS / DAY_MS;
@@ -27,7 +29,23 @@ exports.AdminTokenSchema = zod_1.z.object({
27
29
  expiresAt: zod_1.z.number(),
28
30
  lastUsedAt: zod_1.z.number().nullable(),
29
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(),
30
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
+ }
31
49
  exports.AuthAuditRecordSchema = zod_1.z.object({
32
50
  id: zod_1.z.string(),
33
51
  at: zod_1.z.number(),
@@ -67,47 +85,47 @@ function createAdminTokenProcedures(deps) {
67
85
  }))
68
86
  .output(zod_1.z.object({ token: zod_1.z.string(), record: exports.AdminTokenSchema }))
69
87
  .mutation(async ({ ctx, input }) => {
70
- if (ctx.user.isScoped === true ||
71
- ctx.user.shareView !== undefined ||
72
- ctx.user.credential !== undefined) {
88
+ if (ctx.user.interactive !== true) {
73
89
  throw new server_1.TRPCError({
74
90
  code: 'FORBIDDEN',
75
91
  message: 'An admin token can only be created from a signed-in admin session',
76
92
  });
77
93
  }
78
- (0, auth_reauth_procedures_js_1.assertReauthenticated)(deps.auth, ctx.user, input.reauthToken);
79
- const internal = requireInternal(deps.registry);
80
- const created = await internal.adminTokens.create({
81
- createdBy: ctx.user.id,
82
- name: input.name,
83
- expiresAt: Date.now() + input.expiresInDays * DAY_MS,
84
- });
85
- const ip = deps.network.clientAddress(ctx.req).ip;
86
- await internal.audit.record({
87
- action: 'admin-token-created',
88
- userId: ctx.user.id,
89
- ...(ip !== null ? { ip } : {}),
90
- detail: {
91
- tokenId: created.record.id,
92
- name: created.record.name,
93
- prefix: created.record.tokenPrefix,
94
- expiresAt: created.record.expiresAt,
95
- },
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 } : {}),
96
101
  });
97
- return created;
102
+ const expiresAt = minted.record.expiresAt ?? Date.now() + input.expiresInDays * DAY_MS;
103
+ return { token: minted.token, record: adminSummaryOf(minted.record, expiresAt) };
98
104
  }),
99
105
  /** ADMIN — every admin token not yet reaped (no secrets). */
100
106
  listAdminTokens: trpc_middleware_js_1.adminProcedure
101
107
  .input(zod_1.z.void())
102
108
  .output(zod_1.z.array(exports.AdminTokenSchema))
103
- .query(async () => [...(await requireInternal(deps.registry).adminTokens.list())]),
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
+ }),
104
119
  /** ADMIN — revoke one admin token; it stops working at once. */
105
120
  revokeAdminToken: trpc_middleware_js_1.adminProcedure
106
121
  .input(zod_1.z.object({ id: zod_1.z.string().min(1) }))
107
122
  .output(zod_1.z.object({ success: zod_1.z.boolean() }))
108
123
  .mutation(async ({ ctx, input }) => {
109
124
  const internal = requireInternal(deps.registry);
110
- const success = await internal.adminTokens.revoke(input.id);
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);
111
129
  if (success) {
112
130
  const ip = deps.network.clientAddress(ctx.req).ip;
113
131
  await internal.audit.record({
@@ -110,7 +110,7 @@ function createDeviceCertProcedures(deps) {
110
110
  }
111
111
  };
112
112
  const assertPerson = (user) => {
113
- if (user.isScoped === true || user.shareView !== undefined || user.adminTokenId !== undefined) {
113
+ if (user.userDelegated !== true) {
114
114
  throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'auth.session-required', undefined, 'Requires a signed-in user session');
115
115
  }
116
116
  };
@@ -31,7 +31,7 @@ function isPasswordChecker(value) {
31
31
  }
32
32
  /** Real user sessions only: a scoped or share token cannot prove a password. */
33
33
  function assertPerson(user) {
34
- if (user.isScoped === true || user.shareView !== undefined || user.adminTokenId !== undefined) {
34
+ if (user.userDelegated !== true) {
35
35
  throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'auth.session-required', undefined, 'Requires a signed-in user session');
36
36
  }
37
37
  }
@@ -10,6 +10,7 @@ const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
10
10
  exports.REVOKE_CREDENTIALS_MAX = 500;
11
11
  /** What a bulk revoke can target. Each has a per-row revoke on the server. */
12
12
  exports.RevocableCredentialKindSchema = zod_1.z.enum([
13
+ 'token',
13
14
  'share-link',
14
15
  'admin-token',
15
16
  'login-session',
@@ -19,7 +20,13 @@ exports.RevocableCredentialKindSchema = zod_1.z.enum([
19
20
  exports.RevokeSkipReasonSchema = zod_1.z.enum(['current-session', 'not-active']);
20
21
  const RevokeCredentialsInputSchema = zod_1.z
21
22
  .object({
22
- kind: exports.RevocableCredentialKindSchema,
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(),
23
30
  ids: zod_1.z.array(zod_1.z.string().min(1)).min(1).max(exports.REVOKE_CREDENTIALS_MAX),
24
31
  /** The count the operator confirmed. Must equal `ids.length`. */
25
32
  confirmCount: zod_1.z.number().int().min(1),
@@ -35,10 +42,7 @@ const RevokeCredentialsOutputSchema = zod_1.z.object({
35
42
  /** Keep the audit row bounded; the ids beyond this are counted, not listed. */
36
43
  const AUDIT_IDS_MAX_CHARS = 4000;
37
44
  function assertSignedInAdminSession(user) {
38
- if (user.isScoped === true ||
39
- user.shareView !== undefined ||
40
- user.credential !== undefined ||
41
- user.adminTokenId !== undefined) {
45
+ if (user.interactive !== true) {
42
46
  throw new server_1.TRPCError({
43
47
  code: 'FORBIDDEN',
44
48
  message: 'Revoking credentials in bulk needs a signed-in admin session',
@@ -57,6 +61,13 @@ function requireInternal(registry) {
57
61
  }
58
62
  /** The per-row revoke for `kind`, bound to the caller. `null` = revoked. */
59
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
+ }
60
71
  if (kind === 'share-link') {
61
72
  const service = deps.shareTokens;
62
73
  if (service === null) {
@@ -84,6 +95,40 @@ function revokerFor(kind, caller, deps) {
84
95
  return (await internal.sessions.revoke(id, 'revoked-by-admin')) ? null : 'not-active';
85
96
  };
86
97
  }
98
+ /**
99
+ * Revoke one credential BY ITS ID, whatever it is (D892). Every store is asked
100
+ * in turn; ids are random UUIDs per store, so at most one knows it. The
101
+ * caller's own credential is never revoked here.
102
+ */
103
+ function revokerById(caller, deps) {
104
+ const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
105
+ return async (id) => {
106
+ if (id === caller.sessionId || id === caller.credentialId)
107
+ return 'current-session';
108
+ const tokens = deps.credentialTokens ?? null;
109
+ if (tokens !== null && (await tokens.get(id)) !== null) {
110
+ return (await tokens.revoke(id)) ? null : 'not-active';
111
+ }
112
+ if (internal !== null) {
113
+ if (internal.sessions.check(id).state === 'active') {
114
+ return (await internal.sessions.revoke(id, 'revoked-by-admin')) ? null : 'not-active';
115
+ }
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
+ if (await internal.oauthSessions.revoke(id))
127
+ return null;
128
+ }
129
+ return 'not-active';
130
+ };
131
+ }
87
132
  function auditIds(ids) {
88
133
  const joined = ids.join(',');
89
134
  return joined.length <= AUDIT_IDS_MAX_CHARS ? joined : `${joined.slice(0, AUDIT_IDS_MAX_CHARS)}…`;
@@ -102,7 +147,9 @@ function createRevokeCredentialsProcedures(deps) {
102
147
  message: `Confirmed ${input.confirmCount} but ${input.ids.length} were listed — nothing was revoked`,
103
148
  });
104
149
  }
105
- const revokeOne = revokerFor(input.kind, ctx.user, deps);
150
+ const revokeOne = input.kind === undefined
151
+ ? revokerById(ctx.user, deps)
152
+ : revokerFor(input.kind, ctx.user, deps);
106
153
  const revoked = [];
107
154
  const skipped = [];
108
155
  // Sequential on purpose: the stores are single-writer, and a partial
@@ -122,7 +169,7 @@ function createRevokeCredentialsProcedures(deps) {
122
169
  userId: ctx.user.id,
123
170
  ...(ip !== null ? { ip } : {}),
124
171
  detail: {
125
- kind: input.kind,
172
+ kind: input.kind ?? 'by-id',
126
173
  requested: input.ids.length,
127
174
  revoked: revoked.length,
128
175
  skipped: skipped.length,
@@ -51,7 +51,7 @@ function requireSessions(registry) {
51
51
  }
52
52
  /** Real user sessions only — a scoped or share token has no login session to manage. */
53
53
  function assertLoginSession(user) {
54
- if (user.isScoped === true || user.shareView !== undefined || user.adminTokenId !== undefined) {
54
+ if (user.userDelegated !== true) {
55
55
  throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'auth.session-required', undefined, 'Requires a signed-in user session');
56
56
  }
57
57
  }
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TokenSummarySchema = exports.TokenFormatSchema = exports.MintTokenInputSchema = exports.TokenExpiresInSchema = exports.TOKEN_MAX_LIFETIME_SEC = void 0;
4
+ exports.summarizeToken = summarizeToken;
5
+ exports.requireCredentialTokens = requireCredentialTokens;
6
+ exports.mintFor = mintFor;
7
+ exports.createTokenProcedures = createTokenProcedures;
8
+ exports.daysToSeconds = daysToSeconds;
9
+ const types_1 = require("@camstack/types");
10
+ const server_1 = require("@trpc/server");
11
+ const zod_1 = require("zod");
12
+ const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
13
+ const scope_escalation_js_1 = require("../../auth/scope-escalation.js");
14
+ const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
15
+ const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
16
+ const DAY_MS = 86_400_000;
17
+ /** Every token's longest lifetime — the admin token's bound, now everyone's (D816). */
18
+ exports.TOKEN_MAX_LIFETIME_SEC = 90 * 86_400;
19
+ const TOKEN_MIN_LIFETIME_SEC = 60;
20
+ const MAX_SCOPES = 32;
21
+ exports.TokenExpiresInSchema = zod_1.z.union([
22
+ zod_1.z.number().int().min(TOKEN_MIN_LIFETIME_SEC).max(exports.TOKEN_MAX_LIFETIME_SEC),
23
+ zod_1.z.literal('never'),
24
+ ]);
25
+ exports.MintTokenInputSchema = zod_1.z.object({
26
+ scopes: zod_1.z.array(types_1.CredentialScopeSchema).min(1).max(MAX_SCOPES),
27
+ /** Seconds until expiry, or `'never'` (a share-view token only). */
28
+ expiresIn: exports.TokenExpiresInSchema,
29
+ label: zod_1.z.string().trim().min(1).max(80),
30
+ /** Required when `scopes` holds `'*'`. */
31
+ reauthToken: auth_reauth_procedures_js_1.ReauthTokenInput.optional(),
32
+ });
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
+ exports.TokenSummarySchema = zod_1.z.object({
36
+ id: zod_1.z.string(),
37
+ subjectId: zod_1.z.string(),
38
+ label: zod_1.z.string(),
39
+ scopes: zod_1.z.array(types_1.CredentialScopeSchema),
40
+ createdAt: zod_1.z.number(),
41
+ expiresAt: zod_1.z.number().nullable(),
42
+ lastUsedAt: zod_1.z.number().nullable(),
43
+ 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
+ });
48
+ function summarizeToken(record) {
49
+ return {
50
+ id: record.id,
51
+ subjectId: record.subjectId,
52
+ label: record.label,
53
+ scopes: [...record.scopes],
54
+ createdAt: record.createdAt,
55
+ expiresAt: record.expiresAt,
56
+ lastUsedAt: record.lastUsedAt,
57
+ revokedAt: record.revokedAt,
58
+ legacy: false,
59
+ format: 'token',
60
+ };
61
+ }
62
+ function requireCredentialTokens(service) {
63
+ if (service === null) {
64
+ throw new server_1.TRPCError({ code: 'PRECONDITION_FAILED', message: 'Tokens are not available' });
65
+ }
66
+ return service;
67
+ }
68
+ /**
69
+ * The one mint, shared by `auth.mintToken` and every wrapper that kept its
70
+ * route (`createAdminToken`, `createShareToken`). Refuses; never trims.
71
+ */
72
+ async function mintFor(deps, ctx, request) {
73
+ const user = ctx.user;
74
+ const provenanceOk = request.allowAccountLink === true ? user.userDelegated === true : user.interactive === true;
75
+ if (!provenanceOk) {
76
+ throw new server_1.TRPCError({
77
+ code: 'FORBIDDEN',
78
+ message: 'A token can only be minted from a signed-in session',
79
+ });
80
+ }
81
+ const share = (0, types_1.shareViewGrantOf)(request.scopes);
82
+ if (share !== null && request.scopes.length !== 1) {
83
+ throw new server_1.TRPCError({
84
+ code: 'BAD_REQUEST',
85
+ message: 'A share-view grant must be the token’s only scope',
86
+ });
87
+ }
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
+ }
94
+ const verdict = (0, scope_escalation_js_1.scopesWithin)(request.scopes, user.scopes ?? [], ctx.deviceScopeLookup);
95
+ if (!verdict.ok) {
96
+ throw new server_1.TRPCError({
97
+ code: 'FORBIDDEN',
98
+ message: `A token cannot carry more than you hold: ${verdict.reason}`,
99
+ });
100
+ }
101
+ if ((0, types_1.scopesGrantAdmin)(request.scopes)) {
102
+ (0, auth_reauth_procedures_js_1.assertReauthenticated)(deps.auth, user, request.reauthToken);
103
+ }
104
+ const subjectId = request.subjectId ?? user.id;
105
+ if (subjectId !== user.id && !user.isAdmin) {
106
+ throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Only an admin mints for another user' });
107
+ }
108
+ const service = requireCredentialTokens(deps.credentialTokens);
109
+ const minted = await service.mint({
110
+ subjectId,
111
+ createdBy: user.id,
112
+ label: request.label,
113
+ scopes: request.scopes,
114
+ expiresAt: request.expiresIn === 'never' ? null : Date.now() + request.expiresIn * 1000,
115
+ });
116
+ const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
117
+ if (internal !== null) {
118
+ const ip = deps.network.clientAddress(ctx.req).ip;
119
+ await internal.audit
120
+ .record({
121
+ // An admin-capable token keeps the action the audit page already words.
122
+ action: (0, types_1.scopesGrantAdmin)(request.scopes) ? 'admin-token-created' : 'token-minted',
123
+ userId: user.id,
124
+ ...(ip !== null ? { ip } : {}),
125
+ detail: {
126
+ tokenId: minted.record.id,
127
+ name: minted.record.label,
128
+ subjectId,
129
+ scopes: request.scopes.length,
130
+ ...(minted.record.expiresAt !== null ? { expiresAt: minted.record.expiresAt } : {}),
131
+ },
132
+ })
133
+ .catch(() => { });
134
+ }
135
+ return minted;
136
+ }
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
+ function createTokenProcedures(deps) {
181
+ return {
182
+ /** Issue a token carrying `scopes` (see the module header). Shown once. */
183
+ mintToken: trpc_middleware_js_1.protectedProcedure
184
+ .input(exports.MintTokenInputSchema)
185
+ .output(zod_1.z.object({ token: zod_1.z.string(), record: exports.TokenSummarySchema }))
186
+ .mutation(async ({ ctx, input }) => {
187
+ const minted = await mintFor(deps, ctx, {
188
+ scopes: input.scopes,
189
+ expiresIn: input.expiresIn,
190
+ label: input.label,
191
+ ...(input.reauthToken !== undefined ? { reauthToken: input.reauthToken } : {}),
192
+ });
193
+ return { token: minted.token, record: summarizeToken(minted.record) };
194
+ }),
195
+ /**
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.)
199
+ */
200
+ listTokens: trpc_middleware_js_1.protectedProcedure
201
+ .input(zod_1.z.void())
202
+ .output(zod_1.z.array(exports.TokenSummarySchema))
203
+ .query(async ({ ctx }) => {
204
+ if (ctx.user.userDelegated !== true) {
205
+ throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Requires a signed-in user session' });
206
+ }
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))];
211
+ }),
212
+ /** Revoke one unified token: its owner or an admin. It stops working at once. */
213
+ revokeToken: trpc_middleware_js_1.protectedProcedure
214
+ .input(zod_1.z.object({ id: zod_1.z.string().min(1) }))
215
+ .output(zod_1.z.object({ success: zod_1.z.boolean() }))
216
+ .mutation(async ({ ctx, input }) => {
217
+ if (ctx.user.userDelegated !== true) {
218
+ throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Requires a signed-in user session' });
219
+ }
220
+ const service = requireCredentialTokens(deps.credentialTokens);
221
+ const record = await service.get(input.id);
222
+ if (record === null)
223
+ return { success: false };
224
+ if (!ctx.user.isAdmin && record.subjectId !== ctx.user.id) {
225
+ throw new server_1.TRPCError({
226
+ code: 'FORBIDDEN',
227
+ message: 'Only the owner or an admin revokes a token',
228
+ });
229
+ }
230
+ const success = await service.revoke(input.id);
231
+ const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
232
+ if (success && internal !== null) {
233
+ const ip = deps.network.clientAddress(ctx.req).ip;
234
+ await internal.audit
235
+ .record({
236
+ action: 'token-revoked',
237
+ userId: ctx.user.id,
238
+ ...(ip !== null ? { ip } : {}),
239
+ detail: { tokenId: input.id },
240
+ })
241
+ .catch(() => { });
242
+ }
243
+ return { success };
244
+ }),
245
+ };
246
+ }
247
+ /** Days → the `expiresIn` seconds the mint takes. */
248
+ function daysToSeconds(days) {
249
+ return (days * DAY_MS) / 1000;
250
+ }