@camstack/server 1.1.49 → 1.1.51

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
- // Re-fetch the user record so scopes / allowedDevices reflect
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
- const userMgmt = registry?.getSingleton('user-management');
381
- if (!userMgmt) {
382
- throw new Error('Login unavailable — `user-management` capability not registered');
383
- }
384
- const fresh = typeof userMgmt.listUsers === 'function'
385
- ? (await userMgmt.listUsers()).find((u) => u.id === claims.userId)
386
- : null;
387
- if (!fresh) {
388
- throw new Error('User no longer exists');
389
- }
390
- const sessionToken = auth.signToken({
391
- userId: fresh.id,
392
- username: fresh.username,
393
- isAdmin: fresh.isAdmin,
394
- allowedProviders: fresh.allowedProviders ?? '*',
395
- allowedDevices: fresh.allowedDevices ?? {},
396
- scopes: fresh.scopes ?? [],
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
- return {
399
- token: sessionToken,
400
- user: { id: fresh.id, username: fresh.username, isAdmin: fresh.isAdmin },
401
- requiresTotp: false,
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.49",
3
+ "version": "1.1.51",
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.42",
26
+ "@camstack/addon-admin-ui": "1.1.43",
27
27
  "@camstack/addon-advanced-notifier": "1.1.21",
28
- "@camstack/addon-auth": "1.1.4",
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.50",
32
- "@camstack/addon-pipeline-orchestrator": "1.1.37",
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.20",
34
+ "@camstack/sdk": "1.1.21",
35
35
  "@camstack/shm-ring": "1.0.21",
36
- "@camstack/system": "1.1.39",
37
- "@camstack/types": "1.1.36",
36
+ "@camstack/system": "1.1.40",
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",