@camstack/server 1.1.48 → 1.1.50
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.
|
@@ -69,6 +69,10 @@ const LoginResultSchema = zod_1.z.object({
|
|
|
69
69
|
const OptionsJsonSchema = zod_1.z.object({ optionsJSON: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()) });
|
|
70
70
|
const FinishRegistrationSchema = zod_1.z.object({ success: zod_1.z.literal(true), credentialId: zod_1.z.string() });
|
|
71
71
|
const FinishAuthenticationSchema = zod_1.z.object({ verified: zod_1.z.boolean() });
|
|
72
|
+
const FinishDiscoverableAuthenticationSchema = zod_1.z.object({
|
|
73
|
+
verified: zod_1.z.boolean(),
|
|
74
|
+
userId: zod_1.z.string().nullable(),
|
|
75
|
+
});
|
|
72
76
|
const RemovePasskeySchema = zod_1.z.object({ success: zod_1.z.literal(true) });
|
|
73
77
|
function proxyPasskeyProvider(proxy) {
|
|
74
78
|
const call = async (method, params) => {
|
|
@@ -82,6 +86,8 @@ function proxyPasskeyProvider(proxy) {
|
|
|
82
86
|
finishRegistration: async (input) => FinishRegistrationSchema.parse(await call('finishRegistration', input)),
|
|
83
87
|
beginAuthentication: async (input) => OptionsJsonSchema.parse(await call('beginAuthentication', input)),
|
|
84
88
|
finishAuthentication: async (input) => FinishAuthenticationSchema.parse(await call('finishAuthentication', input)),
|
|
89
|
+
beginDiscoverableAuthentication: async (input) => OptionsJsonSchema.parse(await call('beginDiscoverableAuthentication', input)),
|
|
90
|
+
finishDiscoverableAuthentication: async (input) => FinishDiscoverableAuthenticationSchema.parse(await call('finishDiscoverableAuthentication', input)),
|
|
85
91
|
listPasskeys: async (input) => zod_1.z.array(types_1.PasskeySummarySchema).parse(await call('listPasskeys', input)),
|
|
86
92
|
removePasskey: async (input) => RemovePasskeySchema.parse(await call('removePasskey', input)),
|
|
87
93
|
};
|
|
@@ -204,6 +210,38 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null)
|
|
|
204
210
|
}
|
|
205
211
|
return shareTokens;
|
|
206
212
|
};
|
|
213
|
+
/**
|
|
214
|
+
* Mint the REAL session JWT for `userId` — the single shared tail of
|
|
215
|
+
* every successful login leg (TOTP second factor, passkey second
|
|
216
|
+
* factor, usernameless passkey-first). Re-fetches the user record so
|
|
217
|
+
* scopes / allowedDevices reflect any admin change made mid-flow —
|
|
218
|
+
* signing from stale claims could mint a stale-scope session.
|
|
219
|
+
*/
|
|
220
|
+
const mintSessionForUserId = async (userId) => {
|
|
221
|
+
const userMgmt = registry?.getSingleton('user-management');
|
|
222
|
+
if (!userMgmt) {
|
|
223
|
+
throw new Error('Login unavailable — `user-management` capability not registered');
|
|
224
|
+
}
|
|
225
|
+
const fresh = typeof userMgmt.listUsers === 'function'
|
|
226
|
+
? (await userMgmt.listUsers()).find((u) => u.id === userId)
|
|
227
|
+
: null;
|
|
228
|
+
if (!fresh) {
|
|
229
|
+
throw new Error('User no longer exists');
|
|
230
|
+
}
|
|
231
|
+
const sessionToken = auth.signToken({
|
|
232
|
+
userId: fresh.id,
|
|
233
|
+
username: fresh.username,
|
|
234
|
+
isAdmin: fresh.isAdmin,
|
|
235
|
+
allowedProviders: fresh.allowedProviders ?? '*',
|
|
236
|
+
allowedDevices: fresh.allowedDevices ?? {},
|
|
237
|
+
scopes: fresh.scopes ?? [],
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
token: sessionToken,
|
|
241
|
+
user: { id: fresh.id, username: fresh.username, isAdmin: fresh.isAdmin },
|
|
242
|
+
requiresTotp: false,
|
|
243
|
+
};
|
|
244
|
+
};
|
|
207
245
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
208
246
|
login: trpc_middleware_js_1.publicProcedure
|
|
209
247
|
.input(zod_1.z.object({ username: zod_1.z.string(), password: zod_1.z.string() }))
|
|
@@ -309,28 +347,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null)
|
|
|
309
347
|
if (!verify.valid) {
|
|
310
348
|
throw new Error('Invalid TOTP code');
|
|
311
349
|
}
|
|
312
|
-
|
|
313
|
-
// any admin change made between the two legs. Without this we
|
|
314
|
-
// could mint a stale-scope session.
|
|
315
|
-
const fresh = typeof userMgmt.listUsers === 'function'
|
|
316
|
-
? (await userMgmt.listUsers()).find((u) => u.id === claims.userId)
|
|
317
|
-
: null;
|
|
318
|
-
if (!fresh) {
|
|
319
|
-
throw new Error('User no longer exists');
|
|
320
|
-
}
|
|
321
|
-
const sessionToken = auth.signToken({
|
|
322
|
-
userId: fresh.id,
|
|
323
|
-
username: fresh.username,
|
|
324
|
-
isAdmin: fresh.isAdmin,
|
|
325
|
-
allowedProviders: fresh.allowedProviders ?? '*',
|
|
326
|
-
allowedDevices: fresh.allowedDevices ?? {},
|
|
327
|
-
scopes: fresh.scopes ?? [],
|
|
328
|
-
});
|
|
329
|
-
return {
|
|
330
|
-
token: sessionToken,
|
|
331
|
-
user: { id: fresh.id, username: fresh.username, isAdmin: fresh.isAdmin },
|
|
332
|
-
requiresTotp: false,
|
|
333
|
-
};
|
|
350
|
+
return mintSessionForUserId(claims.userId);
|
|
334
351
|
}),
|
|
335
352
|
/**
|
|
336
353
|
* Passkey second leg — step 1 of 2. Accepts the challenge token
|
|
@@ -377,29 +394,55 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null)
|
|
|
377
394
|
if (!result.verified) {
|
|
378
395
|
throw new Error('Passkey verification failed');
|
|
379
396
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
+
return mintSessionForUserId(claims.userId);
|
|
398
|
+
}),
|
|
399
|
+
/**
|
|
400
|
+
* Usernameless (passkey-FIRST) login — step 1 of 2. PUBLIC. Returns
|
|
401
|
+
* WebAuthn assertion options with an EMPTY `allowCredentials` list
|
|
402
|
+
* (the standard discoverable-credentials flow): the browser offers
|
|
403
|
+
* every resident passkey it holds for this RP and the user picks one
|
|
404
|
+
* — no username, no password. The challenge is minted and stored by
|
|
405
|
+
* the `user-passkeys` provider, deliberately NOT bound to any user.
|
|
406
|
+
*
|
|
407
|
+
* Abuse surface: mirrors `login` — no rate limiting exists on any
|
|
408
|
+
* public auth procedure today; the challenge store is bounded only
|
|
409
|
+
* by its 5-min TTL prune.
|
|
410
|
+
*/
|
|
411
|
+
passkeyLoginBegin: trpc_middleware_js_1.publicProcedure
|
|
412
|
+
.input(zod_1.z.void())
|
|
413
|
+
.output(OptionsJsonSchema)
|
|
414
|
+
.mutation(async () => {
|
|
415
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
416
|
+
if (!provider)
|
|
417
|
+
throw new Error('Passkey authentication is not available');
|
|
418
|
+
return provider.beginDiscoverableAuthentication({});
|
|
419
|
+
}),
|
|
420
|
+
/**
|
|
421
|
+
* Usernameless login — step 2 of 2. PUBLIC. Submits the browser
|
|
422
|
+
* assertion; the provider resolves the credential's OWNER, verifies
|
|
423
|
+
* the assertion (challenge + public key + counter + required user
|
|
424
|
+
* verification), and on success we mint the SAME session JWT shape
|
|
425
|
+
* `auth.login` mints — via the shared `mintSessionForUserId` tail
|
|
426
|
+
* (fresh user re-fetch for up-to-date scopes).
|
|
427
|
+
*
|
|
428
|
+
* NO second-factor bounce: a discoverable passkey assertion with
|
|
429
|
+
* user verification is already a strong (possession + inherence/
|
|
430
|
+
* knowledge) factor — enrolled TOTP is irrelevant on this path.
|
|
431
|
+
*/
|
|
432
|
+
passkeyLoginFinish: trpc_middleware_js_1.publicProcedure
|
|
433
|
+
.input(zod_1.z.object({ response: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()) }))
|
|
434
|
+
.output(LoginResultSchema)
|
|
435
|
+
.mutation(async ({ input }) => {
|
|
436
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
437
|
+
if (!provider)
|
|
438
|
+
throw new Error('Passkey authentication is not available');
|
|
439
|
+
const result = await provider.finishDiscoverableAuthentication({
|
|
440
|
+
response: input.response,
|
|
397
441
|
});
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
};
|
|
442
|
+
if (!result.verified || !result.userId) {
|
|
443
|
+
throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'Passkey verification failed' });
|
|
444
|
+
}
|
|
445
|
+
return mintSessionForUserId(result.userId);
|
|
403
446
|
}),
|
|
404
447
|
me: trpc_middleware_js_1.protectedProcedure
|
|
405
448
|
.input(zod_1.z.void())
|
|
@@ -10,6 +10,12 @@ exports.createLiveEventsRouter = createLiveEventsRouter;
|
|
|
10
10
|
const zod_1 = require("zod");
|
|
11
11
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
12
12
|
const share_view_access_js_1 = require("../trpc/share-view-access.js");
|
|
13
|
+
// The pushed wire shape is `SystemEvent` from @camstack/types — the exact
|
|
14
|
+
// type `EventBusService.subscribe` hands its handlers. It MUST be a type
|
|
15
|
+
// exported from @camstack/types (not a local interface): the AppRouter
|
|
16
|
+
// type expansion (`scripts/generate-api-types.ts`) prints subscription
|
|
17
|
+
// output types by name, and a router-local name is unresolvable from the
|
|
18
|
+
// generated `packages/types/src/generated/addon-api.ts`.
|
|
13
19
|
function createLiveEventsRouter(eb, ar) {
|
|
14
20
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
15
21
|
recentSystemEvents: trpc_middleware_js_1.protectedProcedure
|
|
@@ -7523,6 +7523,24 @@ function createCapRouter_userPasskeys(getProvider, createRemoteProxy) {
|
|
|
7523
7523
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
7524
7524
|
return p.finishAuthentication(methodInput);
|
|
7525
7525
|
}),
|
|
7526
|
+
beginDiscoverableAuthentication: trpc_middleware_js_1.protectedProcedure
|
|
7527
|
+
.input(types_105.userPasskeysCapability.methods.beginDiscoverableAuthentication.input.loose())
|
|
7528
|
+
.output(types_105.userPasskeysCapability.methods.beginDiscoverableAuthentication.output)
|
|
7529
|
+
.mutation(async ({ input, ctx }) => {
|
|
7530
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
7531
|
+
const p = resolveProvider('user-passkeys', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
7532
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
7533
|
+
return p.beginDiscoverableAuthentication(methodInput);
|
|
7534
|
+
}),
|
|
7535
|
+
finishDiscoverableAuthentication: trpc_middleware_js_1.protectedProcedure
|
|
7536
|
+
.input(types_105.userPasskeysCapability.methods.finishDiscoverableAuthentication.input.loose())
|
|
7537
|
+
.output(types_105.userPasskeysCapability.methods.finishDiscoverableAuthentication.output)
|
|
7538
|
+
.mutation(async ({ input, ctx }) => {
|
|
7539
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
7540
|
+
const p = resolveProvider('user-passkeys', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
7541
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
7542
|
+
return p.finishDiscoverableAuthentication(methodInput);
|
|
7543
|
+
}),
|
|
7526
7544
|
listPasskeys: trpc_middleware_js_1.adminProcedure
|
|
7527
7545
|
.input(types_105.userPasskeysCapability.methods.listPasskeys.input.loose())
|
|
7528
7546
|
.output(types_105.userPasskeysCapability.methods.listPasskeys.output)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.50",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -23,18 +23,18 @@
|
|
|
23
23
|
"test:watch": "vitest"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@camstack/addon-admin-ui": "1.1.
|
|
26
|
+
"@camstack/addon-admin-ui": "1.1.43",
|
|
27
27
|
"@camstack/addon-advanced-notifier": "1.1.21",
|
|
28
|
-
"@camstack/addon-auth": "1.1.
|
|
28
|
+
"@camstack/addon-auth": "1.1.5",
|
|
29
29
|
"@camstack/addon-decoder-nodeav": "1.1.9",
|
|
30
30
|
"@camstack/addon-notifiers": "1.1.21",
|
|
31
|
-
"@camstack/addon-pipeline": "1.1.
|
|
32
|
-
"@camstack/addon-pipeline-orchestrator": "1.1.
|
|
31
|
+
"@camstack/addon-pipeline": "1.1.51",
|
|
32
|
+
"@camstack/addon-pipeline-orchestrator": "1.1.38",
|
|
33
33
|
"@camstack/addon-post-analysis": "1.1.23",
|
|
34
|
-
"@camstack/sdk": "1.1.
|
|
34
|
+
"@camstack/sdk": "1.1.21",
|
|
35
35
|
"@camstack/shm-ring": "1.0.21",
|
|
36
36
|
"@camstack/system": "1.1.39",
|
|
37
|
-
"@camstack/types": "1.1.
|
|
37
|
+
"@camstack/types": "1.1.37",
|
|
38
38
|
"@camstack/ui-library": "1.1.31",
|
|
39
39
|
"@fastify/compress": "^9.0.0",
|
|
40
40
|
"@fastify/cookie": "^11.0.2",
|