@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.
- package/README.md +97 -7
- package/dist/adapters/memoryAuth.d.ts +13 -3
- package/dist/adapters/memoryAuth.js +116 -8
- package/dist/adapters/sqliteAuth.d.ts +13 -3
- package/dist/adapters/sqliteAuth.js +93 -15
- package/dist/app.d.ts +39 -2
- package/dist/app.js +23 -5
- package/dist/cli.js +0 -0
- package/dist/entrypoints/mongo.d.ts +3 -0
- package/dist/entrypoints/mongo.js +3 -0
- package/dist/entrypoints/queue.d.ts +2 -0
- package/dist/entrypoints/queue.js +1 -0
- package/dist/entrypoints/redis.d.ts +1 -0
- package/dist/entrypoints/redis.js +1 -0
- package/dist/index.d.ts +6 -8
- package/dist/index.js +5 -6
- package/dist/lib/appConfig.d.ts +17 -0
- package/dist/lib/appConfig.js +20 -0
- package/dist/lib/context.d.ts +1 -0
- package/dist/lib/emailVerification.js +11 -10
- package/dist/lib/jwt.d.ts +1 -1
- package/dist/lib/jwt.js +1 -1
- package/dist/lib/mongo.d.ts +9 -4
- package/dist/lib/mongo.js +61 -10
- package/dist/lib/oauth.js +11 -10
- package/dist/lib/queue.d.ts +3 -4
- package/dist/lib/queue.js +18 -3
- package/dist/lib/redis.d.ts +3 -8
- package/dist/lib/redis.js +19 -8
- package/dist/lib/resetPassword.d.ts +12 -0
- package/dist/lib/resetPassword.js +95 -0
- package/dist/lib/session.d.ts +20 -3
- package/dist/lib/session.js +288 -35
- package/dist/middleware/cacheResponse.js +10 -9
- package/dist/middleware/identify.js +21 -7
- package/dist/models/AuthUser.d.ts +14 -106
- package/dist/models/AuthUser.js +31 -14
- package/dist/routes/auth.d.ts +3 -2
- package/dist/routes/auth.js +139 -4
- package/dist/routes/oauth.js +13 -4
- package/dist/services/auth.d.ts +3 -2
- package/dist/services/auth.js +20 -11
- package/dist/ws/index.js +6 -3
- package/package.json +39 -9
package/dist/models/AuthUser.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import mongoose from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
});
|
package/dist/routes/auth.d.ts
CHANGED
|
@@ -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, {}, "/">;
|
package/dist/routes/auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
};
|
package/dist/routes/oauth.js
CHANGED
|
@@ -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
|
|
40
|
-
await
|
|
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.
|
package/dist/services/auth.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const
|
|
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
|
}>;
|
package/dist/services/auth.js
CHANGED
|
@@ -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
|
|
15
|
-
await
|
|
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
|
-
|
|
42
|
-
await createSession(user.id, token);
|
|
50
|
+
await createSession(user.id, token, sessionId, metadata);
|
|
43
51
|
return { token, emailVerified: verified };
|
|
44
52
|
}
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
12
|
-
if (
|
|
13
|
-
|
|
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.
|
|
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
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
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
|
+
}
|