@lastshotlabs/bunshot 0.0.9 → 0.0.13

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.
@@ -1,4 +1,4 @@
1
- import { createRoute } from "@hono/zod-openapi";
1
+ import { createRoute, withSecurity } from "../lib/createRoute";
2
2
  import { z } from "zod";
3
3
  import { setCookie, getCookie, deleteCookie } from "hono/cookie";
4
4
  import * as AuthService from "../services/auth";
@@ -9,10 +9,17 @@ import { isLimited, trackAttempt, bustAuthLimit } from "../lib/authRateLimit";
9
9
  import { getAuthAdapter } from "../lib/authAdapter";
10
10
  import { createRouter } from "../lib/context";
11
11
  import { getVerificationToken, deleteVerificationToken, createVerificationToken } from "../lib/emailVerification";
12
+ import { createResetToken, consumeResetToken } from "../lib/resetPassword";
12
13
  import { getUserSessions, deleteSession } from "../lib/session";
13
14
  const isProd = process.env.NODE_ENV === "production";
14
- const TokenResponse = z.object({ token: z.string(), emailVerified: z.boolean().optional() });
15
- const ErrorResponse = z.object({ error: z.string() });
15
+ const TokenResponse = z.object({
16
+ token: z.string().describe("JWT session token. Also set as an HttpOnly session cookie."),
17
+ userId: z.string().describe("Unique user ID."),
18
+ email: z.string().optional().describe("User's email address (present when primaryField is 'email')."),
19
+ emailVerified: z.boolean().optional().describe("Whether the email address has been verified (present when emailVerification is configured)."),
20
+ googleLinked: z.boolean().optional().describe("Whether a Google OAuth account is linked to this user."),
21
+ }).openapi("TokenResponse");
22
+ const ErrorResponse = z.object({ error: z.string().describe("Human-readable error message.") }).openapi("ErrorResponse");
16
23
  const tags = ["Auth"];
17
24
  const cookieOptions = {
18
25
  httpOnly: true,
@@ -22,7 +29,7 @@ const cookieOptions = {
22
29
  maxAge: 60 * 60 * 24 * 7, // 7 days
23
30
  };
24
31
  const clientIp = (xff, xri) => (xff ? xff.split(",")[0]?.trim() : undefined) ?? xri ?? undefined;
25
- export const createAuthRouter = ({ primaryField, emailVerification, rateLimit }) => {
32
+ export const createAuthRouter = ({ primaryField, emailVerification, passwordReset, rateLimit }) => {
26
33
  const router = createRouter();
27
34
  const RegisterSchema = makeRegisterSchema(primaryField);
28
35
  const LoginSchema = makeLoginSchema(primaryField);
@@ -33,16 +40,20 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
33
40
  const registerOpts = { windowMs: rateLimit?.register?.windowMs ?? 60 * 60 * 1000, max: rateLimit?.register?.max ?? 5 };
34
41
  const verifyOpts = { windowMs: rateLimit?.verifyEmail?.windowMs ?? 15 * 60 * 1000, max: rateLimit?.verifyEmail?.max ?? 10 };
35
42
  const resendOpts = { windowMs: rateLimit?.resendVerification?.windowMs ?? 60 * 60 * 1000, max: rateLimit?.resendVerification?.max ?? 3 };
43
+ const forgotOpts = { windowMs: rateLimit?.forgotPassword?.windowMs ?? 15 * 60 * 1000, max: rateLimit?.forgotPassword?.max ?? 5 };
44
+ const resetOpts = { windowMs: rateLimit?.resetPassword?.windowMs ?? 15 * 60 * 1000, max: rateLimit?.resetPassword?.max ?? 10 };
36
45
  router.openapi(createRoute({
37
46
  method: "post",
38
47
  path: "/auth/register",
48
+ summary: "Register a new account",
49
+ description: "Creates a new user account and returns a JWT session token. The token is also set as an HttpOnly session cookie. Rate-limited by IP.",
39
50
  tags,
40
- request: { body: { content: { "application/json": { schema: RegisterSchema } } } },
51
+ request: { body: { content: { "application/json": { schema: RegisterSchema } }, description: "Registration credentials." } },
41
52
  responses: {
42
- 201: { content: { "application/json": { schema: TokenResponse } }, description: "Registered" },
43
- 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Validation error" },
53
+ 201: { content: { "application/json": { schema: TokenResponse } }, description: "Account created. Returns a session token." },
54
+ 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Validation error (e.g. missing field, password too short)." },
44
55
  409: { content: { "application/json": { schema: ErrorResponse } }, description: alreadyRegisteredMsg },
45
- 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many attempts" },
56
+ 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many registration attempts from this IP. Try again later." },
46
57
  },
47
58
  }), async (c) => {
48
59
  const ip = clientIp(c.req.header("x-forwarded-for"), c.req.header("x-real-ip")) ?? "unknown";
@@ -55,20 +66,22 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
55
66
  ipAddress: ip !== "unknown" ? ip : undefined,
56
67
  userAgent: c.req.header("user-agent") ?? undefined,
57
68
  };
58
- const token = await AuthService.register(identifier, body.password, metadata);
59
- setCookie(c, COOKIE_TOKEN, token, cookieOptions);
60
- return c.json({ token }, 201);
69
+ const result = await AuthService.register(identifier, body.password, metadata);
70
+ setCookie(c, COOKIE_TOKEN, result.token, cookieOptions);
71
+ return c.json(result, 201);
61
72
  });
62
73
  router.openapi(createRoute({
63
74
  method: "post",
64
75
  path: "/auth/login",
76
+ summary: "Log in",
77
+ description: "Authenticates with credentials and returns a JWT session token. The token is also set as an HttpOnly session cookie. Failed attempts are rate-limited per identifier.",
65
78
  tags,
66
- request: { body: { content: { "application/json": { schema: LoginSchema } } } },
79
+ request: { body: { content: { "application/json": { schema: LoginSchema } }, description: "Login credentials." } },
67
80
  responses: {
68
- 200: { content: { "application/json": { schema: TokenResponse } }, description: "Logged in" },
69
- 401: { content: { "application/json": { schema: ErrorResponse } }, description: "Invalid credentials" },
70
- 403: { content: { "application/json": { schema: ErrorResponse } }, description: "Email not verified" },
71
- 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many attempts" },
81
+ 200: { content: { "application/json": { schema: TokenResponse } }, description: "Authenticated. Returns a session token." },
82
+ 401: { content: { "application/json": { schema: ErrorResponse } }, description: "Invalid credentials." },
83
+ 403: { content: { "application/json": { schema: ErrorResponse } }, description: "Email not verified. Verification is required before login." },
84
+ 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many failed login attempts for this identifier. Try again later." },
72
85
  },
73
86
  }), async (c) => {
74
87
  const body = c.req.valid("json");
@@ -93,27 +106,29 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
93
106
  }
94
107
  });
95
108
  router.use("/auth/me", userAuth);
96
- router.openapi(createRoute({
109
+ router.openapi(withSecurity(createRoute({
97
110
  method: "get",
98
111
  path: "/auth/me",
112
+ summary: "Get current user",
113
+ description: "Returns the authenticated user's profile. Requires a valid session via cookie or x-user-token header.",
99
114
  tags,
100
115
  responses: {
101
116
  200: {
102
117
  content: {
103
118
  "application/json": {
104
119
  schema: z.object({
105
- userId: z.string(),
106
- email: z.string().optional(),
107
- emailVerified: z.boolean().optional(),
108
- googleLinked: z.boolean().optional(),
120
+ userId: z.string().describe("Unique user ID."),
121
+ email: z.string().optional().describe("User's email address."),
122
+ emailVerified: z.boolean().optional().describe("Whether the email address has been verified."),
123
+ googleLinked: z.boolean().optional().describe("Whether a Google OAuth account is linked."),
109
124
  }),
110
125
  },
111
126
  },
112
- description: "Current user",
127
+ description: "Authenticated user's profile.",
113
128
  },
114
- 401: { content: { "application/json": { schema: ErrorResponse } }, description: "Unauthorized" },
129
+ 401: { content: { "application/json": { schema: ErrorResponse } }, description: "No valid session." },
115
130
  },
116
- }), async (c) => {
131
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
117
132
  const authUserId = c.get("authUserId");
118
133
  const adapter = getAuthAdapter();
119
134
  const user = adapter.getUser ? await adapter.getUser(authUserId) : null;
@@ -121,17 +136,20 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
121
136
  return c.json({ userId: authUserId, email: user?.email, emailVerified: user?.emailVerified, googleLinked }, 200);
122
137
  });
123
138
  router.use("/auth/set-password", userAuth);
124
- router.openapi(createRoute({
139
+ router.openapi(withSecurity(createRoute({
125
140
  method: "post",
126
141
  path: "/auth/set-password",
142
+ summary: "Set or update password",
143
+ description: "Sets or updates the password for the authenticated user. Useful for OAuth-only users who want to add a password. Requires a valid session.",
127
144
  tags,
128
- request: { body: { content: { "application/json": { schema: z.object({ password: z.string().min(8) }) } } } },
145
+ request: { body: { content: { "application/json": { schema: z.object({ password: z.string().min(8).describe("New password. Minimum 8 characters.") }) } }, description: "New password." } },
129
146
  responses: {
130
- 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Password set" },
131
- 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Validation error" },
132
- 501: { content: { "application/json": { schema: ErrorResponse } }, description: "Not supported by adapter" },
147
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Password updated successfully." },
148
+ 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Validation error (e.g. password too short)." },
149
+ 401: { content: { "application/json": { schema: ErrorResponse } }, description: "No valid session." },
150
+ 501: { content: { "application/json": { schema: ErrorResponse } }, description: "The configured auth adapter does not support setPassword." },
133
151
  },
134
- }), async (c) => {
152
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
135
153
  const adapter = getAuthAdapter();
136
154
  if (!adapter.setPassword) {
137
155
  return c.json({ error: "Auth adapter does not support setPassword" }, 501);
@@ -145,9 +163,11 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
145
163
  router.openapi(createRoute({
146
164
  method: "post",
147
165
  path: "/auth/logout",
166
+ summary: "Log out",
167
+ description: "Invalidates the current session and clears the session cookie. Safe to call even without an active session.",
148
168
  tags,
149
169
  responses: {
150
- 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Logged out" },
170
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Logged out. Session is invalidated and cookie is cleared." },
151
171
  },
152
172
  }), async (c) => {
153
173
  const token = getCookie(c, COOKIE_TOKEN) ?? c.req.header(HEADER_USER_TOKEN) ?? null;
@@ -160,12 +180,14 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
160
180
  router.openapi(createRoute({
161
181
  method: "post",
162
182
  path: "/auth/verify-email",
183
+ summary: "Verify email address",
184
+ description: "Consumes a single-use email verification token and marks the account as verified. The token is delivered by the `emailVerification.onSend` callback configured in CreateAppConfig. Rate-limited by IP.",
163
185
  tags,
164
- request: { body: { content: { "application/json": { schema: z.object({ token: z.string() }) } } } },
186
+ request: { body: { content: { "application/json": { schema: z.object({ token: z.string().describe("Single-use verification token received via email.") }) } }, description: "Verification token." } },
165
187
  responses: {
166
- 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Email verified" },
167
- 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Invalid or expired token" },
168
- 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many attempts" },
188
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Email verified successfully." },
189
+ 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Invalid or expired verification token." },
190
+ 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many verification attempts from this IP. Try again later." },
169
191
  },
170
192
  }), async (c) => {
171
193
  const ip = c.req.header("x-forwarded-for") ?? "unknown";
@@ -183,17 +205,20 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
183
205
  return c.json({ message: "Email verified" }, 200);
184
206
  });
185
207
  router.use("/auth/resend-verification", userAuth);
186
- router.openapi(createRoute({
208
+ router.openapi(withSecurity(createRoute({
187
209
  method: "post",
188
210
  path: "/auth/resend-verification",
211
+ summary: "Resend verification email",
212
+ description: "Sends a new verification email to the authenticated user's address. Returns 400 if already verified. Rate-limited per user. Requires a valid session.",
189
213
  tags,
190
214
  responses: {
191
- 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Verification email sent" },
192
- 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Already verified" },
193
- 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many attempts" },
194
- 501: { content: { "application/json": { schema: ErrorResponse } }, description: "Not supported by adapter" },
215
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Verification email sent." },
216
+ 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Email is already verified, or no email address on file." },
217
+ 401: { content: { "application/json": { schema: ErrorResponse } }, description: "No valid session." },
218
+ 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many resend attempts for this user. Try again later." },
219
+ 501: { content: { "application/json": { schema: ErrorResponse } }, description: "The configured auth adapter does not support email verification." },
195
220
  },
196
- }), async (c) => {
221
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
197
222
  const adapter = getAuthAdapter();
198
223
  if (!adapter.getEmailVerified || !adapter.getUser) {
199
224
  return c.json({ error: "Auth adapter does not support email verification" }, 501);
@@ -213,47 +238,139 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
213
238
  return c.json({ message: "Verification email sent" }, 200);
214
239
  });
215
240
  }
241
+ // Password reset routes — only mounted when passwordReset is configured and primaryField is "email"
242
+ if (passwordReset && primaryField === "email") {
243
+ router.openapi(createRoute({
244
+ method: "post",
245
+ path: "/auth/forgot-password",
246
+ summary: "Request password reset",
247
+ description: "Sends a password reset email if the address is registered. Always returns 200 regardless of whether the address exists, to prevent email enumeration. Rate-limited by both IP and email address.",
248
+ tags,
249
+ request: { body: { content: { "application/json": { schema: z.object({ email: z.string().email().describe("Email address to send the reset link to.") }) } }, description: "Email address for the account to reset." } },
250
+ responses: {
251
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Request received. A reset email will be sent if the address is registered." },
252
+ 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Validation error (e.g. not a valid email address)." },
253
+ 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many attempts from this IP or for this email address. Try again later." },
254
+ },
255
+ }), async (c) => {
256
+ const ip = clientIp(c.req.header("x-forwarded-for"), c.req.header("x-real-ip")) ?? "unknown";
257
+ const { email } = c.req.valid("json");
258
+ // Rate-limit by both IP and email to prevent distributed email-bombing
259
+ const ipLimited = await trackAttempt(`forgot:ip:${ip}`, forgotOpts);
260
+ const emailLimited = await trackAttempt(`forgot:email:${email}`, forgotOpts);
261
+ if (ipLimited || emailLimited) {
262
+ return c.json({ error: "Too many attempts. Try again later." }, 429);
263
+ }
264
+ const adapter = getAuthAdapter();
265
+ const user = await adapter.findByEmail(email);
266
+ // Fire-and-forget: the response does not wait for token creation or email sending,
267
+ // which reduces obvious timing differences between registered and unregistered emails.
268
+ const msg = { message: "If that email is registered, a password reset link has been sent." };
269
+ if (user) {
270
+ void (async () => {
271
+ try {
272
+ const token = await createResetToken(user.id, email);
273
+ await passwordReset.onSend(email, token);
274
+ }
275
+ catch (err) {
276
+ console.error("Failed to send password reset email:", err);
277
+ }
278
+ })();
279
+ }
280
+ return c.json(msg, 200);
281
+ });
282
+ router.openapi(createRoute({
283
+ method: "post",
284
+ path: "/auth/reset-password",
285
+ summary: "Reset password",
286
+ description: "Consumes a single-use reset token and sets a new password. All active sessions are revoked after a successful reset to invalidate any stolen JWTs. Rate-limited by IP.",
287
+ tags,
288
+ request: {
289
+ body: {
290
+ content: {
291
+ "application/json": {
292
+ schema: z.object({
293
+ token: z.string().describe("Single-use reset token received via email."),
294
+ password: z.string().min(8).describe("New password. Minimum 8 characters."),
295
+ }),
296
+ },
297
+ },
298
+ description: "Reset token and new password.",
299
+ },
300
+ },
301
+ responses: {
302
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Password reset. All sessions have been revoked." },
303
+ 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Validation error, or the reset token is invalid or expired." },
304
+ 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many reset attempts from this IP. Try again later." },
305
+ 501: { content: { "application/json": { schema: ErrorResponse } }, description: "The configured auth adapter does not support setPassword." },
306
+ },
307
+ }), async (c) => {
308
+ const ip = clientIp(c.req.header("x-forwarded-for"), c.req.header("x-real-ip")) ?? "unknown";
309
+ if (await trackAttempt(`reset:${ip}`, resetOpts)) {
310
+ return c.json({ error: "Too many attempts. Try again later." }, 429);
311
+ }
312
+ const { token, password } = c.req.valid("json");
313
+ // consumeResetToken atomically gets and deletes — prevents concurrent replay
314
+ const entry = await consumeResetToken(token);
315
+ if (!entry)
316
+ return c.json({ error: "Invalid or expired reset token" }, 400);
317
+ const adapter = getAuthAdapter();
318
+ if (!adapter.setPassword) {
319
+ return c.json({ error: "Auth adapter does not support setPassword" }, 501);
320
+ }
321
+ const passwordHash = await Bun.password.hash(password);
322
+ await adapter.setPassword(entry.userId, passwordHash);
323
+ // Revoke all sessions so stolen JWTs can't stay valid after a reset
324
+ const sessions = await getUserSessions(entry.userId);
325
+ await Promise.all(sessions.map((s) => deleteSession(s.sessionId)));
326
+ return c.json({ message: "Password reset successfully" }, 200);
327
+ });
328
+ }
216
329
  // ---------------------------------------------------------------------------
217
330
  // Session management
218
331
  // ---------------------------------------------------------------------------
219
332
  const SessionInfoSchema = z.object({
220
- sessionId: z.string(),
221
- createdAt: z.number(),
222
- lastActiveAt: z.number(),
223
- expiresAt: z.number(),
224
- ipAddress: z.string().optional(),
225
- userAgent: z.string().optional(),
226
- isActive: z.boolean(),
227
- });
333
+ sessionId: z.string().describe("Unique session identifier (UUID)."),
334
+ createdAt: z.number().describe("Unix timestamp (ms) when the session was created."),
335
+ lastActiveAt: z.number().describe("Unix timestamp (ms) of the most recent authenticated request (updated when trackLastActive is enabled)."),
336
+ expiresAt: z.number().describe("Unix timestamp (ms) when the session expires."),
337
+ ipAddress: z.string().optional().describe("IP address of the client at session creation."),
338
+ userAgent: z.string().optional().describe("User-agent string of the client at session creation."),
339
+ isActive: z.boolean().describe("Whether the session is currently valid and unexpired."),
340
+ }).openapi("SessionInfo");
228
341
  router.use("/auth/sessions", userAuth);
229
342
  router.use("/auth/sessions/*", userAuth);
230
- router.openapi(createRoute({
343
+ router.openapi(withSecurity(createRoute({
231
344
  method: "get",
232
345
  path: "/auth/sessions",
346
+ summary: "List sessions",
347
+ description: "Returns all sessions for the authenticated user. Includes inactive sessions when `sessionPolicy.includeInactiveSessions` is enabled. Requires a valid session.",
233
348
  tags,
234
349
  responses: {
235
350
  200: {
236
351
  content: { "application/json": { schema: z.object({ sessions: z.array(SessionInfoSchema) }) } },
237
- description: "List of sessions for the current user",
352
+ description: "Sessions belonging to the authenticated user.",
238
353
  },
239
- 401: { content: { "application/json": { schema: ErrorResponse } }, description: "Unauthorized" },
354
+ 401: { content: { "application/json": { schema: ErrorResponse } }, description: "No valid session." },
240
355
  },
241
- }), async (c) => {
356
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
242
357
  const userId = c.get("authUserId");
243
358
  const sessions = await getUserSessions(userId);
244
359
  return c.json({ sessions }, 200);
245
360
  });
246
- router.openapi(createRoute({
361
+ router.openapi(withSecurity(createRoute({
247
362
  method: "delete",
248
363
  path: "/auth/sessions/{sessionId}",
364
+ summary: "Revoke a session",
365
+ description: "Revokes a specific session by ID. Users can only revoke their own sessions. Useful for 'sign out of other devices' flows. Requires a valid session.",
249
366
  tags,
250
- request: { params: z.object({ sessionId: z.string() }) },
367
+ request: { params: z.object({ sessionId: z.string().describe("UUID of the session to revoke.") }) },
251
368
  responses: {
252
- 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Session revoked" },
253
- 401: { content: { "application/json": { schema: ErrorResponse } }, description: "Unauthorized" },
254
- 404: { content: { "application/json": { schema: ErrorResponse } }, description: "Session not found" },
369
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Session revoked successfully." },
370
+ 401: { content: { "application/json": { schema: ErrorResponse } }, description: "No valid session." },
371
+ 404: { content: { "application/json": { schema: ErrorResponse } }, description: "Session not found or does not belong to the authenticated user." },
255
372
  },
256
- }), async (c) => {
373
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
257
374
  const userId = c.get("authUserId");
258
375
  const { sessionId } = c.req.valid("param");
259
376
  const sessions = await getUserSessions(userId);
@@ -1,7 +1,14 @@
1
1
  import type { SessionMetadata } from "../lib/session";
2
- export declare const register: (identifier: string, password: string, metadata?: SessionMetadata) => Promise<string>;
2
+ export declare const register: (identifier: string, password: string, metadata?: SessionMetadata) => Promise<{
3
+ token: string;
4
+ userId: string;
5
+ email?: string;
6
+ }>;
3
7
  export declare const login: (identifier: string, password: string, metadata?: SessionMetadata) => Promise<{
4
8
  token: string;
9
+ userId: string;
10
+ email?: string;
5
11
  emailVerified?: boolean;
12
+ googleLinked?: boolean;
6
13
  }>;
7
14
  export declare const logout: (token: string | null) => Promise<void>;
@@ -27,7 +27,7 @@ export const register = async (identifier, password, metadata) => {
27
27
  console.error("[email-verification] Failed to send verification email:", e);
28
28
  }
29
29
  }
30
- return token;
30
+ return { token, userId: user.id, email: identifier };
31
31
  };
32
32
  export const login = async (identifier, password, metadata) => {
33
33
  const adapter = getAuthAdapter();
@@ -41,6 +41,8 @@ export const login = async (identifier, password, metadata) => {
41
41
  while (await getActiveSessionCount(user.id) >= getMaxSessions()) {
42
42
  await evictOldestSession(user.id);
43
43
  }
44
+ const fullUser = adapter.getUser ? await adapter.getUser(user.id) : null;
45
+ const googleLinked = fullUser?.providerIds?.some((id) => id.startsWith("google:")) ?? false;
44
46
  const evConfig = getEmailVerificationConfig();
45
47
  if (evConfig && getPrimaryField() === "email" && adapter.getEmailVerified) {
46
48
  const verified = await adapter.getEmailVerified(user.id);
@@ -48,10 +50,10 @@ export const login = async (identifier, password, metadata) => {
48
50
  throw new HttpError(403, "Email not verified");
49
51
  }
50
52
  await createSession(user.id, token, sessionId, metadata);
51
- return { token, emailVerified: verified };
53
+ return { token, userId: user.id, email: fullUser?.email, emailVerified: verified, googleLinked };
52
54
  }
53
55
  await createSession(user.id, token, sessionId, metadata);
54
- return { token };
56
+ return { token, userId: user.id, email: fullUser?.email, googleLinked };
55
57
  };
56
58
  export const logout = async (token) => {
57
59
  if (token) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastshotlabs/bunshot",
3
- "version": "0.0.9",
3
+ "version": "0.0.13",
4
4
  "description": "Batteries-included Bun + Hono API framework — auth, sessions, rate limiting, WebSocket, queues, and OpenAPI docs out of the box",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,6 +22,18 @@
22
22
  ".": {
23
23
  "import": "./dist/index.js",
24
24
  "types": "./dist/index.d.ts"
25
+ },
26
+ "./mongo": {
27
+ "import": "./dist/entrypoints/mongo.js",
28
+ "types": "./dist/entrypoints/mongo.d.ts"
29
+ },
30
+ "./redis": {
31
+ "import": "./dist/entrypoints/redis.js",
32
+ "types": "./dist/entrypoints/redis.d.ts"
33
+ },
34
+ "./queue": {
35
+ "import": "./dist/entrypoints/queue.js",
36
+ "types": "./dist/entrypoints/queue.d.ts"
25
37
  }
26
38
  },
27
39
  "bin": {
@@ -41,17 +53,35 @@
41
53
  "@hono/zod-openapi": "1.2.2",
42
54
  "@scalar/hono-api-reference": "0.10.0",
43
55
  "arctic": "^3.7.0",
44
- "bullmq": "^5.70.4",
45
- "hono": "4.12.7",
46
- "ioredis": "5.10.0",
47
- "jose": "6.2.0",
48
- "mongoose": "9.2.4",
49
- "zod": "4.3.6"
56
+ "jose": "6.2.0"
57
+ },
58
+ "peerDependencies": {
59
+ "hono": ">=4.12 <5",
60
+ "zod": ">=4.0 <5",
61
+ "mongoose": ">=9.0 <10",
62
+ "ioredis": ">=5.0 <6",
63
+ "bullmq": ">=5.0 <6"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "mongoose": {
67
+ "optional": true
68
+ },
69
+ "ioredis": {
70
+ "optional": true
71
+ },
72
+ "bullmq": {
73
+ "optional": true
74
+ }
50
75
  },
51
76
  "devDependencies": {
52
77
  "@types/bun": "1.3.10",
53
78
  "tsc-alias": "^1.8.16",
54
- "typescript": "^5.9.3"
79
+ "typescript": "^5.9.3",
80
+ "hono": ">=4.12",
81
+ "zod": ">=4.0",
82
+ "mongoose": "9.2.4",
83
+ "ioredis": "5.10.0",
84
+ "bullmq": "^5.70.4"
55
85
  },
56
86
  "publishConfig": {
57
87
  "access": "public"