@lastshotlabs/bunshot 0.0.8 → 0.0.10

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 (44) hide show
  1. package/README.md +97 -7
  2. package/dist/adapters/memoryAuth.d.ts +13 -3
  3. package/dist/adapters/memoryAuth.js +116 -8
  4. package/dist/adapters/sqliteAuth.d.ts +13 -3
  5. package/dist/adapters/sqliteAuth.js +93 -15
  6. package/dist/app.d.ts +39 -2
  7. package/dist/app.js +23 -5
  8. package/dist/cli.js +0 -0
  9. package/dist/entrypoints/mongo.d.ts +3 -0
  10. package/dist/entrypoints/mongo.js +3 -0
  11. package/dist/entrypoints/queue.d.ts +2 -0
  12. package/dist/entrypoints/queue.js +1 -0
  13. package/dist/entrypoints/redis.d.ts +1 -0
  14. package/dist/entrypoints/redis.js +1 -0
  15. package/dist/index.d.ts +6 -8
  16. package/dist/index.js +5 -6
  17. package/dist/lib/appConfig.d.ts +17 -0
  18. package/dist/lib/appConfig.js +20 -0
  19. package/dist/lib/context.d.ts +1 -0
  20. package/dist/lib/emailVerification.js +11 -10
  21. package/dist/lib/jwt.d.ts +1 -1
  22. package/dist/lib/jwt.js +1 -1
  23. package/dist/lib/mongo.d.ts +9 -4
  24. package/dist/lib/mongo.js +61 -10
  25. package/dist/lib/oauth.js +11 -10
  26. package/dist/lib/queue.d.ts +3 -4
  27. package/dist/lib/queue.js +18 -3
  28. package/dist/lib/redis.d.ts +3 -8
  29. package/dist/lib/redis.js +19 -8
  30. package/dist/lib/resetPassword.d.ts +12 -0
  31. package/dist/lib/resetPassword.js +95 -0
  32. package/dist/lib/session.d.ts +20 -3
  33. package/dist/lib/session.js +288 -35
  34. package/dist/middleware/cacheResponse.js +10 -9
  35. package/dist/middleware/identify.js +21 -7
  36. package/dist/models/AuthUser.d.ts +14 -106
  37. package/dist/models/AuthUser.js +31 -14
  38. package/dist/routes/auth.d.ts +3 -2
  39. package/dist/routes/auth.js +139 -4
  40. package/dist/routes/oauth.js +13 -4
  41. package/dist/services/auth.d.ts +3 -2
  42. package/dist/services/auth.js +20 -11
  43. package/dist/ws/index.js +6 -3
  44. package/package.json +39 -9
@@ -1,14 +1,31 @@
1
- import mongoose from "mongoose";
2
- import { authConnection } from "../lib/mongo";
3
- const AuthUserSchema = new mongoose.Schema({
4
- email: { type: String, unique: true, sparse: true, lowercase: true },
5
- password: { type: String },
6
- /** Compound provider keys: ["google:123456", "apple:000111"] */
7
- providerIds: [{ type: String }],
8
- /** App-defined roles assigned to this user: ["admin", "editor", ...] */
9
- roles: [{ type: String }],
10
- /** Whether the user's email address has been verified. */
11
- emailVerified: { type: Boolean, default: false },
12
- }, { timestamps: true });
13
- AuthUserSchema.index({ providerIds: 1 });
14
- export const AuthUser = authConnection.model("AuthUser", AuthUserSchema);
1
+ import { authConnection, mongoose } from "../lib/mongo";
2
+ // Lazily register the model — authConnection and mongoose are proxies that
3
+ // resolve once connectAuthMongo() / connectMongo() has been called.
4
+ let _AuthUser = null;
5
+ function getAuthUser() {
6
+ if (!_AuthUser) {
7
+ const { Schema } = mongoose;
8
+ const schema = new Schema({
9
+ email: { type: String, unique: true, sparse: true, lowercase: true },
10
+ password: { type: String },
11
+ /** Compound provider keys: ["google:123456", "apple:000111"] */
12
+ providerIds: [{ type: String }],
13
+ /** App-defined roles assigned to this user: ["admin", "editor", ...] */
14
+ roles: [{ type: String }],
15
+ /** Whether the user's email address has been verified. */
16
+ emailVerified: { type: Boolean, default: false },
17
+ }, { timestamps: true });
18
+ schema.index({ providerIds: 1 });
19
+ _AuthUser = authConnection.model("AuthUser", schema);
20
+ }
21
+ return _AuthUser;
22
+ }
23
+ // Export a Proxy so callers can use AuthUser.findOne() etc. at any time after
24
+ // connectAuthMongo() / connectMongo() has been called.
25
+ export const AuthUser = new Proxy({}, {
26
+ get(_, prop) {
27
+ const model = getAuthUser();
28
+ const val = model[prop];
29
+ return typeof val === "function" ? val.bind(model) : val;
30
+ },
31
+ });
@@ -1,8 +1,9 @@
1
- import type { PrimaryField, EmailVerificationConfig } from "../lib/appConfig";
1
+ import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "../lib/appConfig";
2
2
  import type { AuthRateLimitConfig } from "../app";
3
3
  export interface AuthRouterOptions {
4
4
  primaryField: PrimaryField;
5
5
  emailVerification?: EmailVerificationConfig;
6
+ passwordReset?: PasswordResetConfig;
6
7
  rateLimit?: AuthRateLimitConfig;
7
8
  }
8
- export declare const createAuthRouter: ({ primaryField, emailVerification, rateLimit }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
9
+ export declare const createAuthRouter: ({ primaryField, emailVerification, passwordReset, rateLimit }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
@@ -9,6 +9,8 @@ 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";
13
+ import { getUserSessions, deleteSession } from "../lib/session";
12
14
  const isProd = process.env.NODE_ENV === "production";
13
15
  const TokenResponse = z.object({ token: z.string(), emailVerified: z.boolean().optional() });
14
16
  const ErrorResponse = z.object({ error: z.string() });
@@ -20,7 +22,8 @@ const cookieOptions = {
20
22
  path: "/",
21
23
  maxAge: 60 * 60 * 24 * 7, // 7 days
22
24
  };
23
- export const createAuthRouter = ({ primaryField, emailVerification, rateLimit }) => {
25
+ const clientIp = (xff, xri) => (xff ? xff.split(",")[0]?.trim() : undefined) ?? xri ?? undefined;
26
+ export const createAuthRouter = ({ primaryField, emailVerification, passwordReset, rateLimit }) => {
24
27
  const router = createRouter();
25
28
  const RegisterSchema = makeRegisterSchema(primaryField);
26
29
  const LoginSchema = makeLoginSchema(primaryField);
@@ -31,6 +34,8 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
31
34
  const registerOpts = { windowMs: rateLimit?.register?.windowMs ?? 60 * 60 * 1000, max: rateLimit?.register?.max ?? 5 };
32
35
  const verifyOpts = { windowMs: rateLimit?.verifyEmail?.windowMs ?? 15 * 60 * 1000, max: rateLimit?.verifyEmail?.max ?? 10 };
33
36
  const resendOpts = { windowMs: rateLimit?.resendVerification?.windowMs ?? 60 * 60 * 1000, max: rateLimit?.resendVerification?.max ?? 3 };
37
+ const forgotOpts = { windowMs: rateLimit?.forgotPassword?.windowMs ?? 15 * 60 * 1000, max: rateLimit?.forgotPassword?.max ?? 5 };
38
+ const resetOpts = { windowMs: rateLimit?.resetPassword?.windowMs ?? 15 * 60 * 1000, max: rateLimit?.resetPassword?.max ?? 10 };
34
39
  router.openapi(createRoute({
35
40
  method: "post",
36
41
  path: "/auth/register",
@@ -43,13 +48,17 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
43
48
  429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many attempts" },
44
49
  },
45
50
  }), async (c) => {
46
- const ip = c.req.header("x-forwarded-for") ?? "unknown";
51
+ const ip = clientIp(c.req.header("x-forwarded-for"), c.req.header("x-real-ip")) ?? "unknown";
47
52
  if (await trackAttempt(`register:${ip}`, registerOpts)) {
48
53
  return c.json({ error: "Too many registration attempts. Try again later." }, 429);
49
54
  }
50
55
  const body = c.req.valid("json");
51
56
  const identifier = body[primaryField];
52
- const token = await AuthService.register(identifier, body.password);
57
+ const metadata = {
58
+ ipAddress: ip !== "unknown" ? ip : undefined,
59
+ userAgent: c.req.header("user-agent") ?? undefined,
60
+ };
61
+ const token = await AuthService.register(identifier, body.password, metadata);
53
62
  setCookie(c, COOKIE_TOKEN, token, cookieOptions);
54
63
  return c.json({ token }, 201);
55
64
  });
@@ -71,8 +80,12 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
71
80
  if (await isLimited(limitKey, loginOpts)) {
72
81
  return c.json({ error: "Too many failed login attempts. Try again later." }, 429);
73
82
  }
83
+ const metadata = {
84
+ ipAddress: clientIp(c.req.header("x-forwarded-for"), c.req.header("x-real-ip")),
85
+ userAgent: c.req.header("user-agent") ?? undefined,
86
+ };
74
87
  try {
75
- const result = await AuthService.login(identifier, body.password);
88
+ const result = await AuthService.login(identifier, body.password, metadata);
76
89
  await bustAuthLimit(limitKey); // success — clear failure count
77
90
  setCookie(c, COOKIE_TOKEN, result.token, cookieOptions);
78
91
  return c.json(result, 200);
@@ -203,5 +216,127 @@ export const createAuthRouter = ({ primaryField, emailVerification, rateLimit })
203
216
  return c.json({ message: "Verification email sent" }, 200);
204
217
  });
205
218
  }
219
+ // Password reset routes — only mounted when passwordReset is configured and primaryField is "email"
220
+ if (passwordReset && primaryField === "email") {
221
+ router.openapi(createRoute({
222
+ method: "post",
223
+ path: "/auth/forgot-password",
224
+ tags,
225
+ request: { body: { content: { "application/json": { schema: z.object({ email: z.string().email() }) } } } },
226
+ responses: {
227
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Reset email sent if address is registered" },
228
+ 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Validation error" },
229
+ 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many attempts" },
230
+ },
231
+ }), async (c) => {
232
+ const ip = clientIp(c.req.header("x-forwarded-for"), c.req.header("x-real-ip")) ?? "unknown";
233
+ const { email } = c.req.valid("json");
234
+ // Rate-limit by both IP and email to prevent distributed email-bombing
235
+ const ipLimited = await trackAttempt(`forgot:ip:${ip}`, forgotOpts);
236
+ const emailLimited = await trackAttempt(`forgot:email:${email}`, forgotOpts);
237
+ if (ipLimited || emailLimited) {
238
+ return c.json({ error: "Too many attempts. Try again later." }, 429);
239
+ }
240
+ const adapter = getAuthAdapter();
241
+ const user = await adapter.findByEmail(email);
242
+ // Fire-and-forget: the response does not wait for token creation or email sending,
243
+ // which reduces obvious timing differences between registered and unregistered emails.
244
+ const msg = { message: "If that email is registered, a password reset link has been sent." };
245
+ if (user) {
246
+ void (async () => {
247
+ try {
248
+ const token = await createResetToken(user.id, email);
249
+ await passwordReset.onSend(email, token);
250
+ }
251
+ catch (err) {
252
+ console.error("Failed to send password reset email:", err);
253
+ }
254
+ })();
255
+ }
256
+ return c.json(msg, 200);
257
+ });
258
+ router.openapi(createRoute({
259
+ method: "post",
260
+ path: "/auth/reset-password",
261
+ tags,
262
+ request: { body: { content: { "application/json": { schema: z.object({ token: z.string(), password: z.string().min(8) }) } } } },
263
+ responses: {
264
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Password reset successfully" },
265
+ 400: { content: { "application/json": { schema: ErrorResponse } }, description: "Validation error or invalid/expired token" },
266
+ 429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many attempts" },
267
+ 501: { content: { "application/json": { schema: ErrorResponse } }, description: "Not supported by adapter" },
268
+ },
269
+ }), async (c) => {
270
+ const ip = clientIp(c.req.header("x-forwarded-for"), c.req.header("x-real-ip")) ?? "unknown";
271
+ if (await trackAttempt(`reset:${ip}`, resetOpts)) {
272
+ return c.json({ error: "Too many attempts. Try again later." }, 429);
273
+ }
274
+ const { token, password } = c.req.valid("json");
275
+ // consumeResetToken atomically gets and deletes — prevents concurrent replay
276
+ const entry = await consumeResetToken(token);
277
+ if (!entry)
278
+ return c.json({ error: "Invalid or expired reset token" }, 400);
279
+ const adapter = getAuthAdapter();
280
+ if (!adapter.setPassword) {
281
+ return c.json({ error: "Auth adapter does not support setPassword" }, 501);
282
+ }
283
+ const passwordHash = await Bun.password.hash(password);
284
+ await adapter.setPassword(entry.userId, passwordHash);
285
+ // Revoke all sessions so stolen JWTs can't stay valid after a reset
286
+ const sessions = await getUserSessions(entry.userId);
287
+ await Promise.all(sessions.map((s) => deleteSession(s.sessionId)));
288
+ return c.json({ message: "Password reset successfully" }, 200);
289
+ });
290
+ }
291
+ // ---------------------------------------------------------------------------
292
+ // Session management
293
+ // ---------------------------------------------------------------------------
294
+ const SessionInfoSchema = z.object({
295
+ sessionId: z.string(),
296
+ createdAt: z.number(),
297
+ lastActiveAt: z.number(),
298
+ expiresAt: z.number(),
299
+ ipAddress: z.string().optional(),
300
+ userAgent: z.string().optional(),
301
+ isActive: z.boolean(),
302
+ });
303
+ router.use("/auth/sessions", userAuth);
304
+ router.use("/auth/sessions/*", userAuth);
305
+ router.openapi(createRoute({
306
+ method: "get",
307
+ path: "/auth/sessions",
308
+ tags,
309
+ responses: {
310
+ 200: {
311
+ content: { "application/json": { schema: z.object({ sessions: z.array(SessionInfoSchema) }) } },
312
+ description: "List of sessions for the current user",
313
+ },
314
+ 401: { content: { "application/json": { schema: ErrorResponse } }, description: "Unauthorized" },
315
+ },
316
+ }), async (c) => {
317
+ const userId = c.get("authUserId");
318
+ const sessions = await getUserSessions(userId);
319
+ return c.json({ sessions }, 200);
320
+ });
321
+ router.openapi(createRoute({
322
+ method: "delete",
323
+ path: "/auth/sessions/{sessionId}",
324
+ tags,
325
+ request: { params: z.object({ sessionId: z.string() }) },
326
+ responses: {
327
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Session revoked" },
328
+ 401: { content: { "application/json": { schema: ErrorResponse } }, description: "Unauthorized" },
329
+ 404: { content: { "application/json": { schema: ErrorResponse } }, description: "Session not found" },
330
+ },
331
+ }), async (c) => {
332
+ const userId = c.get("authUserId");
333
+ const { sessionId } = c.req.valid("param");
334
+ const sessions = await getUserSessions(userId);
335
+ const session = sessions.find((s) => s.sessionId === sessionId);
336
+ if (!session)
337
+ return c.json({ error: "Session not found" }, 404);
338
+ await deleteSession(sessionId);
339
+ return c.json({ message: "Session revoked" }, 200);
340
+ });
206
341
  return router;
207
342
  };
@@ -5,10 +5,10 @@ import { getGoogle, getApple, storeOAuthState, consumeOAuthState, generateState,
5
5
  import { getAuthAdapter } from "../lib/authAdapter";
6
6
  import { HttpError } from "../lib/HttpError";
7
7
  import { signToken } from "../lib/jwt";
8
- import { createSession } from "../lib/session";
8
+ import { createSession, getActiveSessionCount, evictOldestSession } from "../lib/session";
9
9
  import { COOKIE_TOKEN } from "../lib/constants";
10
10
  import { userAuth } from "../middleware/userAuth";
11
- import { getDefaultRole } from "../lib/appConfig";
11
+ import { getDefaultRole, getMaxSessions } from "../lib/appConfig";
12
12
  const isProd = process.env.NODE_ENV === "production";
13
13
  const cookieOptions = {
14
14
  httpOnly: true,
@@ -36,8 +36,17 @@ const finishOAuth = async (c, provider, providerId, profile, postLoginRedirect)
36
36
  if (role && adapter.setRoles)
37
37
  await adapter.setRoles(user.id, [role]);
38
38
  }
39
- const token = await signToken(user.id);
40
- await createSession(user.id, token);
39
+ const sessionId = crypto.randomUUID();
40
+ const token = await signToken(user.id, sessionId);
41
+ const xff = c.req.header("x-forwarded-for");
42
+ const metadata = {
43
+ ipAddress: (xff ? xff.split(",")[0]?.trim() : undefined) ?? c.req.header("x-real-ip") ?? undefined,
44
+ userAgent: c.req.header("user-agent") ?? undefined,
45
+ };
46
+ while (await getActiveSessionCount(user.id) >= getMaxSessions()) {
47
+ await evictOldestSession(user.id);
48
+ }
49
+ await createSession(user.id, token, sessionId, metadata);
41
50
  setCookie(c, COOKIE_TOKEN, token, cookieOptions);
42
51
  // Append token to redirect so non-browser clients (mobile deep links) can extract it.
43
52
  // Browser apps can safely ignore the query param.
@@ -1,5 +1,6 @@
1
- export declare const register: (identifier: string, password: string) => Promise<string>;
2
- export declare const login: (identifier: string, password: string) => Promise<{
1
+ import type { SessionMetadata } from "../lib/session";
2
+ export declare const register: (identifier: string, password: string, metadata?: SessionMetadata) => Promise<string>;
3
+ export declare const login: (identifier: string, password: string, metadata?: SessionMetadata) => Promise<{
3
4
  token: string;
4
5
  emailVerified?: boolean;
5
6
  }>;
@@ -1,18 +1,22 @@
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 } from "../lib/session";
5
- import { getDefaultRole, getPrimaryField, getEmailVerificationConfig } from "../lib/appConfig";
4
+ import { createSession, deleteSession, getActiveSessionCount, evictOldestSession } from "../lib/session";
5
+ import { getDefaultRole, getPrimaryField, getEmailVerificationConfig, getMaxSessions } from "../lib/appConfig";
6
6
  import { createVerificationToken } from "../lib/emailVerification";
7
- export const register = async (identifier, password) => {
7
+ export const register = async (identifier, password, metadata) => {
8
8
  const hashed = await Bun.password.hash(password);
9
9
  const adapter = getAuthAdapter();
10
10
  const user = await adapter.create(identifier, hashed);
11
11
  const role = getDefaultRole();
12
12
  if (role)
13
13
  await adapter.setRoles(user.id, [role]);
14
- const token = await signToken(user.id);
15
- await createSession(user.id, token);
14
+ 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);
16
20
  const evConfig = getEmailVerificationConfig();
17
21
  if (evConfig && getPrimaryField() === "email") {
18
22
  try {
@@ -25,30 +29,35 @@ export const register = async (identifier, password) => {
25
29
  }
26
30
  return token;
27
31
  };
28
- export const login = async (identifier, password) => {
32
+ export const login = async (identifier, password, metadata) => {
29
33
  const adapter = getAuthAdapter();
30
34
  const findFn = adapter.findByIdentifier ?? adapter.findByEmail.bind(adapter);
31
35
  const user = await findFn(identifier);
32
36
  if (!user || !(await Bun.password.verify(password, user.passwordHash))) {
33
37
  throw new HttpError(401, "Invalid credentials");
34
38
  }
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
+ }
35
44
  const evConfig = getEmailVerificationConfig();
36
45
  if (evConfig && getPrimaryField() === "email" && adapter.getEmailVerified) {
37
46
  const verified = await adapter.getEmailVerified(user.id);
38
47
  if (evConfig.required && !verified) {
39
48
  throw new HttpError(403, "Email not verified");
40
49
  }
41
- const token = await signToken(user.id);
42
- await createSession(user.id, token);
50
+ await createSession(user.id, token, sessionId, metadata);
43
51
  return { token, emailVerified: verified };
44
52
  }
45
- const token = await signToken(user.id);
46
- await createSession(user.id, token);
53
+ await createSession(user.id, token, sessionId, metadata);
47
54
  return { token };
48
55
  };
49
56
  export const logout = async (token) => {
50
57
  if (token) {
51
58
  const payload = await verifyToken(token);
52
- await deleteSession(payload.sub);
59
+ const sessionId = payload.sid;
60
+ if (sessionId)
61
+ await deleteSession(sessionId);
53
62
  }
54
63
  };
package/dist/ws/index.js CHANGED
@@ -8,9 +8,12 @@ export const createWsUpgradeHandler = (server) => async (req) => {
8
8
  ?.match(new RegExp(`(?:^|;\\s*)${COOKIE_TOKEN}=([^;]+)`))?.[1] ?? null;
9
9
  if (token) {
10
10
  const payload = await verifyToken(token);
11
- const stored = await getSession(payload.sub);
12
- if (stored === token)
13
- userId = payload.sub;
11
+ const sessionId = payload.sid;
12
+ if (sessionId) {
13
+ const stored = await getSession(sessionId);
14
+ if (stored === token)
15
+ userId = payload.sub;
16
+ }
14
17
  }
15
18
  }
16
19
  catch { /* unauthenticated — userId stays null */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastshotlabs/bunshot",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
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,19 +53,37 @@
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.5",
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"
58
88
  }
59
- }
89
+ }