@lastshotlabs/bunshot 0.0.13 → 0.0.18

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 (123) hide show
  1. package/README.md +2816 -1747
  2. package/dist/adapters/memoryAuth.d.ts +7 -0
  3. package/dist/adapters/memoryAuth.js +177 -2
  4. package/dist/adapters/mongoAuth.js +94 -0
  5. package/dist/adapters/sqliteAuth.d.ts +9 -0
  6. package/dist/adapters/sqliteAuth.js +190 -2
  7. package/dist/app.d.ts +120 -2
  8. package/dist/app.js +104 -4
  9. package/dist/entrypoints/queue.d.ts +2 -2
  10. package/dist/entrypoints/queue.js +1 -1
  11. package/dist/index.d.ts +24 -8
  12. package/dist/index.js +15 -5
  13. package/dist/lib/appConfig.d.ts +81 -0
  14. package/dist/lib/appConfig.js +30 -0
  15. package/dist/lib/authAdapter.d.ts +54 -0
  16. package/dist/lib/authRateLimit.d.ts +2 -0
  17. package/dist/lib/authRateLimit.js +4 -0
  18. package/dist/lib/clientIp.d.ts +14 -0
  19. package/dist/lib/clientIp.js +52 -0
  20. package/dist/lib/constants.d.ts +4 -0
  21. package/dist/lib/constants.js +4 -0
  22. package/dist/lib/context.d.ts +2 -0
  23. package/dist/lib/createDtoMapper.d.ts +33 -0
  24. package/dist/lib/createDtoMapper.js +69 -0
  25. package/dist/lib/crypto.d.ts +11 -0
  26. package/dist/lib/crypto.js +22 -0
  27. package/dist/lib/emailVerification.d.ts +4 -0
  28. package/dist/lib/emailVerification.js +20 -12
  29. package/dist/lib/jwt.d.ts +1 -1
  30. package/dist/lib/jwt.js +19 -6
  31. package/dist/lib/mfaChallenge.d.ts +42 -0
  32. package/dist/lib/mfaChallenge.js +293 -0
  33. package/dist/lib/oauth.d.ts +14 -1
  34. package/dist/lib/oauth.js +19 -1
  35. package/dist/lib/oauthCode.d.ts +15 -0
  36. package/dist/lib/oauthCode.js +90 -0
  37. package/dist/lib/queue.d.ts +33 -0
  38. package/dist/lib/queue.js +98 -0
  39. package/dist/lib/resetPassword.js +12 -16
  40. package/dist/lib/roles.d.ts +4 -0
  41. package/dist/lib/roles.js +27 -0
  42. package/dist/lib/session.d.ts +12 -0
  43. package/dist/lib/session.js +165 -5
  44. package/dist/lib/tenant.d.ts +15 -0
  45. package/dist/lib/tenant.js +65 -0
  46. package/dist/lib/ws.js +5 -1
  47. package/dist/lib/zodToMongoose.d.ts +38 -0
  48. package/dist/lib/zodToMongoose.js +84 -0
  49. package/dist/middleware/bearerAuth.js +4 -3
  50. package/dist/middleware/botProtection.js +2 -2
  51. package/dist/middleware/cacheResponse.d.ts +1 -0
  52. package/dist/middleware/cacheResponse.js +18 -3
  53. package/dist/middleware/cors.d.ts +2 -0
  54. package/dist/middleware/cors.js +22 -8
  55. package/dist/middleware/csrf.d.ts +18 -0
  56. package/dist/middleware/csrf.js +115 -0
  57. package/dist/middleware/rateLimit.d.ts +2 -1
  58. package/dist/middleware/rateLimit.js +7 -5
  59. package/dist/middleware/requireRole.d.ts +14 -3
  60. package/dist/middleware/requireRole.js +46 -6
  61. package/dist/middleware/tenant.d.ts +5 -0
  62. package/dist/middleware/tenant.js +116 -0
  63. package/dist/models/AuthUser.d.ts +17 -0
  64. package/dist/models/AuthUser.js +17 -0
  65. package/dist/models/TenantRole.d.ts +15 -0
  66. package/dist/models/TenantRole.js +23 -0
  67. package/dist/routes/auth.d.ts +5 -3
  68. package/dist/routes/auth.js +173 -30
  69. package/dist/routes/jobs.d.ts +2 -0
  70. package/dist/routes/jobs.js +270 -0
  71. package/dist/routes/mfa.d.ts +5 -0
  72. package/dist/routes/mfa.js +616 -0
  73. package/dist/routes/oauth.js +378 -23
  74. package/dist/schemas/auth.d.ts +2 -0
  75. package/dist/schemas/auth.js +22 -1
  76. package/dist/server.d.ts +6 -0
  77. package/dist/server.js +19 -3
  78. package/dist/services/auth.d.ts +18 -5
  79. package/dist/services/auth.js +112 -18
  80. package/dist/services/mfa.d.ts +84 -0
  81. package/dist/services/mfa.js +543 -0
  82. package/dist/ws/index.js +3 -2
  83. package/docs/sections/adding-middleware/full.md +35 -0
  84. package/docs/sections/adding-models/full.md +125 -0
  85. package/docs/sections/adding-models/overview.md +13 -0
  86. package/docs/sections/adding-routes/full.md +182 -0
  87. package/docs/sections/adding-routes/overview.md +23 -0
  88. package/docs/sections/auth-flow/full.md +634 -0
  89. package/docs/sections/auth-flow/overview.md +10 -0
  90. package/docs/sections/cli/full.md +30 -0
  91. package/docs/sections/configuration/full.md +155 -0
  92. package/docs/sections/configuration/overview.md +17 -0
  93. package/docs/sections/configuration-example/full.md +117 -0
  94. package/docs/sections/configuration-example/overview.md +30 -0
  95. package/docs/sections/documentation/full.md +171 -0
  96. package/docs/sections/environment-variables/full.md +55 -0
  97. package/docs/sections/exports/full.md +92 -0
  98. package/docs/sections/extending-context/full.md +59 -0
  99. package/docs/sections/header.md +3 -0
  100. package/docs/sections/installation/full.md +6 -0
  101. package/docs/sections/jobs/full.md +140 -0
  102. package/docs/sections/jobs/overview.md +15 -0
  103. package/docs/sections/mongodb-connections/full.md +45 -0
  104. package/docs/sections/mongodb-connections/overview.md +7 -0
  105. package/docs/sections/multi-tenancy/full.md +66 -0
  106. package/docs/sections/multi-tenancy/overview.md +15 -0
  107. package/docs/sections/oauth/full.md +189 -0
  108. package/docs/sections/oauth/overview.md +16 -0
  109. package/docs/sections/package-development/full.md +7 -0
  110. package/docs/sections/peer-dependencies/full.md +47 -0
  111. package/docs/sections/quick-start/full.md +43 -0
  112. package/docs/sections/response-caching/full.md +117 -0
  113. package/docs/sections/response-caching/overview.md +13 -0
  114. package/docs/sections/roles/full.md +136 -0
  115. package/docs/sections/roles/overview.md +12 -0
  116. package/docs/sections/running-without-redis/full.md +16 -0
  117. package/docs/sections/running-without-redis-or-mongodb/full.md +60 -0
  118. package/docs/sections/stack/full.md +10 -0
  119. package/docs/sections/websocket/full.md +101 -0
  120. package/docs/sections/websocket/overview.md +5 -0
  121. package/docs/sections/websocket-rooms/full.md +97 -0
  122. package/docs/sections/websocket-rooms/overview.md +5 -0
  123. package/package.json +30 -9
@@ -1,9 +1,31 @@
1
1
  import { getAuthAdapter } from "../lib/authAdapter";
2
2
  import { HttpError } from "../lib/HttpError";
3
3
  import { signToken, verifyToken } from "../lib/jwt";
4
- import { createSession, deleteSession, getActiveSessionCount, evictOldestSession } from "../lib/session";
5
- import { getDefaultRole, getPrimaryField, getEmailVerificationConfig, getMaxSessions } from "../lib/appConfig";
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";
6
6
  import { createVerificationToken } from "../lib/emailVerification";
7
+ import { createMfaChallenge } from "../lib/mfaChallenge";
8
+ import { generateEmailOtpCode, generateWebAuthnAuthenticationOptions } from "./mfa";
9
+ async function createSessionWithRefreshToken(userId, sessionId, metadata) {
10
+ const rtConfig = getRefreshTokenConfig();
11
+ const expirySeconds = rtConfig ? getAccessTokenExpiry() : undefined;
12
+ const token = await signToken(userId, sessionId, expirySeconds);
13
+ while (await getActiveSessionCount(userId) >= getMaxSessions()) {
14
+ await evictOldestSession(userId);
15
+ }
16
+ await createSession(userId, token, sessionId, metadata);
17
+ let refreshToken;
18
+ if (rtConfig) {
19
+ refreshToken = crypto.randomUUID();
20
+ await setRefreshToken(sessionId, refreshToken);
21
+ }
22
+ return { token, refreshToken, sessionId };
23
+ }
24
+ /** Create a session for a user (used internally and by MFA verify). */
25
+ export const createSessionForUser = async (userId, metadata) => {
26
+ const sessionId = crypto.randomUUID();
27
+ return createSessionWithRefreshToken(userId, sessionId, metadata);
28
+ };
7
29
  export const register = async (identifier, password, metadata) => {
8
30
  const hashed = await Bun.password.hash(password);
9
31
  const adapter = getAuthAdapter();
@@ -12,11 +34,7 @@ export const register = async (identifier, password, metadata) => {
12
34
  if (role)
13
35
  await adapter.setRoles(user.id, [role]);
14
36
  const sessionId = crypto.randomUUID();
15
- const token = await signToken(user.id, sessionId);
16
- while (await getActiveSessionCount(user.id) >= getMaxSessions()) {
17
- await evictOldestSession(user.id);
18
- }
19
- await createSession(user.id, token, sessionId, metadata);
37
+ const { token, refreshToken } = await createSessionWithRefreshToken(user.id, sessionId, metadata);
20
38
  const evConfig = getEmailVerificationConfig();
21
39
  if (evConfig && getPrimaryField() === "email") {
22
40
  try {
@@ -27,20 +45,21 @@ export const register = async (identifier, password, metadata) => {
27
45
  console.error("[email-verification] Failed to send verification email:", e);
28
46
  }
29
47
  }
30
- return { token, userId: user.id, email: identifier };
48
+ return { token, userId: user.id, email: identifier, refreshToken };
31
49
  };
50
+ // Pre-computed dummy hash so non-existent-user login takes the same time as wrong-password login
51
+ const DUMMY_HASH = await Bun.password.hash("dummy-timing-safe-placeholder");
32
52
  export const login = async (identifier, password, metadata) => {
33
53
  const adapter = getAuthAdapter();
34
54
  const findFn = adapter.findByIdentifier ?? adapter.findByEmail.bind(adapter);
35
55
  const user = await findFn(identifier);
36
- if (!user || !(await Bun.password.verify(password, user.passwordHash))) {
56
+ // Always verify against a hash to prevent timing-based user enumeration
57
+ const hashToVerify = user?.passwordHash ?? DUMMY_HASH;
58
+ const passwordValid = await Bun.password.verify(password, hashToVerify);
59
+ if (!user || !passwordValid) {
37
60
  throw new HttpError(401, "Invalid credentials");
38
61
  }
39
- const sessionId = crypto.randomUUID();
40
- const token = await signToken(user.id, sessionId);
41
- while (await getActiveSessionCount(user.id) >= getMaxSessions()) {
42
- await evictOldestSession(user.id);
43
- }
62
+ // Check email verification before MFA to avoid leaking MFA status to unverified users
44
63
  const fullUser = adapter.getUser ? await adapter.getUser(user.id) : null;
45
64
  const googleLinked = fullUser?.providerIds?.some((id) => id.startsWith("google:")) ?? false;
46
65
  const evConfig = getEmailVerificationConfig();
@@ -49,11 +68,86 @@ export const login = async (identifier, password, metadata) => {
49
68
  if (evConfig.required && !verified) {
50
69
  throw new HttpError(403, "Email not verified");
51
70
  }
52
- await createSession(user.id, token, sessionId, metadata);
53
- return { token, userId: user.id, email: fullUser?.email, emailVerified: verified, googleLinked };
54
71
  }
55
- await createSession(user.id, token, sessionId, metadata);
56
- return { token, userId: user.id, email: fullUser?.email, googleLinked };
72
+ // Check MFA — if enabled, return challenge token instead of session
73
+ if (getMfaConfig() && adapter.isMfaEnabled && await adapter.isMfaEnabled(user.id)) {
74
+ const methods = adapter.getMfaMethods
75
+ ? await adapter.getMfaMethods(user.id)
76
+ : ["totp"];
77
+ // Auto-send email OTP if enabled
78
+ let emailOtpHash;
79
+ const emailOtpConfig = getMfaEmailOtpConfig();
80
+ if (methods.includes("emailOtp") && emailOtpConfig) {
81
+ const { code, hash } = generateEmailOtpCode();
82
+ emailOtpHash = hash;
83
+ const email = fullUser?.email;
84
+ if (email)
85
+ await emailOtpConfig.onSend(email, code);
86
+ }
87
+ // Generate WebAuthn authentication options if enabled
88
+ let webauthnChallenge;
89
+ let webauthnOptions;
90
+ const webauthnConfig = getMfaWebAuthnConfig();
91
+ if (methods.includes("webauthn") && webauthnConfig && adapter.getWebAuthnCredentials) {
92
+ const result = await generateWebAuthnAuthenticationOptions(user.id);
93
+ if (result) {
94
+ webauthnChallenge = result.challenge;
95
+ webauthnOptions = result.options;
96
+ }
97
+ }
98
+ const mfaToken = await createMfaChallenge(user.id, { emailOtpHash, webauthnChallenge });
99
+ return { token: "", userId: user.id, mfaRequired: true, mfaToken, mfaMethods: methods, webauthnOptions };
100
+ }
101
+ const sessionId = crypto.randomUUID();
102
+ const { token, refreshToken } = await createSessionWithRefreshToken(user.id, sessionId, metadata);
103
+ if (evConfig && getPrimaryField() === "email" && adapter.getEmailVerified) {
104
+ const verified = await adapter.getEmailVerified(user.id);
105
+ return { token, userId: user.id, email: fullUser?.email, emailVerified: verified, googleLinked, refreshToken };
106
+ }
107
+ return { token, userId: user.id, email: fullUser?.email, googleLinked, refreshToken };
108
+ };
109
+ export const refresh = async (refreshTokenValue) => {
110
+ const result = await getSessionByRefreshToken(refreshTokenValue);
111
+ if (!result) {
112
+ throw new HttpError(401, "Invalid or expired refresh token");
113
+ }
114
+ const { sessionId, userId, newRefreshToken } = result;
115
+ // If the returned newRefreshToken differs from what was sent, we're in a grace window replay.
116
+ // Return the current tokens without rotating again.
117
+ if (newRefreshToken !== refreshTokenValue) {
118
+ const accessToken = await signToken(userId, sessionId, getAccessTokenExpiry());
119
+ return { token: accessToken, refreshToken: newRefreshToken, userId };
120
+ }
121
+ // Normal rotation: generate new refresh + access tokens
122
+ const newRT = crypto.randomUUID();
123
+ const newAccessToken = await signToken(userId, sessionId, getAccessTokenExpiry());
124
+ await rotateRefreshToken(sessionId, newRT, newAccessToken);
125
+ return { token: newAccessToken, refreshToken: newRT, userId };
126
+ };
127
+ export const deleteAccount = async (userId, password) => {
128
+ const adapter = getAuthAdapter();
129
+ if (!adapter.deleteUser) {
130
+ throw new HttpError(501, "Auth adapter does not support deleteUser");
131
+ }
132
+ // Verify password for credential accounts
133
+ if (password) {
134
+ const user = adapter.getUser ? await adapter.getUser(userId) : null;
135
+ const email = user?.email;
136
+ if (email) {
137
+ const findFn = adapter.findByIdentifier ?? adapter.findByEmail.bind(adapter);
138
+ const found = await findFn(email);
139
+ if (found && !(await Bun.password.verify(password, found.passwordHash))) {
140
+ throw new HttpError(401, "Invalid password");
141
+ }
142
+ }
143
+ }
144
+ else if (adapter.hasPassword && await adapter.hasPassword(userId)) {
145
+ throw new HttpError(400, "Password is required to delete a credential account");
146
+ }
147
+ // Revoke all sessions
148
+ await deleteUserSessions(userId);
149
+ // Delete the user
150
+ await adapter.deleteUser(userId);
57
151
  };
58
152
  export const logout = async (token) => {
59
153
  if (token) {
@@ -0,0 +1,84 @@
1
+ export interface MfaSetupResult {
2
+ secret: string;
3
+ uri: string;
4
+ }
5
+ export declare const setupMfa: (userId: string) => Promise<MfaSetupResult>;
6
+ export declare const verifySetup: (userId: string, code: string) => Promise<string[]>;
7
+ export declare const verifyTotp: (userId: string, code: string) => Promise<boolean>;
8
+ export declare const verifyRecoveryCode: (userId: string, code: string) => Promise<boolean>;
9
+ export declare const disableMfa: (userId: string, code: string) => Promise<void>;
10
+ export declare const regenerateRecoveryCodes: (userId: string, code: string) => Promise<string[]>;
11
+ /** Generate a cryptographically random numeric OTP code. Returns { code, hash }. */
12
+ export declare const generateEmailOtpCode: (length?: number) => {
13
+ code: string;
14
+ hash: string;
15
+ };
16
+ /** Verify an email OTP code against a stored hash. */
17
+ export declare const verifyEmailOtp: (emailOtpHash: string, code: string) => boolean;
18
+ /**
19
+ * Initiate email OTP setup: sends a verification code to the user's email.
20
+ * Returns a setup challenge token that must be confirmed via confirmEmailOtp.
21
+ */
22
+ export declare const initiateEmailOtp: (userId: string) => Promise<string>;
23
+ /**
24
+ * Confirm email OTP setup: verifies the code sent during initiateEmailOtp.
25
+ * Enables email OTP as an MFA method. Returns recovery codes if MFA was not previously active.
26
+ */
27
+ export declare const confirmEmailOtp: (userId: string, setupToken: string, code: string) => Promise<string[] | null>;
28
+ /**
29
+ * Disable email OTP for a user.
30
+ * If TOTP is also enabled, requires a TOTP code. Otherwise requires password.
31
+ */
32
+ export declare const disableEmailOtp: (userId: string, params: {
33
+ code?: string;
34
+ password?: string;
35
+ }) => Promise<void>;
36
+ /** Get the MFA methods enabled for a user. */
37
+ export declare const getMfaMethods: (userId: string) => Promise<string[]>;
38
+ /**
39
+ * Eager startup check — call at route mount time to fail fast if the peer dependency is missing.
40
+ */
41
+ export declare const assertWebAuthnDependency: () => Promise<void>;
42
+ /**
43
+ * Generate WebAuthn authentication options for the login MFA flow.
44
+ * Called from auth.ts login when the user has "webauthn" in their methods.
45
+ */
46
+ export declare const generateWebAuthnAuthenticationOptions: (userId: string) => Promise<{
47
+ challenge: string;
48
+ options: Record<string, unknown>;
49
+ } | null>;
50
+ /**
51
+ * Initiate WebAuthn registration: generates registration options for the client.
52
+ * Returns options + a registration challenge token.
53
+ */
54
+ export declare const initiateWebAuthnRegistration: (userId: string) => Promise<{
55
+ options: Record<string, unknown>;
56
+ registrationToken: string;
57
+ }>;
58
+ /**
59
+ * Complete WebAuthn registration: verifies attestation and stores the credential.
60
+ * Returns recovery codes if this is the first MFA method.
61
+ */
62
+ export declare const completeWebAuthnRegistration: (userId: string, registrationToken: string, attestationResponse: any, name?: string) => Promise<{
63
+ credentialId: string;
64
+ recoveryCodes: string[] | null;
65
+ }>;
66
+ /**
67
+ * Verify a WebAuthn authentication assertion during login MFA.
68
+ */
69
+ export declare const verifyWebAuthn: (userId: string, assertionResponse: any, expectedChallenge: string) => Promise<boolean>;
70
+ /**
71
+ * Remove a single WebAuthn credential.
72
+ * Only requires identity verification when removing the last credential of the last MFA method.
73
+ */
74
+ export declare const removeWebAuthnCredential: (userId: string, credentialId: string, params: {
75
+ code?: string;
76
+ password?: string;
77
+ }) => Promise<void>;
78
+ /**
79
+ * Disable WebAuthn entirely: removes all credentials and the method.
80
+ */
81
+ export declare const disableWebAuthn: (userId: string, params: {
82
+ code?: string;
83
+ password?: string;
84
+ }) => Promise<void>;