@lastshotlabs/bunshot 0.0.25 → 0.0.27

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 (108) hide show
  1. package/dist/adapters/localStorage.js +20 -5
  2. package/dist/adapters/memoryAuth.d.ts +6 -0
  3. package/dist/adapters/memoryAuth.js +117 -2
  4. package/dist/adapters/mongoAuth.js +97 -1
  5. package/dist/adapters/sqliteAuth.d.ts +23 -0
  6. package/dist/adapters/sqliteAuth.js +153 -2
  7. package/dist/app.d.ts +105 -2
  8. package/dist/app.js +112 -9
  9. package/dist/index.d.ts +23 -4
  10. package/dist/index.js +13 -2
  11. package/dist/lib/HttpError.d.ts +2 -1
  12. package/dist/lib/HttpError.js +3 -1
  13. package/dist/lib/appConfig.d.ts +113 -0
  14. package/dist/lib/appConfig.js +38 -0
  15. package/dist/lib/auditLog.d.ts +6 -0
  16. package/dist/lib/auditLog.js +17 -0
  17. package/dist/lib/authAdapter.d.ts +71 -1
  18. package/dist/lib/authRateLimit.js +36 -0
  19. package/dist/lib/breachedPassword.d.ts +13 -0
  20. package/dist/lib/breachedPassword.js +48 -0
  21. package/dist/lib/captcha.d.ts +25 -0
  22. package/dist/lib/captcha.js +37 -0
  23. package/dist/lib/context.d.ts +5 -0
  24. package/dist/lib/credentialStuffing.d.ts +31 -0
  25. package/dist/lib/credentialStuffing.js +77 -0
  26. package/dist/lib/emailVerification.d.ts +6 -0
  27. package/dist/lib/emailVerification.js +46 -3
  28. package/dist/lib/jwks.d.ts +25 -0
  29. package/dist/lib/jwks.js +51 -0
  30. package/dist/lib/jwt.d.ts +15 -2
  31. package/dist/lib/jwt.js +92 -5
  32. package/dist/lib/logger.d.ts +2 -0
  33. package/dist/lib/logger.js +6 -0
  34. package/dist/lib/m2m.d.ts +29 -0
  35. package/dist/lib/m2m.js +48 -0
  36. package/dist/lib/mfaChallenge.d.ts +14 -1
  37. package/dist/lib/mfaChallenge.js +111 -6
  38. package/dist/lib/mongo.js +1 -1
  39. package/dist/lib/oauthCode.js +23 -18
  40. package/dist/lib/resetPassword.js +3 -1
  41. package/dist/lib/saml.d.ts +25 -0
  42. package/dist/lib/saml.js +64 -0
  43. package/dist/lib/scim.d.ts +44 -0
  44. package/dist/lib/scim.js +54 -0
  45. package/dist/lib/securityEvents.d.ts +28 -0
  46. package/dist/lib/securityEvents.js +26 -0
  47. package/dist/lib/session.d.ts +10 -0
  48. package/dist/lib/session.js +67 -5
  49. package/dist/lib/signing.js +5 -2
  50. package/dist/lib/suspension.d.ts +13 -0
  51. package/dist/lib/suspension.js +23 -0
  52. package/dist/lib/upload.d.ts +4 -0
  53. package/dist/lib/upload.js +26 -1
  54. package/dist/lib/uploadRegistry.d.ts +18 -0
  55. package/dist/lib/uploadRegistry.js +83 -0
  56. package/dist/lib/ws.js +7 -0
  57. package/dist/middleware/bearerAuth.js +1 -1
  58. package/dist/middleware/captcha.d.ts +10 -0
  59. package/dist/middleware/captcha.js +36 -0
  60. package/dist/middleware/csrf.js +8 -4
  61. package/dist/middleware/errorHandler.js +4 -1
  62. package/dist/middleware/identify.js +40 -13
  63. package/dist/middleware/requestSigning.js +6 -5
  64. package/dist/middleware/requireMfaSetup.js +2 -1
  65. package/dist/middleware/requireScope.d.ts +10 -0
  66. package/dist/middleware/requireScope.js +25 -0
  67. package/dist/middleware/requireStepUp.d.ts +18 -0
  68. package/dist/middleware/requireStepUp.js +29 -0
  69. package/dist/middleware/scimAuth.d.ts +8 -0
  70. package/dist/middleware/scimAuth.js +29 -0
  71. package/dist/middleware/webhookAuth.d.ts +1 -1
  72. package/dist/middleware/webhookAuth.js +6 -5
  73. package/dist/models/AuthUser.d.ts +7 -0
  74. package/dist/models/AuthUser.js +7 -0
  75. package/dist/models/M2MClient.d.ts +18 -0
  76. package/dist/models/M2MClient.js +18 -0
  77. package/dist/routes/auth.d.ts +3 -2
  78. package/dist/routes/auth.js +155 -16
  79. package/dist/routes/jobs.js +21 -3
  80. package/dist/routes/m2m.d.ts +2 -0
  81. package/dist/routes/m2m.js +72 -0
  82. package/dist/routes/metrics.d.ts +1 -0
  83. package/dist/routes/metrics.js +3 -0
  84. package/dist/routes/mfa.js +9 -1
  85. package/dist/routes/oauth.js +6 -0
  86. package/dist/routes/oidc.d.ts +2 -0
  87. package/dist/routes/oidc.js +29 -0
  88. package/dist/routes/passkey.d.ts +1 -0
  89. package/dist/routes/passkey.js +157 -0
  90. package/dist/routes/saml.d.ts +2 -0
  91. package/dist/routes/saml.js +86 -0
  92. package/dist/routes/scim.d.ts +2 -0
  93. package/dist/routes/scim.js +255 -0
  94. package/dist/routes/uploads.d.ts +13 -1
  95. package/dist/routes/uploads.js +98 -6
  96. package/dist/services/auth.d.ts +2 -0
  97. package/dist/services/auth.js +101 -22
  98. package/dist/services/mfa.js +2 -2
  99. package/dist/ws/index.js +2 -1
  100. package/docs/sections/auth-flow/full.md +790 -779
  101. package/docs/sections/auth-security-examples/full.md +23 -0
  102. package/docs/sections/metrics/full.md +6 -2
  103. package/docs/sections/passkey-login/full.md +90 -0
  104. package/docs/sections/passkey-login/overview.md +1 -0
  105. package/docs/sections/uploads/full.md +11 -2
  106. package/docs/sections/webhook-auth/full.md +1 -1
  107. package/docs/sections/websocket/full.md +12 -0
  108. package/package.json +3 -2
@@ -2,14 +2,54 @@ import { z } from "zod";
2
2
  import { createRouter } from "../lib/context";
3
3
  import { createRoute } from "../lib/createRoute";
4
4
  import { userAuth } from "../middleware/userAuth";
5
- import { getStorageAdapter, getUploadConfig } from "../lib/upload";
5
+ import { getStorageAdapter, generateUploadKeyFromFilename } from "../lib/upload";
6
6
  import { getSigningConfig, getSigningSecret } from "../lib/appConfig";
7
7
  import { createPresignedUrl } from "../lib/signing";
8
+ import { getUploadRecord, deleteUploadRecord, registerUpload } from "../lib/uploadRegistry";
8
9
  const tags = ["Uploads"];
10
+ async function checkUploadAccess(action, key, userId, tenantId, config) {
11
+ const record = await getUploadRecord(key);
12
+ const authorize = config.authorization?.authorize;
13
+ const allowExternalKeys = config.allowExternalKeys ?? false;
14
+ if (record) {
15
+ // If the registry record has a tenantId, the requester must match — period.
16
+ if (record.tenantId && record.tenantId !== tenantId) {
17
+ return { allowed: false, notFound: false };
18
+ }
19
+ // Owner match → allow
20
+ if (record.ownerUserId && record.ownerUserId === userId) {
21
+ return { allowed: true, notFound: false };
22
+ }
23
+ // No owner or owner mismatch → try callback
24
+ if (authorize) {
25
+ const ok = await authorize({ action, key, userId: userId ?? undefined, tenantId: tenantId ?? undefined });
26
+ return { allowed: ok, notFound: false };
27
+ }
28
+ return { allowed: false, notFound: false };
29
+ }
30
+ // Record not in registry
31
+ if (allowExternalKeys) {
32
+ if (authorize) {
33
+ const ok = await authorize({ action, key, userId: userId ?? undefined, tenantId: tenantId ?? undefined });
34
+ return { allowed: ok, notFound: false };
35
+ }
36
+ return { allowed: false, notFound: false };
37
+ }
38
+ return { allowed: false, notFound: true };
39
+ }
9
40
  export const createUploadsRouter = (config) => {
10
41
  const router = createRouter();
11
42
  const basePath = (config.path ?? "/uploads").replace(/\/$/, "");
12
43
  router.use(`${basePath}/*`, userAuth);
44
+ const BLOCKED_MIME_TYPES = new Set([
45
+ "application/x-executable",
46
+ "application/x-sh",
47
+ "application/x-msdownload",
48
+ "text/html",
49
+ "application/x-httpd-php",
50
+ "application/javascript",
51
+ "text/javascript",
52
+ ]);
13
53
  const presignRoute = createRoute({
14
54
  method: "post",
15
55
  path: `${basePath}/presign`,
@@ -20,9 +60,11 @@ export const createUploadsRouter = (config) => {
20
60
  content: {
21
61
  "application/json": {
22
62
  schema: z.object({
23
- key: z.string().describe("Storage key for the upload"),
63
+ filename: z.string().optional().describe("Original filename (used to derive the storage key extension)"),
24
64
  mimeType: z.string().optional().describe("MIME type of the file"),
25
65
  expirySeconds: z.number().int().positive().optional().describe("URL expiry in seconds"),
66
+ maxBytes: z.number().int().positive().max(100 * 1024 * 1024).optional()
67
+ .describe("Maximum allowed file size in bytes (client-enforced via Content-Length header). Defaults to 10MB. Maximum: 100MB."),
26
68
  }),
27
69
  },
28
70
  },
@@ -31,7 +73,11 @@ export const createUploadsRouter = (config) => {
31
73
  responses: {
32
74
  200: {
33
75
  description: "Presigned URL generated",
34
- content: { "application/json": { schema: z.object({ url: z.string(), key: z.string() }) } },
76
+ content: { "application/json": { schema: z.object({ url: z.string(), key: z.string(), maxBytes: z.number().optional() }) } },
77
+ },
78
+ 400: {
79
+ description: "File type not allowed",
80
+ content: { "application/json": { schema: z.object({ error: z.string() }) } },
35
81
  },
36
82
  501: {
37
83
  description: "Not implemented by adapter",
@@ -44,11 +90,26 @@ export const createUploadsRouter = (config) => {
44
90
  if (!adapter?.presignPut) {
45
91
  return c.json({ error: "Presigned URLs not supported by the configured storage adapter" }, 501);
46
92
  }
47
- const { key, mimeType, expirySeconds } = c.req.valid("json");
48
- const _uploadConfig = getUploadConfig();
93
+ const { filename, mimeType, expirySeconds, maxBytes } = c.req.valid("json");
94
+ if (mimeType && BLOCKED_MIME_TYPES.has(mimeType)) {
95
+ return c.json({ error: "File type not allowed." }, 400);
96
+ }
97
+ const userId = c.get("authUserId") ?? undefined;
98
+ const tenantId = c.get("tenantId") ?? undefined;
99
+ // Server-generates the key — client cannot control the storage path
100
+ const key = generateUploadKeyFromFilename(filename, { userId, tenantId });
49
101
  const expiry = expirySeconds ?? (typeof config.expirySeconds === "number" ? config.expirySeconds : 3600);
50
102
  const url = await adapter.presignPut(key, { expirySeconds: expiry, mimeType });
51
- return c.json({ url, key }, 200);
103
+ // Register the upload for ownership tracking
104
+ await registerUpload({
105
+ key,
106
+ ownerUserId: userId,
107
+ tenantId,
108
+ mimeType,
109
+ bucket: c.get("uploadBucket") ?? undefined,
110
+ createdAt: Date.now(),
111
+ });
112
+ return c.json({ url, key, ...(maxBytes !== undefined ? { maxBytes } : {}) }, 200);
52
113
  });
53
114
  const presignGetRoute = createRoute({
54
115
  method: "get",
@@ -73,6 +134,14 @@ export const createUploadsRouter = (config) => {
73
134
  },
74
135
  },
75
136
  },
137
+ 403: {
138
+ description: "Forbidden — not the owner or unauthorized",
139
+ content: { "application/json": { schema: z.object({ error: z.string() }) } },
140
+ },
141
+ 404: {
142
+ description: "Key not found in upload registry",
143
+ content: { "application/json": { schema: z.object({ error: z.string() }) } },
144
+ },
76
145
  501: {
77
146
  description: "Not implemented",
78
147
  content: { "application/json": { schema: z.object({ error: z.string() }) } },
@@ -82,6 +151,13 @@ export const createUploadsRouter = (config) => {
82
151
  router.openapi(presignGetRoute, async (c) => {
83
152
  const { key } = c.req.valid("param");
84
153
  const { expiry: expiryStr } = c.req.valid("query");
154
+ const userId = c.get("authUserId");
155
+ const tenantId = c.get("tenantId");
156
+ const { allowed, notFound } = await checkUploadAccess("read", key, userId, tenantId, config);
157
+ if (notFound)
158
+ return c.json({ error: "Not found" }, 404);
159
+ if (!allowed)
160
+ return c.json({ error: "Forbidden" }, 403);
85
161
  const expirySeconds = expiryStr ? parseInt(expiryStr, 10) : (typeof config.expirySeconds === "number" ? config.expirySeconds : 3600);
86
162
  const signingCfg = getSigningConfig();
87
163
  if (signingCfg?.presignedUrls) {
@@ -117,6 +193,14 @@ export const createUploadsRouter = (config) => {
117
193
  },
118
194
  responses: {
119
195
  204: { description: "Deleted" },
196
+ 403: {
197
+ description: "Forbidden — not the owner or unauthorized",
198
+ content: { "application/json": { schema: z.object({ error: z.string() }) } },
199
+ },
200
+ 404: {
201
+ description: "Key not found in upload registry",
202
+ content: { "application/json": { schema: z.object({ error: z.string() }) } },
203
+ },
120
204
  500: {
121
205
  description: "No storage adapter configured",
122
206
  content: { "application/json": { schema: z.object({ error: z.string() }) } },
@@ -128,7 +212,15 @@ export const createUploadsRouter = (config) => {
128
212
  if (!adapter)
129
213
  return c.json({ error: "No storage adapter configured" }, 500);
130
214
  const { key } = c.req.valid("param");
215
+ const userId = c.get("authUserId");
216
+ const tenantId = c.get("tenantId");
217
+ const { allowed, notFound } = await checkUploadAccess("delete", key, userId, tenantId, config);
218
+ if (notFound)
219
+ return c.json({ error: "Not found" }, 404);
220
+ if (!allowed)
221
+ return c.json({ error: "Forbidden" }, 403);
131
222
  await adapter.delete(key);
223
+ await deleteUploadRecord(key);
132
224
  return c.body(null, 204);
133
225
  });
134
226
  return router;
@@ -15,6 +15,7 @@ export interface AuthResult {
15
15
  export declare const createSessionForUser: (userId: string, metadata?: SessionMetadata) => Promise<{
16
16
  token: string;
17
17
  refreshToken?: string;
18
+ sessionId: string;
18
19
  }>;
19
20
  export declare const register: (identifier: string, password: string, metadata?: SessionMetadata) => Promise<AuthResult>;
20
21
  export declare const login: (identifier: string, password: string, metadata?: SessionMetadata) => Promise<AuthResult>;
@@ -25,3 +26,4 @@ export declare const refresh: (refreshTokenValue: string) => Promise<{
25
26
  }>;
26
27
  export declare const deleteAccount: (userId: string, password?: string) => Promise<void>;
27
28
  export declare const logout: (token: string | null) => Promise<void>;
29
+ export declare const passkeyLogin: (passkeyToken: string, assertionResponse: any, metadata?: SessionMetadata) => Promise<AuthResult>;
@@ -2,14 +2,16 @@ import { getAuthAdapter } from "../lib/authAdapter";
2
2
  import { HttpError } from "../lib/HttpError";
3
3
  import { signToken, verifyToken } from "../lib/jwt";
4
4
  import { createSession, deleteSession, getActiveSessionCount, evictOldestSession, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken } from "../lib/session";
5
- import { getDefaultRole, getPrimaryField, getEmailVerificationConfig, getMaxSessions, getRefreshTokenConfig, getAccessTokenExpiry, getMfaConfig, getMfaEmailOtpConfig, getMfaWebAuthnConfig } from "../lib/appConfig";
5
+ import { getDefaultRole, getPrimaryField, getEmailVerificationConfig, getMaxSessions, getRefreshTokenConfig, getAccessTokenExpiry, getMfaConfig, getMfaEmailOtpConfig, getMfaWebAuthnConfig, getMfaWebAuthnPasskeyMfaBypass } from "../lib/appConfig";
6
+ import { getSuspended } from "../lib/suspension";
6
7
  import { createVerificationToken } from "../lib/emailVerification";
7
8
  import { createMfaChallenge } from "../lib/mfaChallenge";
8
9
  import { generateEmailOtpCode, generateWebAuthnAuthenticationOptions } from "./mfa";
10
+ import { emitSecurityEvent } from "../lib/securityEvents";
9
11
  async function createSessionWithRefreshToken(userId, sessionId, metadata) {
10
12
  const rtConfig = getRefreshTokenConfig();
11
13
  const expirySeconds = rtConfig ? getAccessTokenExpiry() : undefined;
12
- const token = await signToken(userId, sessionId, expirySeconds);
14
+ const token = await signToken({ sub: userId, sid: sessionId }, expirySeconds);
13
15
  while (await getActiveSessionCount(userId) >= getMaxSessions()) {
14
16
  await evictOldestSession(userId);
15
17
  }
@@ -27,25 +29,32 @@ export const createSessionForUser = async (userId, metadata) => {
27
29
  return createSessionWithRefreshToken(userId, sessionId, metadata);
28
30
  };
29
31
  export const register = async (identifier, password, metadata) => {
30
- const hashed = await Bun.password.hash(password);
31
- const adapter = getAuthAdapter();
32
- const user = await adapter.create(identifier, hashed);
33
- const role = getDefaultRole();
34
- if (role)
35
- await adapter.setRoles(user.id, [role]);
36
- const sessionId = crypto.randomUUID();
37
- const { token, refreshToken } = await createSessionWithRefreshToken(user.id, sessionId, metadata);
38
- const evConfig = getEmailVerificationConfig();
39
- if (evConfig && getPrimaryField() === "email") {
40
- try {
41
- const verificationToken = await createVerificationToken(user.id, identifier);
42
- await evConfig.onSend(identifier, verificationToken);
43
- }
44
- catch (e) {
45
- console.error("[email-verification] Failed to send verification email:", e);
32
+ try {
33
+ const hashed = await Bun.password.hash(password);
34
+ const adapter = getAuthAdapter();
35
+ const user = await adapter.create(identifier, hashed);
36
+ const role = getDefaultRole();
37
+ if (role)
38
+ await adapter.setRoles(user.id, [role]);
39
+ const sessionId = crypto.randomUUID();
40
+ const { token, refreshToken } = await createSessionWithRefreshToken(user.id, sessionId, metadata);
41
+ const evConfig = getEmailVerificationConfig();
42
+ if (evConfig && getPrimaryField() === "email") {
43
+ try {
44
+ const verificationToken = await createVerificationToken(user.id, identifier);
45
+ await evConfig.onSend(identifier, verificationToken);
46
+ }
47
+ catch (e) {
48
+ console.error("[email-verification] Failed to send verification email:", e);
49
+ }
46
50
  }
51
+ emitSecurityEvent({ eventType: "auth.register.success", severity: "info", timestamp: new Date().toISOString(), userId: user.id });
52
+ return { token, userId: user.id, email: identifier, refreshToken };
53
+ }
54
+ catch (err) {
55
+ emitSecurityEvent({ eventType: "auth.register.failure", severity: "warn", timestamp: new Date().toISOString() });
56
+ throw err;
47
57
  }
48
- return { token, userId: user.id, email: identifier, refreshToken };
49
58
  };
50
59
  // Pre-computed dummy hash so non-existent-user login takes the same time as wrong-password login
51
60
  const DUMMY_HASH = await Bun.password.hash("dummy-timing-safe-placeholder");
@@ -57,8 +66,15 @@ export const login = async (identifier, password, metadata) => {
57
66
  const hashToVerify = user?.passwordHash ?? DUMMY_HASH;
58
67
  const passwordValid = await Bun.password.verify(password, hashToVerify);
59
68
  if (!user || !passwordValid) {
69
+ emitSecurityEvent({ eventType: "auth.login.failure", severity: "warn", timestamp: new Date().toISOString(), meta: { identifier } });
60
70
  throw new HttpError(401, "Invalid credentials");
61
71
  }
72
+ // Check suspension
73
+ const suspensionStatus = await getSuspended(user.id);
74
+ if (suspensionStatus.suspended) {
75
+ emitSecurityEvent({ eventType: "auth.login.blocked", severity: "critical", timestamp: new Date().toISOString(), meta: { reason: "suspended" } });
76
+ throw new HttpError(403, "Account suspended", "ACCOUNT_SUSPENDED");
77
+ }
62
78
  // Check email verification before MFA to avoid leaking MFA status to unverified users
63
79
  const fullUser = adapter.getUser ? await adapter.getUser(user.id) : null;
64
80
  const googleLinked = fullUser?.providerIds?.some((id) => id.startsWith("google:")) ?? false;
@@ -100,6 +116,7 @@ export const login = async (identifier, password, metadata) => {
100
116
  }
101
117
  const sessionId = crypto.randomUUID();
102
118
  const { token, refreshToken } = await createSessionWithRefreshToken(user.id, sessionId, metadata);
119
+ emitSecurityEvent({ eventType: "auth.login.success", severity: "info", timestamp: new Date().toISOString(), userId: user.id });
103
120
  if (evConfig && getPrimaryField() === "email" && adapter.getEmailVerified) {
104
121
  const verified = await adapter.getEmailVerified(user.id);
105
122
  return { token, userId: user.id, email: fullUser?.email, emailVerified: verified, googleLinked, refreshToken };
@@ -115,12 +132,12 @@ export const refresh = async (refreshTokenValue) => {
115
132
  // If the returned newRefreshToken differs from what was sent, we're in a grace window replay.
116
133
  // Return the current tokens without rotating again.
117
134
  if (newRefreshToken !== refreshTokenValue) {
118
- const accessToken = await signToken(userId, sessionId, getAccessTokenExpiry());
135
+ const accessToken = await signToken({ sub: userId, sid: sessionId }, getAccessTokenExpiry());
119
136
  return { token: accessToken, refreshToken: newRefreshToken, userId };
120
137
  }
121
138
  // Normal rotation: generate new refresh + access tokens
122
139
  const newRT = crypto.randomUUID();
123
- const newAccessToken = await signToken(userId, sessionId, getAccessTokenExpiry());
140
+ const newAccessToken = await signToken({ sub: userId, sid: sessionId }, getAccessTokenExpiry());
124
141
  await rotateRefreshToken(sessionId, newRT, newAccessToken);
125
142
  return { token: newAccessToken, refreshToken: newRT, userId };
126
143
  };
@@ -148,12 +165,74 @@ export const deleteAccount = async (userId, password) => {
148
165
  await deleteUserSessions(userId);
149
166
  // Delete the user
150
167
  await adapter.deleteUser(userId);
168
+ emitSecurityEvent({ eventType: "auth.account.deleted", severity: "warn", timestamp: new Date().toISOString(), userId });
151
169
  };
152
170
  export const logout = async (token) => {
153
171
  if (token) {
154
172
  const payload = await verifyToken(token);
155
173
  const sessionId = payload.sid;
156
- if (sessionId)
174
+ if (sessionId) {
157
175
  await deleteSession(sessionId);
176
+ emitSecurityEvent({ eventType: "auth.logout", severity: "info", timestamp: new Date().toISOString(), sessionId });
177
+ }
178
+ }
179
+ };
180
+ export const passkeyLogin = async (passkeyToken, assertionResponse, metadata) => {
181
+ const adapter = getAuthAdapter();
182
+ if (!adapter.findUserByWebAuthnCredentialId || !adapter.getWebAuthnCredentials) {
183
+ throw new HttpError(501, "Auth adapter does not support passkey login");
184
+ }
185
+ const { consumePasskeyLoginChallenge } = await import("../lib/mfaChallenge");
186
+ const challengeData = await consumePasskeyLoginChallenge(passkeyToken);
187
+ if (!challengeData) {
188
+ throw new HttpError(401, "Invalid or expired passkey token");
189
+ }
190
+ const credentialId = assertionResponse?.id;
191
+ if (!credentialId) {
192
+ throw new HttpError(401, "Invalid assertion response");
193
+ }
194
+ const userId = await adapter.findUserByWebAuthnCredentialId(credentialId);
195
+ if (!userId) {
196
+ throw new HttpError(401, "Invalid credentials");
197
+ }
198
+ const { verifyWebAuthn } = await import("./mfa");
199
+ const verified = await verifyWebAuthn(userId, assertionResponse, challengeData.webauthnChallenge);
200
+ if (!verified) {
201
+ throw new HttpError(401, "WebAuthn verification failed");
202
+ }
203
+ // Check suspension
204
+ const suspensionStatus = await getSuspended(userId);
205
+ if (suspensionStatus.suspended) {
206
+ throw new HttpError(403, "Account suspended", "ACCOUNT_SUSPENDED");
207
+ }
208
+ // passkeyMfaBypass=true (default): passkey with userVerification=required satisfies both factors
209
+ const mfaBypass = getMfaWebAuthnPasskeyMfaBypass();
210
+ if (!mfaBypass && getMfaConfig() && adapter.isMfaEnabled && await adapter.isMfaEnabled(userId)) {
211
+ const methods = adapter.getMfaMethods ? await adapter.getMfaMethods(userId) : ["totp"];
212
+ let emailOtpHash;
213
+ const emailOtpConfig = getMfaEmailOtpConfig();
214
+ if (methods.includes("emailOtp") && emailOtpConfig) {
215
+ const { generateEmailOtpCode } = await import("./mfa");
216
+ const { code, hash } = generateEmailOtpCode();
217
+ emailOtpHash = hash;
218
+ const fullUser = adapter.getUser ? await adapter.getUser(userId) : null;
219
+ if (fullUser?.email)
220
+ await emailOtpConfig.onSend(fullUser.email, code);
221
+ }
222
+ let webauthnChallenge2;
223
+ let webauthnOptions;
224
+ if (methods.includes("webauthn") && getMfaWebAuthnConfig()) {
225
+ const { generateWebAuthnAuthenticationOptions } = await import("./mfa");
226
+ const result = await generateWebAuthnAuthenticationOptions(userId);
227
+ if (result) {
228
+ webauthnChallenge2 = result.challenge;
229
+ webauthnOptions = result.options;
230
+ }
231
+ }
232
+ const mfaToken = await createMfaChallenge(userId, { emailOtpHash, webauthnChallenge: webauthnChallenge2 });
233
+ return { token: "", userId, mfaRequired: true, mfaToken, mfaMethods: methods, webauthnOptions };
158
234
  }
235
+ const { token, refreshToken } = await createSessionForUser(userId, metadata);
236
+ const fullUser = adapter.getUser ? await adapter.getUser(userId) : null;
237
+ return { token, userId, email: fullUser?.email, refreshToken };
159
238
  };
@@ -343,8 +343,8 @@ export const initiateWebAuthnRegistration = async (userId) => {
343
343
  })),
344
344
  authenticatorSelection: {
345
345
  authenticatorAttachment: config.authenticatorAttachment,
346
- userVerification: config.userVerification ?? "preferred",
347
- residentKey: "preferred",
346
+ userVerification: config.userVerification ?? "required",
347
+ residentKey: "required",
348
348
  },
349
349
  timeout: config.timeout ?? 60000,
350
350
  });
package/dist/ws/index.js CHANGED
@@ -2,6 +2,7 @@ import { verifyToken } from "../lib/jwt";
2
2
  import { getSession } from "../lib/session";
3
3
  import { COOKIE_TOKEN } from "../lib/constants";
4
4
  import { trackSocket, untrackSocket } from "../lib/wsPresence";
5
+ import { timingSafeEqual } from "../lib/crypto";
5
6
  export const createWsUpgradeHandler = (server) => async (req) => {
6
7
  let userId = null;
7
8
  try {
@@ -12,7 +13,7 @@ export const createWsUpgradeHandler = (server) => async (req) => {
12
13
  const sessionId = payload.sid;
13
14
  if (sessionId) {
14
15
  const stored = await getSession(sessionId);
15
- if (stored === token)
16
+ if (timingSafeEqual(stored ?? "", token))
16
17
  userId = payload.sub;
17
18
  }
18
19
  }