@byline/admin 6.7.2 → 6.8.0

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.
Files changed (86) hide show
  1. package/dist/modules/admin-account/components/change-password.js +1 -1
  2. package/dist/modules/admin-account/components/container.js +18 -3
  3. package/dist/modules/admin-account/components/container.module.js +2 -0
  4. package/dist/modules/admin-account/components/container_module.css +7 -0
  5. package/dist/modules/admin-account/components/preferences.js +1 -1
  6. package/dist/modules/admin-account/components/update.js +1 -1
  7. package/dist/modules/admin-roles/components/create.js +1 -1
  8. package/dist/modules/admin-roles/components/permissions.js +1 -1
  9. package/dist/modules/admin-roles/components/update.js +1 -1
  10. package/dist/modules/admin-users/abilities.d.ts +1 -0
  11. package/dist/modules/admin-users/abilities.js +8 -1
  12. package/dist/modules/admin-users/components/create.js +1 -1
  13. package/dist/modules/admin-users/components/roles.js +1 -1
  14. package/dist/modules/admin-users/components/set-password.js +1 -1
  15. package/dist/modules/admin-users/components/update.js +1 -1
  16. package/dist/modules/auth/auth-maintenance-repository.d.ts +31 -0
  17. package/dist/modules/auth/auth-maintenance-repository.js +1 -0
  18. package/dist/modules/auth/auth-maintenance.d.ts +11 -0
  19. package/dist/modules/auth/auth-maintenance.js +39 -0
  20. package/dist/modules/auth/auth-maintenance.test.node.d.ts +1 -0
  21. package/dist/modules/auth/components/passkey-browser.d.ts +14 -0
  22. package/dist/modules/auth/components/passkey-browser.js +118 -0
  23. package/dist/modules/auth/components/passkey-browser.test.node.d.ts +1 -0
  24. package/dist/modules/auth/components/passkey-manager.d.ts +26 -0
  25. package/dist/modules/auth/components/passkey-manager.js +406 -0
  26. package/dist/modules/auth/components/passkey-manager.module.js +22 -0
  27. package/dist/modules/auth/components/passkey-manager_module.css +104 -0
  28. package/dist/modules/auth/components/sign-in-form.js +62 -3
  29. package/dist/modules/auth/components/sign-in-form.module.js +5 -1
  30. package/dist/modules/auth/components/sign-in-form_module.css +40 -0
  31. package/dist/modules/auth/index.d.ts +5 -0
  32. package/dist/modules/auth/index.js +3 -0
  33. package/dist/modules/auth/jwt-session-provider.d.ts +12 -4
  34. package/dist/modules/auth/jwt-session-provider.js +135 -94
  35. package/dist/modules/auth/native-passkey-provider.d.ts +72 -0
  36. package/dist/modules/auth/native-passkey-provider.js +618 -0
  37. package/dist/modules/auth/passkey-rate-limiter.d.ts +14 -0
  38. package/dist/modules/auth/passkey-rate-limiter.js +60 -0
  39. package/dist/modules/auth/passkey-rate-limiter.test.node.d.ts +8 -0
  40. package/dist/modules/auth/passkey-storage-validation.d.ts +14 -0
  41. package/dist/modules/auth/passkey-storage-validation.js +59 -0
  42. package/dist/modules/auth/passkeys-repository.d.ts +96 -0
  43. package/dist/modules/auth/passkeys-repository.js +1 -0
  44. package/dist/modules/auth/refresh-tokens-repository.d.ts +1 -1
  45. package/dist/modules/auth/sign-in-rate-limiter.d.ts +2 -0
  46. package/dist/modules/auth/sign-in-rate-limiter.js +1 -1
  47. package/dist/services/admin-services-context.d.ts +1 -1
  48. package/dist/services/admin-services-types.d.ts +60 -0
  49. package/dist/store.d.ts +4 -0
  50. package/package.json +16 -9
  51. package/src/modules/admin-account/components/change-password.tsx +1 -1
  52. package/src/modules/admin-account/components/container.module.css +9 -0
  53. package/src/modules/admin-account/components/container.tsx +12 -3
  54. package/src/modules/admin-account/components/preferences.tsx +1 -1
  55. package/src/modules/admin-account/components/update.tsx +1 -1
  56. package/src/modules/admin-roles/components/create.tsx +1 -1
  57. package/src/modules/admin-roles/components/permissions.tsx +1 -1
  58. package/src/modules/admin-roles/components/update.tsx +1 -1
  59. package/src/modules/admin-users/abilities.ts +7 -0
  60. package/src/modules/admin-users/components/create.tsx +1 -1
  61. package/src/modules/admin-users/components/roles.tsx +1 -1
  62. package/src/modules/admin-users/components/set-password.tsx +1 -1
  63. package/src/modules/admin-users/components/update.tsx +1 -1
  64. package/src/modules/auth/auth-maintenance-repository.ts +26 -0
  65. package/src/modules/auth/auth-maintenance.test.node.ts +110 -0
  66. package/src/modules/auth/auth-maintenance.ts +62 -0
  67. package/src/modules/auth/components/passkey-browser.test.node.ts +188 -0
  68. package/src/modules/auth/components/passkey-browser.ts +139 -0
  69. package/src/modules/auth/components/passkey-manager.module.css +100 -0
  70. package/src/modules/auth/components/passkey-manager.test.tsx +189 -0
  71. package/src/modules/auth/components/passkey-manager.tsx +386 -0
  72. package/src/modules/auth/components/sign-in-form.module.css +37 -0
  73. package/src/modules/auth/components/sign-in-form.test.tsx +92 -0
  74. package/src/modules/auth/components/sign-in-form.tsx +65 -4
  75. package/src/modules/auth/index.ts +15 -0
  76. package/src/modules/auth/jwt-session-provider.ts +116 -44
  77. package/src/modules/auth/native-passkey-provider.ts +710 -0
  78. package/src/modules/auth/passkey-rate-limiter.test.node.ts +83 -0
  79. package/src/modules/auth/passkey-rate-limiter.ts +76 -0
  80. package/src/modules/auth/passkey-storage-validation.ts +108 -0
  81. package/src/modules/auth/passkeys-repository.ts +97 -0
  82. package/src/modules/auth/refresh-tokens-repository.ts +1 -1
  83. package/src/modules/auth/sign-in-rate-limiter.ts +1 -1
  84. package/src/services/admin-services-context.tsx +2 -0
  85. package/src/services/admin-services-types.ts +60 -0
  86. package/src/store.ts +4 -0
@@ -19,9 +19,14 @@
19
19
  * Clerk, institutional SSO) should be shipped as separate packages
20
20
  * against `@byline/auth` rather than added here.
21
21
  */
22
+ export { createAuthMaintenanceTasks } from './auth-maintenance.js';
22
23
  export { JwtSessionProvider, type JwtSessionProviderConfig, type NativeSessionEvent, } from './jwt-session-provider.js';
24
+ export { createPasskeyRequestLimiter } from './passkey-rate-limiter.js';
25
+ export { assertPasskeyAttempt, assertPasskeyBytes, assertPasskeyClaim, assertPasskeyTokenHash, } from './passkey-storage-validation.js';
23
26
  export { hashPassword, verifyPassword } from './password.js';
24
27
  export { resolveActor } from './resolve-actor.js';
25
28
  export { createPasswordSignInLimiter, type SignInLimiterOptions, type SignInRateLimitPolicy, type SignInRateLimitStore, type SignInSecurityEvent, } from './sign-in-rate-limiter.js';
29
+ export type { AuthMaintenanceRepository } from './auth-maintenance-repository.js';
26
30
  export type { LoginSessionRow, LoginSessionsRepository } from './login-sessions-repository.js';
31
+ export type { PasskeyAttemptRow, PasskeyClaimInput, PasskeyCredentialRow, PasskeysRepository, } from './passkeys-repository.js';
27
32
  export type { IssueRefreshTokenInput, RefreshTokenRow, RefreshTokensRepository, } from './refresh-tokens-repository.js';
@@ -1,4 +1,7 @@
1
+ export { createAuthMaintenanceTasks } from "./auth-maintenance.js";
1
2
  export { JwtSessionProvider } from "./jwt-session-provider.js";
3
+ export { createPasskeyRequestLimiter } from "./passkey-rate-limiter.js";
4
+ export { assertPasskeyAttempt, assertPasskeyBytes, assertPasskeyClaim, assertPasskeyTokenHash } from "./passkey-storage-validation.js";
2
5
  export { hashPassword, verifyPassword } from "./password.js";
3
6
  export { resolveActor } from "./resolve-actor.js";
4
7
  export { createPasswordSignInLimiter } from "./sign-in-rate-limiter.js";
@@ -5,13 +5,20 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import { type AdminAuth, type RefreshSessionArgs, type RevokeSessionArgs, type SessionProvider, type SessionProviderCapabilities, type SessionTokens, type SignInResult, type SignInWithPasswordArgs } from '@byline/auth';
8
+ import { type AdminAuth, type PasskeyProvider, type RefreshSessionArgs, type RevokeSessionArgs, type SessionProvider, type SessionProviderCapabilities, type SessionTokens, type SignInResult, type SignInWithPasswordArgs } from '@byline/auth';
9
+ import type { PasskeyConfig } from '@byline/core';
9
10
  import type { AdminStore } from '../../store.js';
10
- export interface NativeSessionEvent {
11
+ export type NativeSessionEvent = {
11
12
  type: 'refresh_attempted' | 'refresh_completed' | 'refresh_contested';
12
- }
13
+ } | {
14
+ type: 'passkey_attempted' | 'passkey_completed' | 'passkey_failed' | 'passkey_admission_denied';
15
+ purpose: 'sign-in-options' | 'sign-in' | 'password-proof' | 'proof-options' | 'proof' | 'enrollment-options' | 'enrollment' | 'list' | 'rename' | 'remove';
16
+ reason?: 'invalid-proof' | 'stale-authority' | 'forbidden' | 'limit' | 'unavailable' | 'capacity' | 'network';
17
+ };
13
18
  export interface JwtSessionProviderConfig {
14
- /** Best-effort, token-free renewal telemetry. Errors never change authentication outcomes. */
19
+ /** Optional explicit WebAuthn trust configuration; disabled when absent. */
20
+ passkeys?: PasskeyConfig;
21
+ /** Best-effort, token-free authentication telemetry. Errors never change authentication outcomes. */
15
22
  onEvent?: (event: NativeSessionEvent) => void;
16
23
  /**
17
24
  * Adapter-backed admin repositories. Construct via the DB adapter's
@@ -40,6 +47,7 @@ export interface JwtSessionProviderConfig {
40
47
  export declare class JwtSessionProvider implements SessionProvider {
41
48
  #private;
42
49
  readonly capabilities: SessionProviderCapabilities;
50
+ readonly passkeys?: PasskeyProvider;
43
51
  constructor(config: JwtSessionProviderConfig);
44
52
  signInWithPassword(args: SignInWithPasswordArgs): Promise<SignInResult>;
45
53
  verifyAccessToken(token: string): Promise<{
@@ -3,6 +3,7 @@ import { ERR_ACCESS_EXPIRED, ERR_ACCOUNT_DISABLED, ERR_INVALID_CREDENTIALS, ERR_
3
3
  import { passwordSignInSchema } from "@byline/core/validation";
4
4
  import { SignJWT, compactVerify, jwtVerify } from "jose";
5
5
  import { v7 } from "uuid";
6
+ import { NativePasskeyProvider } from "./native-passkey-provider.js";
6
7
  import { verifyPassword } from "./password.js";
7
8
  import { resolveActor, resolveActorFromUser } from "./resolve-actor.js";
8
9
  function _check_private_redeclaration(obj, privateCollection) {
@@ -52,11 +53,11 @@ const CAPABILITIES = {
52
53
  magicLink: false,
53
54
  sso: false
54
55
  };
55
- var _onEvent = /*#__PURE__*/ new WeakMap(), _store = /*#__PURE__*/ new WeakMap(), _signingKey = /*#__PURE__*/ new WeakMap(), _issuer = /*#__PURE__*/ new WeakMap(), _accessTtl = /*#__PURE__*/ new WeakMap(), _refreshTtl = /*#__PURE__*/ new WeakMap(), _now = /*#__PURE__*/ new WeakMap(), _emit = /*#__PURE__*/ new WeakSet(), _observedLogins = /*#__PURE__*/ new WeakSet(), _issueTokens = /*#__PURE__*/ new WeakSet(), _signAccessToken = /*#__PURE__*/ new WeakSet();
56
+ var _onEvent = /*#__PURE__*/ new WeakMap(), jwt_session_provider_store = /*#__PURE__*/ new WeakMap(), _signingKey = /*#__PURE__*/ new WeakMap(), _issuer = /*#__PURE__*/ new WeakMap(), _accessTtl = /*#__PURE__*/ new WeakMap(), _refreshTtl = /*#__PURE__*/ new WeakMap(), _now = /*#__PURE__*/ new WeakMap(), _verifyAccessTokenAtStore = /*#__PURE__*/ new WeakSet(), _emit = /*#__PURE__*/ new WeakSet(), _emitEvent = /*#__PURE__*/ new WeakSet(), _authenticatePasskeySession = /*#__PURE__*/ new WeakSet(), _completeSignIn = /*#__PURE__*/ new WeakSet(), _observedLogins = /*#__PURE__*/ new WeakSet(), _issueTokens = /*#__PURE__*/ new WeakSet(), _signAccessToken = /*#__PURE__*/ new WeakSet();
56
57
  class JwtSessionProvider {
57
58
  async signInWithPassword(args) {
58
59
  const credentials = passwordSignInSchema.parse(args);
59
- const users = _class_private_field_get(this, _store).adminUsers;
60
+ const users = _class_private_field_get(this, jwt_session_provider_store).adminUsers;
60
61
  const row = await users.getByEmailForSignIn(credentials.email);
61
62
  if (!row) {
62
63
  await verifyPassword(credentials.password, DUMMY_HASH_FOR_TIMING);
@@ -74,99 +75,27 @@ class JwtSessionProvider {
74
75
  if (!row.is_enabled) throw ERR_ACCOUNT_DISABLED({
75
76
  message: 'account disabled'
76
77
  });
77
- const previous = await _class_private_method_get(this, _observedLogins, observedLogins).call(this, args);
78
- const withLocks = (work)=>previous.length ? _class_private_field_get(this, _store).withSessionLocks([
79
- row.id,
80
- ...previous.map((login)=>login.admin_user_id)
81
- ], work) : _class_private_field_get(this, _store).withSessionLock(row.id, (store)=>work(store));
82
- return withLocks(async (store)=>{
83
- const current = await store.adminUsers.getByIdForSignIn(row.id);
84
- if (!current?.is_enabled) throw ERR_ACCOUNT_DISABLED({
85
- message: 'account disabled'
86
- });
87
- if (current.password_hash !== row.password_hash || current.session_version !== row.session_version) throw ERR_INVALID_CREDENTIALS({
78
+ return _class_private_method_get(this, _completeSignIn, completeSignIn).call(this, row, args, async (_store, current)=>{
79
+ if (current.password_hash !== row.password_hash) throw ERR_INVALID_CREDENTIALS({
88
80
  message: 'credentials changed during sign-in'
89
81
  });
90
- for (const observed of previous){
91
- const login = await store.loginSessions.findById(observed.id);
92
- if (login && login.admin_user_id === observed.admin_user_id) await store.loginSessions.revoke(login.id, _class_private_field_get(this, _now).call(this));
93
- }
94
- await store.adminUsers.recordLoginSuccess(row.id, args.ip ?? null);
95
- const actor = await resolveActorFromUser(store, current);
96
- if (!actor) throw ERR_ACCOUNT_DISABLED({
97
- message: 'account disabled'
98
- });
99
- const tokens = await _class_private_method_get(this, _issueTokens, issueTokens).call(this, store, {
100
- adminUserId: row.id,
101
- sessionVersion: current.session_version,
102
- ip: args.ip ?? null,
103
- userAgent: args.userAgent ?? null
104
- });
105
- return {
106
- ...tokens,
107
- actor
108
- };
109
82
  });
110
83
  }
111
84
  async verifyAccessToken(token) {
112
- let payload;
113
- try {
114
- const result = await jwtVerify(token, _class_private_field_get(this, _signingKey), {
115
- issuer: _class_private_field_get(this, _issuer),
116
- algorithms: [
117
- 'HS256'
118
- ],
119
- currentDate: _class_private_field_get(this, _now).call(this),
120
- requiredClaims: [
121
- 'sub',
122
- 'iat',
123
- 'exp',
124
- 'jti',
125
- 'sv',
126
- 'sid'
127
- ]
128
- });
129
- payload = result.payload;
130
- } catch (err) {
131
- if (err instanceof Error && 'code' in err && 'ERR_JWT_EXPIRED' === err.code) throw ERR_ACCESS_EXPIRED({
132
- message: 'access token expired'
133
- });
134
- throw ERR_INVALID_TOKEN({
135
- message: 'access token verification failed',
136
- cause: err
137
- });
138
- }
139
- if ('access' !== payload.typ || 'string' != typeof payload.sub || !validSid(payload.sid) || !Number.isSafeInteger(payload.sv) || payload.sv < 0) throw ERR_INVALID_TOKEN({
140
- message: 'unexpected token type'
141
- });
142
- const current = await _class_private_field_get(this, _store).adminUsers.getByIdForSignIn(payload.sub);
143
- if (!current?.is_enabled) throw ERR_ACCOUNT_DISABLED({
144
- message: 'account disabled or deleted'
145
- });
146
- if (current.session_version !== payload.sv) throw ERR_REVOKED_TOKEN({
147
- message: 'account session generation changed'
148
- });
149
- const login = await _class_private_field_get(this, _store).loginSessions.findById(payload.sid);
150
- if (!login || login.admin_user_id !== payload.sub || login.session_version !== payload.sv || null != login.revoked_at || login.expires_at.getTime() <= _class_private_field_get(this, _now).call(this).getTime()) throw ERR_REVOKED_TOKEN({
151
- message: 'login revoked or expired'
152
- });
153
- const actor = await resolveActorFromUser(_class_private_field_get(this, _store), current);
154
- if (!actor) throw ERR_ACCOUNT_DISABLED({
155
- message: 'account disabled or deleted'
156
- });
85
+ const { actor, sessionId } = await _class_private_method_get(this, _verifyAccessTokenAtStore, verifyAccessTokenAtStore).call(this, token, _class_private_field_get(this, jwt_session_provider_store));
157
86
  return {
158
87
  actor,
159
- sessionId: payload.sid
88
+ sessionId
160
89
  };
161
90
  }
162
91
  async refreshSession(args) {
163
92
  _class_private_method_get(this, _emit, emit).call(this, 'refresh_attempted');
164
93
  const hash = hashToken(args.refreshToken);
165
- const observed = await _class_private_field_get(this, _store).refreshTokens.findByHash(hash);
94
+ const observed = await _class_private_field_get(this, jwt_session_provider_store).refreshTokens.findByHash(hash);
166
95
  if (!observed) throw ERR_INVALID_TOKEN({
167
96
  message: 'refresh token not recognised'
168
97
  });
169
- const outcome = await _class_private_field_get(this, _store).withSessionLock(observed.admin_user_id, async (store, user)=>{
98
+ const outcome = await _class_private_field_get(this, jwt_session_provider_store).withSessionLock(observed.admin_user_id, async (store, user)=>{
170
99
  if (!user?.is_enabled) throw ERR_ACCOUNT_DISABLED({
171
100
  message: 'account disabled or deleted'
172
101
  });
@@ -242,21 +171,25 @@ class JwtSessionProvider {
242
171
  if (args.expectedSessionId && logins.some((login)=>login.id !== args.expectedSessionId)) throw ERR_SESSION_CHANGED({
243
172
  message: 'session changed before logout'
244
173
  });
245
- for (const login of logins)await _class_private_field_get(this, _store).withSessionLock(login.admin_user_id, async (store)=>{
174
+ for (const login of logins)await _class_private_field_get(this, jwt_session_provider_store).withSessionLock(login.admin_user_id, async (store)=>{
246
175
  await store.loginSessions.revoke(login.id, _class_private_field_get(this, _now).call(this));
247
176
  });
248
177
  if (args.refreshToken) {
249
- const row = await _class_private_field_get(this, _store).refreshTokens.findByHash(hashToken(args.refreshToken));
250
- if (row) await _class_private_field_get(this, _store).withSessionLock(row.admin_user_id, async (store)=>{
178
+ const row = await _class_private_field_get(this, jwt_session_provider_store).refreshTokens.findByHash(hashToken(args.refreshToken));
179
+ if (row) await _class_private_field_get(this, jwt_session_provider_store).withSessionLock(row.admin_user_id, async (store)=>{
251
180
  await store.refreshTokens.revokeChain(row.id, _class_private_field_get(this, _now).call(this));
252
181
  });
253
182
  }
254
183
  }
255
184
  async resolveActor(adminUserId) {
256
- return resolveActor(_class_private_field_get(this, _store), adminUserId);
185
+ return resolveActor(_class_private_field_get(this, jwt_session_provider_store), adminUserId);
257
186
  }
258
187
  constructor(config){
188
+ _class_private_method_init(this, _verifyAccessTokenAtStore);
259
189
  _class_private_method_init(this, _emit);
190
+ _class_private_method_init(this, _emitEvent);
191
+ _class_private_method_init(this, _authenticatePasskeySession);
192
+ _class_private_method_init(this, _completeSignIn);
260
193
  _class_private_method_init(this, _observedLogins);
261
194
  _class_private_method_init(this, _issueTokens);
262
195
  _class_private_method_init(this, _signAccessToken);
@@ -264,7 +197,7 @@ class JwtSessionProvider {
264
197
  writable: true,
265
198
  value: void 0
266
199
  });
267
- _class_private_field_init(this, _store, {
200
+ _class_private_field_init(this, jwt_session_provider_store, {
268
201
  writable: true,
269
202
  value: void 0
270
203
  });
@@ -288,25 +221,133 @@ class JwtSessionProvider {
288
221
  writable: true,
289
222
  value: void 0
290
223
  });
291
- this.capabilities = CAPABILITIES;
292
224
  _class_private_field_set(this, _onEvent, config.onEvent);
293
- _class_private_field_set(this, _store, config.store);
225
+ _class_private_field_set(this, jwt_session_provider_store, config.store);
294
226
  _class_private_field_set(this, _signingKey, 'string' == typeof config.signingSecret ? new TextEncoder().encode(config.signingSecret) : config.signingSecret);
295
227
  if (_class_private_field_get(this, _signingKey).byteLength < 32) throw new Error('JwtSessionProvider: signingSecret must carry at least 32 bytes of entropy (256 bits)');
296
228
  _class_private_field_set(this, _issuer, config.issuer ?? DEFAULT_ISSUER);
297
229
  _class_private_field_set(this, _accessTtl, config.accessTokenTtlSeconds ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS);
298
230
  _class_private_field_set(this, _refreshTtl, config.refreshTokenTtlSeconds ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS);
299
231
  _class_private_field_set(this, _now, config.now ?? (()=>new Date()));
232
+ this.capabilities = config.passkeys ? {
233
+ ...CAPABILITIES,
234
+ passkeys: true
235
+ } : CAPABILITIES;
236
+ if (config.passkeys) this.passkeys = new NativePasskeyProvider(config.passkeys, {
237
+ store: _class_private_field_get(this, jwt_session_provider_store),
238
+ now: _class_private_field_get(this, _now),
239
+ authenticate: (session, store)=>_class_private_method_get(this, _authenticatePasskeySession, authenticatePasskeySession).call(this, session, store),
240
+ signIn: (snapshot, args, recheck)=>_class_private_method_get(this, _completeSignIn, completeSignIn).call(this, snapshot, args, recheck),
241
+ emit: (event)=>_class_private_method_get(this, _emitEvent, emitEvent).call(this, event)
242
+ });
243
+ }
244
+ }
245
+ async function verifyAccessTokenAtStore(token, store) {
246
+ let payload;
247
+ try {
248
+ const result = await jwtVerify(token, _class_private_field_get(this, _signingKey), {
249
+ issuer: _class_private_field_get(this, _issuer),
250
+ algorithms: [
251
+ 'HS256'
252
+ ],
253
+ currentDate: _class_private_field_get(this, _now).call(this),
254
+ requiredClaims: [
255
+ 'sub',
256
+ 'iat',
257
+ 'exp',
258
+ 'jti',
259
+ 'sv',
260
+ 'sid'
261
+ ]
262
+ });
263
+ payload = result.payload;
264
+ } catch (err) {
265
+ if (err instanceof Error && 'code' in err && 'ERR_JWT_EXPIRED' === err.code) throw ERR_ACCESS_EXPIRED({
266
+ message: 'access token expired'
267
+ });
268
+ throw ERR_INVALID_TOKEN({
269
+ message: 'access token verification failed',
270
+ cause: err
271
+ });
300
272
  }
273
+ if ('access' !== payload.typ || 'string' != typeof payload.sub || !validSid(payload.sid) || !Number.isSafeInteger(payload.sv) || payload.sv < 0) throw ERR_INVALID_TOKEN({
274
+ message: 'unexpected token type'
275
+ });
276
+ const current = await store.adminUsers.getByIdForSignIn(payload.sub);
277
+ if (!current?.is_enabled) throw ERR_ACCOUNT_DISABLED({
278
+ message: 'account disabled or deleted'
279
+ });
280
+ if (current.session_version !== payload.sv) throw ERR_REVOKED_TOKEN({
281
+ message: 'account session generation changed'
282
+ });
283
+ const login = await store.loginSessions.findById(payload.sid);
284
+ if (!login || login.admin_user_id !== payload.sub || login.session_version !== payload.sv || null != login.revoked_at || login.expires_at.getTime() <= _class_private_field_get(this, _now).call(this).getTime()) throw ERR_REVOKED_TOKEN({
285
+ message: 'login revoked or expired'
286
+ });
287
+ const actor = await resolveActorFromUser(store, current);
288
+ if (!actor) throw ERR_ACCOUNT_DISABLED({
289
+ message: 'account disabled or deleted'
290
+ });
291
+ return {
292
+ actor,
293
+ sessionId: payload.sid,
294
+ user: current
295
+ };
301
296
  }
302
297
  function emit(type) {
298
+ _class_private_method_get(this, _emitEvent, emitEvent).call(this, {
299
+ type
300
+ });
301
+ }
302
+ function emitEvent(event) {
303
303
  var _this, _this1;
304
304
  try {
305
- null == (_this = _class_private_field_get(_this1 = this, _onEvent)) || _this.call(_this1, {
306
- type
307
- });
305
+ Promise.resolve(null == (_this = _class_private_field_get(_this1 = this, _onEvent)) ? void 0 : _this.call(_this1, event)).catch(()=>{});
308
306
  } catch {}
309
307
  }
308
+ async function authenticatePasskeySession(session, store = _class_private_field_get(this, jwt_session_provider_store)) {
309
+ const result = await _class_private_method_get(this, _verifyAccessTokenAtStore, verifyAccessTokenAtStore).call(this, session.accessToken, store);
310
+ if (result.sessionId !== session.expectedSessionId) throw ERR_SESSION_CHANGED({
311
+ message: 'session changed'
312
+ });
313
+ return result;
314
+ }
315
+ async function completeSignIn(snapshot, args, recheck) {
316
+ const previous = await _class_private_method_get(this, _observedLogins, observedLogins).call(this, args);
317
+ const withLocks = (work)=>previous.length ? _class_private_field_get(this, jwt_session_provider_store).withSessionLocks([
318
+ snapshot.id,
319
+ ...previous.map((login)=>login.admin_user_id)
320
+ ], work) : _class_private_field_get(this, jwt_session_provider_store).withSessionLock(snapshot.id, (store)=>work(store));
321
+ return withLocks(async (store)=>{
322
+ const current = await store.adminUsers.getByIdForSignIn(snapshot.id);
323
+ if (!current?.is_enabled) throw ERR_ACCOUNT_DISABLED({
324
+ message: 'account disabled'
325
+ });
326
+ if (current.session_version !== snapshot.session_version) throw ERR_INVALID_CREDENTIALS({
327
+ message: 'credentials changed during sign-in'
328
+ });
329
+ await recheck(store, current);
330
+ for (const observed of previous){
331
+ const login = await store.loginSessions.findById(observed.id);
332
+ if (login && login.admin_user_id === observed.admin_user_id) await store.loginSessions.revoke(login.id, _class_private_field_get(this, _now).call(this));
333
+ }
334
+ await store.adminUsers.recordLoginSuccess(snapshot.id, args.ip ?? null);
335
+ const actor = await resolveActorFromUser(store, current);
336
+ if (!actor) throw ERR_ACCOUNT_DISABLED({
337
+ message: 'account disabled'
338
+ });
339
+ const tokens = await _class_private_method_get(this, _issueTokens, issueTokens).call(this, store, {
340
+ adminUserId: snapshot.id,
341
+ sessionVersion: current.session_version,
342
+ ip: args.ip ?? null,
343
+ userAgent: args.userAgent ?? null
344
+ });
345
+ return {
346
+ ...tokens,
347
+ actor
348
+ };
349
+ });
350
+ }
310
351
  async function observedLogins(args) {
311
352
  const ids = new Set();
312
353
  if (args.previousAccessToken) {
@@ -320,20 +361,20 @@ async function observedLogins(args) {
320
361
  claims = JSON.parse(new TextDecoder().decode(verified.payload));
321
362
  } catch {}
322
363
  if (claims?.iss === _class_private_field_get(this, _issuer) && 'access' === claims.typ && validSid(claims.sid) && 'string' == typeof claims.sub) {
323
- const login = await _class_private_field_get(this, _store).loginSessions.findById(claims.sid);
364
+ const login = await _class_private_field_get(this, jwt_session_provider_store).loginSessions.findById(claims.sid);
324
365
  if (login?.admin_user_id === claims.sub) ids.add(login.id);
325
366
  }
326
367
  }
327
368
  if (args.previousRefreshToken) {
328
- const token = await _class_private_field_get(this, _store).refreshTokens.findByHash(hashToken(args.previousRefreshToken));
369
+ const token = await _class_private_field_get(this, jwt_session_provider_store).refreshTokens.findByHash(hashToken(args.previousRefreshToken));
329
370
  if (token?.sid) {
330
- const login = await _class_private_field_get(this, _store).loginSessions.findById(token.sid);
371
+ const login = await _class_private_field_get(this, jwt_session_provider_store).loginSessions.findById(token.sid);
331
372
  if (login?.admin_user_id === token.admin_user_id) ids.add(login.id);
332
373
  }
333
374
  }
334
375
  const result = [];
335
376
  for (const id of ids){
336
- const login = await _class_private_field_get(this, _store).loginSessions.findById(id);
377
+ const login = await _class_private_field_get(this, jwt_session_provider_store).loginSessions.findById(id);
337
378
  if (login) result.push(login);
338
379
  }
339
380
  return result;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ * Copyright (c) Infonomic Company Limited
6
+ */
7
+ import { type AdminAuth, type PasskeyAdmissionReason, type PasskeyAuthenticationOptions, type PasskeyCeremony, type PasskeyCredentialSummary, type PasskeyManagementAction, type PasskeyManagementGrant, type PasskeyOperation, type PasskeyProvider, type PasskeyRegistrationOptions, type PasskeyRequestMetadata, type PasskeySession, type SignInResult, type SignInWithPasswordArgs } from '@byline/auth';
8
+ import { type PasskeyConfig } from '@byline/core';
9
+ import type { AdminStore } from '../../store.js';
10
+ import type { AdminUserWithPasswordRow } from '../admin-users/repository.js';
11
+ import type { NativeSessionEvent } from './jwt-session-provider.js';
12
+ type Authenticated = {
13
+ actor: AdminAuth;
14
+ user: AdminUserWithPasswordRow;
15
+ sessionId: string;
16
+ };
17
+ type SignInArgs = Pick<SignInWithPasswordArgs, 'previousAccessToken' | 'previousRefreshToken' | 'ip' | 'userAgent'>;
18
+ type PasskeyEvent = Extract<NativeSessionEvent, {
19
+ purpose: string;
20
+ }>;
21
+ interface NativeDependencies {
22
+ store: AdminStore;
23
+ now(): Date;
24
+ authenticate(session: PasskeySession, store?: AdminStore): Promise<Authenticated>;
25
+ /** Private constructor closure supplied only by JwtSessionProvider; never publicly exported. */
26
+ signIn(snapshot: AdminUserWithPasswordRow, args: SignInArgs, recheck: (store: AdminStore, user: AdminUserWithPasswordRow) => Promise<void>): Promise<SignInResult>;
27
+ emit(event: PasskeyEvent): void;
28
+ }
29
+ /** Internal native implementation. Public callers enter through SessionProvider.passkeys. */
30
+ export declare class NativePasskeyProvider implements PasskeyProvider {
31
+ #private;
32
+ private readonly deps;
33
+ constructor(trust: PasskeyConfig, deps: NativeDependencies);
34
+ recordAdmissionDenied(purpose: PasskeyOperation, reason: PasskeyAdmissionReason): void;
35
+ signInOptions(_metadata: PasskeyRequestMetadata): Promise<PasskeyCeremony<PasskeyAuthenticationOptions>>;
36
+ verifySignIn(args: SignInArgs & {
37
+ ceremonyId: string;
38
+ response: unknown;
39
+ }): Promise<SignInResult>;
40
+ passwordProof(args: PasskeySession & {
41
+ password: string;
42
+ action: PasskeyManagementAction;
43
+ }): Promise<PasskeyManagementGrant>;
44
+ proofOptions(args: PasskeySession & {
45
+ action: PasskeyManagementAction;
46
+ }): Promise<PasskeyCeremony<PasskeyAuthenticationOptions>>;
47
+ verifyProof(args: PasskeySession & {
48
+ ceremonyId: string;
49
+ response: unknown;
50
+ }): Promise<PasskeyManagementGrant>;
51
+ enrollmentOptions(args: PasskeySession & {
52
+ grantId: string;
53
+ }): Promise<PasskeyCeremony<PasskeyRegistrationOptions>>;
54
+ verifyEnrollment(args: PasskeySession & {
55
+ ceremonyId: string;
56
+ response: unknown;
57
+ name: string;
58
+ }): Promise<PasskeyCredentialSummary>;
59
+ listCredentials(args: PasskeySession & {
60
+ targetUserId?: string;
61
+ }): Promise<PasskeyCredentialSummary[]>;
62
+ renameCredential(args: PasskeySession & {
63
+ credentialRecordId: string;
64
+ name: string;
65
+ }): Promise<PasskeyCredentialSummary>;
66
+ removeCredential(args: PasskeySession & {
67
+ grantId: string;
68
+ }): Promise<{
69
+ signedOut: boolean;
70
+ }>;
71
+ }
72
+ export {};