@camstack/server 1.2.605 → 1.2.607
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/cap-providers.js +38 -1
- package/dist/api/core/integration-secrets.js +22 -1
- package/dist/api/core/logs.router.js +219 -49
- 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 +53 -42
- 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 +7 -2
- 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/core/logging/log-history.js +119 -0
- package/dist/core/logging/logging.service.js +38 -1
- 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
|
@@ -11,7 +11,9 @@ exports.createLogsRouter = createLogsRouter;
|
|
|
11
11
|
*/
|
|
12
12
|
const zod_1 = require("zod");
|
|
13
13
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
14
|
-
const
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
const system_1 = require("@camstack/system");
|
|
16
|
+
const log_history_js_1 = require("../../core/logging/log-history.js");
|
|
15
17
|
const LogLevelSchema = zod_1.z.enum(['debug', 'info', 'warn', 'error']);
|
|
16
18
|
const LogTagsSchema = zod_1.z.object({
|
|
17
19
|
agentId: zod_1.z.string().optional(),
|
|
@@ -39,6 +41,8 @@ const LogTagsSchema = zod_1.z.object({
|
|
|
39
41
|
});
|
|
40
42
|
/** Wire shape of a log entry — timestamp is serialised to ISO string by tRPC. */
|
|
41
43
|
const LogEntrySchema = zod_1.z.object({
|
|
44
|
+
/** `<bootId>.<seq>` — identical live and on disk; the client's dedupe key (D898). */
|
|
45
|
+
id: zod_1.z.string().optional(),
|
|
42
46
|
timestamp: zod_1.z.string(),
|
|
43
47
|
level: zod_1.z.string(),
|
|
44
48
|
scope: zod_1.z.string().optional(),
|
|
@@ -53,63 +57,214 @@ const LogEntrySchema = zod_1.z.object({
|
|
|
53
57
|
meta: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
54
58
|
});
|
|
55
59
|
const LogEntryArraySchema = zod_1.z.array(LogEntrySchema);
|
|
56
|
-
|
|
60
|
+
/** Wire shape of a history page (D898). */
|
|
61
|
+
const LogPageSchema = zod_1.z.object({
|
|
62
|
+
entries: LogEntryArraySchema,
|
|
63
|
+
/** Pass back as `before` to load the next OLDER page; null = nothing older. */
|
|
64
|
+
nextBefore: zod_1.z.string().nullable(),
|
|
65
|
+
nextAfter: zod_1.z.string().nullable(),
|
|
66
|
+
/** Bytes the source scanned for this slice — the "N MB scanned" indicator. */
|
|
67
|
+
scannedBytes: zod_1.z.number().int().nonnegative(),
|
|
68
|
+
/**
|
|
69
|
+
* True when the source has nothing older. False with fewer than `limit`
|
|
70
|
+
* entries = the slice's time budget ran out first; ask again with
|
|
71
|
+
* `nextBefore` to keep searching.
|
|
72
|
+
*/
|
|
73
|
+
done: zod_1.z.boolean(),
|
|
74
|
+
cancelled: zod_1.z.boolean().optional(),
|
|
75
|
+
/** The source that answered — `ring` when the requested one cannot read. */
|
|
76
|
+
source: zod_1.z.string(),
|
|
77
|
+
});
|
|
78
|
+
const LogSourceSchema = zod_1.z.object({
|
|
79
|
+
addonId: zod_1.z.string(),
|
|
80
|
+
readable: zod_1.z.boolean(),
|
|
81
|
+
reason: zod_1.z.string().nullable(),
|
|
82
|
+
isDefault: zod_1.z.boolean(),
|
|
83
|
+
retentionDays: zod_1.z.number().nullable(),
|
|
84
|
+
filters: zod_1.z.object({
|
|
85
|
+
deviceId: zod_1.z.boolean(),
|
|
86
|
+
addonId: zod_1.z.boolean(),
|
|
87
|
+
nodeId: zod_1.z.boolean(),
|
|
88
|
+
tags: zod_1.z.boolean(),
|
|
89
|
+
level: zod_1.z.boolean(),
|
|
90
|
+
text: zod_1.z.boolean(),
|
|
91
|
+
timeRange: zod_1.z.boolean(),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
/** `tags` (the LogStream scope) → the read contract's indexed fields + the rest. */
|
|
95
|
+
function readFiltersFromTags(tags) {
|
|
96
|
+
if (tags === undefined)
|
|
97
|
+
return {};
|
|
98
|
+
const rest = {};
|
|
99
|
+
let deviceId;
|
|
100
|
+
let addonId;
|
|
101
|
+
let nodeId;
|
|
102
|
+
for (const [key, raw] of Object.entries(tags)) {
|
|
103
|
+
if (raw === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
if (key === 'deviceId') {
|
|
106
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
107
|
+
if (Number.isInteger(n))
|
|
108
|
+
deviceId = n;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (key === 'addonId' && typeof raw === 'string') {
|
|
112
|
+
addonId = raw;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (key === 'agentId' && typeof raw === 'string') {
|
|
116
|
+
nodeId = raw;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (key === 'containerDeviceId') {
|
|
120
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
121
|
+
rest[key] = Number.isInteger(n) ? n : String(raw);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
rest[key] = raw;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...(deviceId !== undefined ? { deviceId } : {}),
|
|
128
|
+
...(addonId !== undefined ? { fromAddonId: addonId } : {}),
|
|
129
|
+
...(nodeId !== undefined ? { fromNodeId: nodeId } : {}),
|
|
130
|
+
...(Object.keys(rest).length > 0 ? { tags: rest } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function toWire(entry) {
|
|
134
|
+
return {
|
|
135
|
+
...(entry.id !== undefined ? { id: entry.id } : {}),
|
|
136
|
+
timestamp: new Date(entry.timestamp).toISOString(),
|
|
137
|
+
level: entry.level,
|
|
138
|
+
...(entry.scope !== undefined ? { scope: entry.scope } : {}),
|
|
139
|
+
message: entry.message,
|
|
140
|
+
...(entry.tags !== undefined
|
|
141
|
+
? {
|
|
142
|
+
tags: Object.fromEntries(Object.entries(entry.tags).map(([k, v]) => [k, String(v)])),
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
...(entry.meta !== undefined ? { meta: entry.meta } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** Hub-local providers only: an agent's own files hold lines the hub already has. */
|
|
149
|
+
function hubLocalDestinations(registry) {
|
|
150
|
+
if (registry === null)
|
|
151
|
+
return [];
|
|
152
|
+
return registry
|
|
153
|
+
.getCollectionEntries('log-destination')
|
|
154
|
+
.filter(([addonId]) => !addonId.includes('@'));
|
|
155
|
+
}
|
|
156
|
+
const NOT_READABLE = {
|
|
157
|
+
readable: false,
|
|
158
|
+
reason: 'This destination only ships lines',
|
|
159
|
+
retentionDays: null,
|
|
160
|
+
filters: {
|
|
161
|
+
deviceId: false,
|
|
162
|
+
addonId: false,
|
|
163
|
+
nodeId: false,
|
|
164
|
+
tags: false,
|
|
165
|
+
level: false,
|
|
166
|
+
text: false,
|
|
167
|
+
timeRange: false,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
function createLogsRouter(logging, capabilityRegistry = null) {
|
|
57
171
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
172
|
+
/**
|
|
173
|
+
* One page of history (D898): newest first, from the source the client
|
|
174
|
+
* picked (default the hub's rotated files), every filter applied by the
|
|
175
|
+
* source BEFORE the limit. Page older with `before: nextBefore`. A slice
|
|
176
|
+
* is time-bounded — `done: false` with a short page means "still
|
|
177
|
+
* searching", and aborting the request cancels the slice in flight.
|
|
178
|
+
*/
|
|
58
179
|
query: trpc_middleware_js_1.adminProcedure
|
|
59
180
|
.input(zod_1.z.object({
|
|
181
|
+
/** A read-capable log-destination addonId, or `ring`. Default `winston-logging`. */
|
|
182
|
+
source: zod_1.z.string().min(1).max(200).optional(),
|
|
60
183
|
level: LogLevelSchema.optional(),
|
|
61
184
|
since: zod_1.z.number().optional(),
|
|
62
185
|
until: zod_1.z.number().optional(),
|
|
63
|
-
limit: zod_1.z.number().optional(),
|
|
186
|
+
limit: zod_1.z.number().int().min(1).max(1000).optional(),
|
|
64
187
|
tags: LogTagsSchema.optional(),
|
|
188
|
+
/** Case-insensitive substring over message, scope, tags and meta. */
|
|
189
|
+
text: zod_1.z.string().max(200).optional(),
|
|
190
|
+
before: zod_1.z.string().min(1).max(1024).optional(),
|
|
191
|
+
after: zod_1.z.string().min(1).max(1024).optional(),
|
|
192
|
+
budgetMs: zod_1.z.number().int().min(50).max(10_000).optional(),
|
|
65
193
|
}))
|
|
66
|
-
.output(
|
|
67
|
-
.query(({ input,
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
194
|
+
.output(LogPageSchema)
|
|
195
|
+
.query(async ({ input, signal }) => {
|
|
196
|
+
const sourceId = input.source ?? log_history_js_1.DEFAULT_LOG_SOURCE;
|
|
197
|
+
const provider = sourceId === log_history_js_1.RING_LOG_SOURCE
|
|
198
|
+
? null
|
|
199
|
+
: (hubLocalDestinations(capabilityRegistry).find(([id]) => id === sourceId)?.[1] ??
|
|
200
|
+
null);
|
|
201
|
+
const requestId = (0, node_crypto_1.randomUUID)();
|
|
202
|
+
const text = input.text?.trim();
|
|
203
|
+
const query = {
|
|
204
|
+
requestId,
|
|
205
|
+
...readFiltersFromTags(input.tags),
|
|
206
|
+
...(input.level !== undefined ? { level: input.level } : {}),
|
|
207
|
+
...(text !== undefined && text.length > 0 ? { text } : {}),
|
|
208
|
+
...(input.since !== undefined ? { since: input.since } : {}),
|
|
209
|
+
...(input.until !== undefined ? { until: input.until } : {}),
|
|
210
|
+
...(input.before !== undefined ? { before: input.before } : {}),
|
|
211
|
+
...(input.after !== undefined ? { after: input.after } : {}),
|
|
212
|
+
...(input.budgetMs !== undefined ? { budgetMs: input.budgetMs } : {}),
|
|
213
|
+
limit: input.limit ?? log_history_js_1.DEFAULT_PAGE_LIMIT,
|
|
214
|
+
};
|
|
215
|
+
const onAbort = () => {
|
|
216
|
+
void provider?.cancelRead?.({ requestId }).catch(() => undefined);
|
|
217
|
+
};
|
|
218
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
219
|
+
try {
|
|
220
|
+
const page = await (0, log_history_js_1.readLogHistory)({
|
|
221
|
+
query,
|
|
222
|
+
source: sourceId,
|
|
223
|
+
tail: logging.query({}),
|
|
224
|
+
provider,
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
entries: page.entries.map(toWire),
|
|
228
|
+
nextBefore: page.nextBefore,
|
|
229
|
+
nextAfter: page.nextAfter,
|
|
230
|
+
scannedBytes: page.scannedBytes,
|
|
231
|
+
done: page.done,
|
|
232
|
+
...(page.cancelled !== undefined ? { cancelled: page.cancelled } : {}),
|
|
233
|
+
source: page.source,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
signal?.removeEventListener('abort', onAbort);
|
|
238
|
+
}
|
|
239
|
+
}),
|
|
240
|
+
/**
|
|
241
|
+
* The history sources the Logs page can offer: every hub-local
|
|
242
|
+
* log-destination with what it can read. A destination without
|
|
243
|
+
* `describeRead` is listed as not readable, never omitted — "ships only"
|
|
244
|
+
* is an answer, "not installed" is a different one.
|
|
245
|
+
*/
|
|
246
|
+
sources: trpc_middleware_js_1.adminProcedure.output(zod_1.z.array(LogSourceSchema)).query(async () => {
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const [addonId, dest] of hubLocalDestinations(capabilityRegistry)) {
|
|
249
|
+
const isDefault = addonId === log_history_js_1.DEFAULT_LOG_SOURCE;
|
|
250
|
+
if (dest.describeRead === undefined) {
|
|
251
|
+
out.push({ addonId, isDefault, ...NOT_READABLE });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const caps = await dest.describeRead();
|
|
256
|
+
out.push({ addonId, isDefault, ...caps });
|
|
93
257
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// the same normalisation applied in `subscribe` below.
|
|
105
|
-
tags: e.tags
|
|
106
|
-
? Object.fromEntries(Object.entries(e.tags).map(([k, v]) => [
|
|
107
|
-
k,
|
|
108
|
-
v === undefined ? undefined : String(v),
|
|
109
|
-
]))
|
|
110
|
-
: undefined,
|
|
111
|
-
...(e.meta !== undefined ? { meta: e.meta } : {}),
|
|
112
|
-
}));
|
|
258
|
+
catch (error) {
|
|
259
|
+
out.push({
|
|
260
|
+
addonId,
|
|
261
|
+
isDefault,
|
|
262
|
+
...NOT_READABLE,
|
|
263
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
113
268
|
}),
|
|
114
269
|
/**
|
|
115
270
|
* Drop matching entries from the in-memory ring buffer. Powers
|
|
@@ -149,13 +304,27 @@ function createLogsRouter(logging) {
|
|
|
149
304
|
.input(zod_1.z.object({
|
|
150
305
|
level: LogLevelSchema.optional(),
|
|
151
306
|
tags: LogTagsSchema.optional(),
|
|
307
|
+
/** Same text filter as `query`, applied before the push (D898). */
|
|
308
|
+
text: zod_1.z.string().max(200).optional(),
|
|
152
309
|
}))
|
|
153
310
|
.subscription(({ input }) => {
|
|
311
|
+
const needle = input.text?.trim().toLowerCase() ?? '';
|
|
154
312
|
return (0, trpc_middleware_js_1.iterableSubscription)((push) => {
|
|
155
313
|
return logging.subscribe({
|
|
156
314
|
level: input.level,
|
|
157
315
|
tags: input.tags,
|
|
158
316
|
}, (entry) => {
|
|
317
|
+
if (needle.length > 0 &&
|
|
318
|
+
!(0, system_1.lineContainsText)({
|
|
319
|
+
timestamp: 0,
|
|
320
|
+
level: entry.level,
|
|
321
|
+
message: entry.message,
|
|
322
|
+
scope: entry.scope,
|
|
323
|
+
tags: entry.tags,
|
|
324
|
+
meta: entry.meta,
|
|
325
|
+
}, needle)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
159
328
|
const stringifiedTags = entry.tags
|
|
160
329
|
? Object.fromEntries(Object.entries(entry.tags).map(([k, v]) => [
|
|
161
330
|
k,
|
|
@@ -163,6 +332,7 @@ function createLogsRouter(logging) {
|
|
|
163
332
|
]))
|
|
164
333
|
: undefined;
|
|
165
334
|
push({
|
|
335
|
+
...(entry.id !== undefined ? { id: entry.id } : {}),
|
|
166
336
|
timestamp: entry.timestamp instanceof Date
|
|
167
337
|
? entry.timestamp.toISOString()
|
|
168
338
|
: String(entry.timestamp),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferencesFromRegistry = preferencesFromRegistry;
|
|
4
|
+
exports.preferencesOwnerOf = preferencesOwnerOf;
|
|
5
|
+
exports.createPreferencesRouter = createPreferencesRouter;
|
|
6
|
+
/**
|
|
7
|
+
* Account preferences — fixed core API (D894), not a capability.
|
|
8
|
+
*
|
|
9
|
+
* get the caller's rows (all, or the namespaces asked)
|
|
10
|
+
* put write one namespace; `baseRevision` turns it into a
|
|
11
|
+
* compare-and-set that answers `conflict` with the current row
|
|
12
|
+
* delete remove one namespace
|
|
13
|
+
* changedSince the cheap poll: only rows the client does not hold yet
|
|
14
|
+
*
|
|
15
|
+
* There is no `userId` input anywhere: every call acts on the PRINCIPAL's own
|
|
16
|
+
* rows, admin included — an admin cannot read another user's preferences
|
|
17
|
+
* through this router, and nothing else serves them. The principal must hold
|
|
18
|
+
* `preferences-self` (every person's session does; a unified token only when
|
|
19
|
+
* minted with it), so a share link, a legacy token, an account link and the
|
|
20
|
+
* hub's own mesh principal are refused.
|
|
21
|
+
*
|
|
22
|
+
* Sync is by polling `changedSince`: there is no per-user push channel to a
|
|
23
|
+
* user's sessions, and the event bus is fleet-wide telemetry (D8).
|
|
24
|
+
*
|
|
25
|
+
* The rows live in `local-auth` (it owns the users, and a user's delete wipes
|
|
26
|
+
* them), reached through the hub-internal half of `user-management`.
|
|
27
|
+
*/
|
|
28
|
+
const system_1 = require("@camstack/system");
|
|
29
|
+
const types_1 = require("@camstack/types");
|
|
30
|
+
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
31
|
+
const user_error_js_1 = require("../trpc/user-error.js");
|
|
32
|
+
/** The store behind the hub's in-process `user-management` provider. */
|
|
33
|
+
function preferencesFromRegistry(registry) {
|
|
34
|
+
return () => registry === null
|
|
35
|
+
? null
|
|
36
|
+
: ((0, system_1.getUserManagementInternal)(registry.getSingleton('user-management'))?.preferences ?? null);
|
|
37
|
+
}
|
|
38
|
+
/** The user whose rows this call may touch: the principal itself, or a refusal. */
|
|
39
|
+
function preferencesOwnerOf(principal) {
|
|
40
|
+
if (!(0, types_1.scopesAllowOwnPreferences)(principal.scopes ?? [])) {
|
|
41
|
+
throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'preferences.forbidden', undefined, 'This credential cannot reach account preferences');
|
|
42
|
+
}
|
|
43
|
+
return principal.id;
|
|
44
|
+
}
|
|
45
|
+
function storeOf(deps) {
|
|
46
|
+
const store = deps.preferences();
|
|
47
|
+
if (store === null) {
|
|
48
|
+
throw (0, user_error_js_1.trpcUserError)('PRECONDITION_FAILED', 'preferences.unavailable', undefined, 'Account preferences are not available');
|
|
49
|
+
}
|
|
50
|
+
return store;
|
|
51
|
+
}
|
|
52
|
+
function limitRefusal(error) {
|
|
53
|
+
const reason = (0, system_1.preferencesLimitReasonOf)(error);
|
|
54
|
+
if (reason === 'value-too-large') {
|
|
55
|
+
throw (0, user_error_js_1.trpcUserError)('PAYLOAD_TOO_LARGE', 'preferences.value-too-large', undefined, 'This preference value is too large');
|
|
56
|
+
}
|
|
57
|
+
if (reason === 'too-many-namespaces') {
|
|
58
|
+
throw (0, user_error_js_1.trpcUserError)('BAD_REQUEST', 'preferences.too-many-namespaces', undefined, 'Too many preference namespaces for this account');
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
function createPreferencesRouter(deps) {
|
|
63
|
+
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
64
|
+
get: trpc_middleware_js_1.protectedProcedure
|
|
65
|
+
.input(types_1.PreferencesGetInputSchema)
|
|
66
|
+
.output(types_1.PreferencesGetOutputSchema)
|
|
67
|
+
.query(async ({ ctx, input }) => {
|
|
68
|
+
const owner = preferencesOwnerOf(ctx.user);
|
|
69
|
+
return { rows: [...(await storeOf(deps).get(owner, input.namespaces))] };
|
|
70
|
+
}),
|
|
71
|
+
put: trpc_middleware_js_1.protectedProcedure
|
|
72
|
+
.input(types_1.PreferencesPutInputSchema)
|
|
73
|
+
.output(types_1.PreferencesPutOutputSchema)
|
|
74
|
+
.mutation(async ({ ctx, input }) => {
|
|
75
|
+
const owner = preferencesOwnerOf(ctx.user);
|
|
76
|
+
try {
|
|
77
|
+
return await storeOf(deps).put(owner, input);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return limitRefusal(error);
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
delete: trpc_middleware_js_1.protectedProcedure
|
|
84
|
+
.input(types_1.PreferencesDeleteInputSchema)
|
|
85
|
+
.output(types_1.PreferencesDeleteOutputSchema)
|
|
86
|
+
.mutation(async ({ ctx, input }) => {
|
|
87
|
+
const owner = preferencesOwnerOf(ctx.user);
|
|
88
|
+
return { deleted: await storeOf(deps).delete(owner, input.namespace) };
|
|
89
|
+
}),
|
|
90
|
+
changedSince: trpc_middleware_js_1.protectedProcedure
|
|
91
|
+
.input(types_1.PreferencesChangedSinceInputSchema)
|
|
92
|
+
.output(types_1.PreferencesChangedSinceOutputSchema)
|
|
93
|
+
.query(async ({ ctx, input }) => {
|
|
94
|
+
const owner = preferencesOwnerOf(ctx.user);
|
|
95
|
+
return storeOf(deps).changedSince(owner, input.revisionByNamespace);
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -269,6 +269,35 @@ function answerRefusal(request, reply, status, body, reason, integration) {
|
|
|
269
269
|
}
|
|
270
270
|
return reply.status(status).send(body);
|
|
271
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Who consents. ONLY a person's login session: a session-grade JWT (no
|
|
274
|
+
* `kind` — not a bridge, a challenge or a minted token) naming a user that
|
|
275
|
+
* still exists. Anything else this hub signed — a share link, a script's
|
|
276
|
+
* token, an account link — is refused, because a consent mints an account
|
|
277
|
+
* link with the integration's whole grant (security review 2026-10-21: a
|
|
278
|
+
* share link set as the cookie reached `consent=allow` and came back with a
|
|
279
|
+
* fleet-wide device grant for `userId: ''`).
|
|
280
|
+
*/
|
|
281
|
+
async function consentingUserOf(deps, cookie) {
|
|
282
|
+
let payload;
|
|
283
|
+
try {
|
|
284
|
+
payload = deps.verifyToken(cookie);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return { ok: false, reason: 'session-invalid' };
|
|
288
|
+
}
|
|
289
|
+
if (typeof payload !== 'object' || payload === null || !(0, session_cookie_js_1.isSessionGradeJwtPayload)(payload)) {
|
|
290
|
+
return { ok: false, reason: 'not-a-login-session' };
|
|
291
|
+
}
|
|
292
|
+
const userId = Reflect.get(payload, 'userId');
|
|
293
|
+
const username = Reflect.get(payload, 'username');
|
|
294
|
+
if (typeof userId !== 'string' || userId === '') {
|
|
295
|
+
return { ok: false, reason: 'not-a-login-session' };
|
|
296
|
+
}
|
|
297
|
+
if (!(await deps.isLiveUser(userId)))
|
|
298
|
+
return { ok: false, reason: 'user-gone' };
|
|
299
|
+
return { ok: true, user: { userId, username: typeof username === 'string' ? username : '' } };
|
|
300
|
+
}
|
|
272
301
|
/**
|
|
273
302
|
* An authorize call turned away for want of a usable session.
|
|
274
303
|
*
|
|
@@ -330,13 +359,11 @@ function registerOauth2Routes(fastify, deps) {
|
|
|
330
359
|
if (!cookie) {
|
|
331
360
|
return refuseUnauthenticated(request, reply, deps.logger, call, 'no-session-cookie');
|
|
332
361
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
catch {
|
|
338
|
-
return refuseUnauthenticated(request, reply, deps.logger, call, 'session-invalid');
|
|
362
|
+
const consenting = await consentingUserOf(deps, cookie);
|
|
363
|
+
if (!consenting.ok) {
|
|
364
|
+
return refuseUnauthenticated(request, reply, deps.logger, call, consenting.reason);
|
|
339
365
|
}
|
|
366
|
+
const tokenInfo = consenting.user;
|
|
340
367
|
const registry = deps.getRegistry();
|
|
341
368
|
if (!registry) {
|
|
342
369
|
return answerRefusal(request, reply, 503, { error: 'service_unavailable' }, 'unavailable');
|
|
@@ -367,7 +394,7 @@ function registerOauth2Routes(fastify, deps) {
|
|
|
367
394
|
acceptLanguage: request.headers['accept-language'],
|
|
368
395
|
}),
|
|
369
396
|
displayName: descriptor.displayName,
|
|
370
|
-
username: tokenInfo.username
|
|
397
|
+
username: tokenInfo.username,
|
|
371
398
|
scopeSummary: summariseScopes(descriptor.requestedScopes),
|
|
372
399
|
// Shown, not merely validated. A `redirect_uri` on a private host is
|
|
373
400
|
// admitted by pattern rather than by registration, so the operator is
|
|
@@ -428,13 +455,11 @@ function registerOauth2Routes(fastify, deps) {
|
|
|
428
455
|
if (!cookie) {
|
|
429
456
|
return refuseUnauthenticated(request, reply, deps.logger, call, 'no-session-cookie');
|
|
430
457
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
catch {
|
|
436
|
-
return refuseUnauthenticated(request, reply, deps.logger, call, 'session-invalid');
|
|
458
|
+
const consenting = await consentingUserOf(deps, cookie);
|
|
459
|
+
if (!consenting.ok) {
|
|
460
|
+
return refuseUnauthenticated(request, reply, deps.logger, call, consenting.reason);
|
|
437
461
|
}
|
|
462
|
+
const tokenInfo = consenting.user;
|
|
438
463
|
const registry = deps.getRegistry();
|
|
439
464
|
if (!registry) {
|
|
440
465
|
return answerRefusal(request, reply, 503, { error: 'service_unavailable' }, 'unavailable');
|
|
@@ -478,8 +503,8 @@ function registerOauth2Routes(fastify, deps) {
|
|
|
478
503
|
}
|
|
479
504
|
const { code } = await userMgmt.oauthIssueCode({
|
|
480
505
|
integrationId: v.integration,
|
|
481
|
-
userId: tokenInfo.userId
|
|
482
|
-
username: tokenInfo.username
|
|
506
|
+
userId: tokenInfo.userId,
|
|
507
|
+
username: tokenInfo.username,
|
|
483
508
|
scopes: descriptor.requestedScopes,
|
|
484
509
|
redirectUri: v.redirectUri,
|
|
485
510
|
// Prefer the integration's own public origin (e.g. the operator-selected
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Those methods are declared `auth: 'protected'` (the default in
|
|
12
12
|
* `capability-definition.ts`), which is NOT admin — it is "authenticated, and
|
|
13
|
-
* scope-matched". Measured against the live hub: a
|
|
13
|
+
* scope-matched". Measured against the live hub: a scoped token
|
|
14
14
|
* carrying nothing but `category:system` + `view` — a READ-ONLY grant, the one
|
|
15
15
|
* the deploy runbook mints for CI — read `deviceManager.getDevice` and received
|
|
16
16
|
* `config.password` verbatim, and `deviceManager.listAll` answered with a
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* write-only for admins too is a bigger, UI-side change; it is not needed to
|
|
32
32
|
* close this hole and would regress the form.
|
|
33
33
|
*
|
|
34
|
-
* Share-view
|
|
34
|
+
* Share-view principals never reach this code — they are gated
|
|
35
35
|
* earlier by the fail-closed allowlist in `share-view-access.ts`, which does
|
|
36
36
|
* not expose `getDevice` at all and projects `listAll` down to seven presentation
|
|
37
37
|
* fields. This module is the gate for the tier BETWEEN a share link and an admin.
|
|
@@ -314,7 +314,6 @@ function mountAllCaps(services) {
|
|
|
314
314
|
...first,
|
|
315
315
|
listProfileKinds: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listProfileKinds'),
|
|
316
316
|
listProfiles: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listProfiles'),
|
|
317
|
-
listModels: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listModels'),
|
|
318
317
|
getDefaults: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'getDefaults'),
|
|
319
318
|
getUsage: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'getUsage'),
|
|
320
319
|
listModelCatalog: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listModelCatalog'),
|
|
@@ -2959,6 +2959,16 @@ function createCapRouter_deviceHistory(getProvider, createRemoteProxy) {
|
|
|
2959
2959
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2960
2960
|
return p.queryNode(methodInput);
|
|
2961
2961
|
}),
|
|
2962
|
+
queryMany: trpc_middleware_js_1.adminProcedure
|
|
2963
|
+
.input(types_50.deviceHistoryCapability.methods.queryMany.input.loose())
|
|
2964
|
+
.output(types_50.deviceHistoryCapability.methods.queryMany.output)
|
|
2965
|
+
.query(async ({ input, ctx }) => {
|
|
2966
|
+
const parsedInput = ctx.req !== undefined ? (0, types_3.withoutCallerStamp)(input) : input;
|
|
2967
|
+
const { nodeId, ...methodInput } = parsedInput;
|
|
2968
|
+
const p = resolveProvider('device-history', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2969
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2970
|
+
return p.queryMany(methodInput);
|
|
2971
|
+
}),
|
|
2962
2972
|
declareSeries: trpc_middleware_js_1.adminProcedure
|
|
2963
2973
|
.input(types_50.deviceHistoryCapability.methods.declareSeries.input.loose())
|
|
2964
2974
|
.output(types_50.deviceHistoryCapability.methods.declareSeries.output)
|
|
@@ -5300,10 +5310,10 @@ function createCapRouter_llm(getProvider, _createRemoteProxy) {
|
|
|
5300
5310
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5301
5311
|
return p.testProfile(input);
|
|
5302
5312
|
}),
|
|
5303
|
-
listModels: trpc_middleware_js_1.
|
|
5313
|
+
listModels: trpc_middleware_js_1.adminProcedure
|
|
5304
5314
|
.input(types_69.llmCapability.methods.listModels.input.loose())
|
|
5305
5315
|
.output(types_69.llmCapability.methods.listModels.output)
|
|
5306
|
-
.
|
|
5316
|
+
.mutation(async ({ input, ctx }) => {
|
|
5307
5317
|
const p = requireCapProvider('llm', () => getProvider(ctx));
|
|
5308
5318
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5309
5319
|
return p.listModels(input);
|
|
@@ -5760,14 +5770,51 @@ function createCapRouter_logDestination(getProvider, createRemoteProxy) {
|
|
|
5760
5770
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5761
5771
|
return p.write(methodInput);
|
|
5762
5772
|
}),
|
|
5763
|
-
|
|
5764
|
-
.input(
|
|
5765
|
-
.output(types_75.logDestinationCapability.methods.
|
|
5773
|
+
describeRead: trpc_middleware_js_1.adminProcedure
|
|
5774
|
+
.input(zod_1.z.object({ nodeId: zod_1.z.string().optional(), addonId: zod_1.z.string().optional() }).optional())
|
|
5775
|
+
.output(types_75.logDestinationCapability.methods.describeRead.output)
|
|
5776
|
+
.query(async ({ input, ctx }) => {
|
|
5777
|
+
const p = resolveProvider('log-destination', input?.nodeId, () => getProvider(ctx, input?.addonId), createRemoteProxy);
|
|
5778
|
+
const fn = p.describeRead;
|
|
5779
|
+
if (typeof fn !== 'function') {
|
|
5780
|
+
throw new server_1.TRPCError({
|
|
5781
|
+
code: 'NOT_IMPLEMENTED',
|
|
5782
|
+
message: 'Capability "log-destination" provider does not implement "describeRead"',
|
|
5783
|
+
});
|
|
5784
|
+
}
|
|
5785
|
+
return fn.call(p);
|
|
5786
|
+
}),
|
|
5787
|
+
read: trpc_middleware_js_1.adminProcedure
|
|
5788
|
+
.input(types_75.logDestinationCapability.methods.read.input.loose())
|
|
5789
|
+
.output(types_75.logDestinationCapability.methods.read.output)
|
|
5766
5790
|
.query(async ({ input, ctx }) => {
|
|
5767
5791
|
const { nodeId, addonId, ...methodInput } = input;
|
|
5768
5792
|
const p = resolveProvider('log-destination', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
5793
|
+
const fn = p.read;
|
|
5794
|
+
if (typeof fn !== 'function') {
|
|
5795
|
+
throw new server_1.TRPCError({
|
|
5796
|
+
code: 'NOT_IMPLEMENTED',
|
|
5797
|
+
message: 'Capability "log-destination" provider does not implement "read"',
|
|
5798
|
+
});
|
|
5799
|
+
}
|
|
5769
5800
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5770
|
-
return
|
|
5801
|
+
return fn.call(p, methodInput);
|
|
5802
|
+
}),
|
|
5803
|
+
cancelRead: trpc_middleware_js_1.adminProcedure
|
|
5804
|
+
.input(types_75.logDestinationCapability.methods.cancelRead.input.loose())
|
|
5805
|
+
.output(types_75.logDestinationCapability.methods.cancelRead.output)
|
|
5806
|
+
.mutation(async ({ input, ctx }) => {
|
|
5807
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
5808
|
+
const p = resolveProvider('log-destination', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
5809
|
+
const fn = p.cancelRead;
|
|
5810
|
+
if (typeof fn !== 'function') {
|
|
5811
|
+
throw new server_1.TRPCError({
|
|
5812
|
+
code: 'NOT_IMPLEMENTED',
|
|
5813
|
+
message: 'Capability "log-destination" provider does not implement "cancelRead"',
|
|
5814
|
+
});
|
|
5815
|
+
}
|
|
5816
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5817
|
+
return fn.call(p, methodInput);
|
|
5771
5818
|
}),
|
|
5772
5819
|
});
|
|
5773
5820
|
}
|
|
@@ -12446,42 +12493,6 @@ function createCapRouter_userManagement(getProvider, createRemoteProxy) {
|
|
|
12446
12493
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
12447
12494
|
return p.revokeApiKey(methodInput);
|
|
12448
12495
|
}),
|
|
12449
|
-
createScopedToken: trpc_middleware_js_1.adminProcedure
|
|
12450
|
-
.input(types_142.userManagementCapability.methods.createScopedToken.input.loose())
|
|
12451
|
-
.output(types_142.userManagementCapability.methods.createScopedToken.output)
|
|
12452
|
-
.mutation(async ({ input, ctx }) => {
|
|
12453
|
-
const { nodeId, ...methodInput } = input;
|
|
12454
|
-
const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
12455
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
12456
|
-
return p.createScopedToken(methodInput);
|
|
12457
|
-
}),
|
|
12458
|
-
revokeScopedToken: trpc_middleware_js_1.adminProcedure
|
|
12459
|
-
.input(types_142.userManagementCapability.methods.revokeScopedToken.input.loose())
|
|
12460
|
-
.output(types_142.userManagementCapability.methods.revokeScopedToken.output)
|
|
12461
|
-
.mutation(async ({ input, ctx }) => {
|
|
12462
|
-
const { nodeId, ...methodInput } = input;
|
|
12463
|
-
const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
12464
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
12465
|
-
return p.revokeScopedToken(methodInput);
|
|
12466
|
-
}),
|
|
12467
|
-
validateScopedToken: trpc_middleware_js_1.protectedProcedure
|
|
12468
|
-
.input(types_142.userManagementCapability.methods.validateScopedToken.input.loose())
|
|
12469
|
-
.output(types_142.userManagementCapability.methods.validateScopedToken.output)
|
|
12470
|
-
.query(async ({ input, ctx }) => {
|
|
12471
|
-
const { nodeId, ...methodInput } = input;
|
|
12472
|
-
const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
12473
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
12474
|
-
return p.validateScopedToken(methodInput);
|
|
12475
|
-
}),
|
|
12476
|
-
listScopedTokens: trpc_middleware_js_1.adminProcedure
|
|
12477
|
-
.input(types_142.userManagementCapability.methods.listScopedTokens.input.loose())
|
|
12478
|
-
.output(types_142.userManagementCapability.methods.listScopedTokens.output)
|
|
12479
|
-
.query(async ({ input, ctx }) => {
|
|
12480
|
-
const { nodeId, ...methodInput } = input;
|
|
12481
|
-
const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
12482
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
12483
|
-
return p.listScopedTokens(methodInput);
|
|
12484
|
-
}),
|
|
12485
12496
|
setupTotp: trpc_middleware_js_1.adminProcedure
|
|
12486
12497
|
.input(types_142.userManagementCapability.methods.setupTotp.input.loose())
|
|
12487
12498
|
.output(types_142.userManagementCapability.methods.setupTotp.output)
|