@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/api/auth-whoami.js
CHANGED
|
@@ -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.
|
|
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
|
|
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 =
|
|
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:
|
|
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.
|
|
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
|
-
*
|
|
70
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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(
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
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) {
|