@lastshotlabs/bunshot 0.0.6 → 0.0.8

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 (46) hide show
  1. package/dist/adapters/memoryAuth.js +207 -0
  2. package/dist/adapters/mongoAuth.js +93 -0
  3. package/dist/adapters/sqliteAuth.js +242 -0
  4. package/dist/app.js +175 -0
  5. package/dist/cli.js +1 -1
  6. package/dist/index.js +37 -27
  7. package/dist/lib/HttpError.js +7 -0
  8. package/dist/lib/appConfig.js +17 -0
  9. package/dist/lib/authAdapter.js +7 -0
  10. package/dist/lib/authRateLimit.js +77 -0
  11. package/dist/lib/constants.js +2 -0
  12. package/dist/lib/context.js +8 -0
  13. package/dist/lib/emailVerification.js +77 -0
  14. package/dist/lib/fingerprint.js +36 -0
  15. package/dist/lib/jwt.js +11 -0
  16. package/dist/lib/logger.js +7 -0
  17. package/dist/lib/mongo.js +73 -0
  18. package/dist/lib/oauth.js +82 -0
  19. package/dist/lib/queue.js +4 -0
  20. package/dist/lib/redis.js +50 -0
  21. package/dist/lib/roles.js +22 -0
  22. package/dist/lib/session.js +68 -0
  23. package/dist/lib/validate.js +14 -0
  24. package/dist/lib/ws.js +64 -0
  25. package/dist/middleware/bearerAuth.js +10 -0
  26. package/dist/middleware/botProtection.js +50 -0
  27. package/dist/middleware/cacheResponse.js +158 -0
  28. package/dist/middleware/cors.js +17 -0
  29. package/dist/middleware/errorHandler.js +13 -0
  30. package/dist/middleware/identify.js +33 -0
  31. package/dist/middleware/index.js +1 -0
  32. package/dist/middleware/logger.js +7 -0
  33. package/dist/middleware/rateLimit.js +20 -0
  34. package/dist/middleware/requireRole.js +36 -0
  35. package/dist/middleware/requireVerifiedEmail.js +25 -0
  36. package/dist/middleware/userAuth.js +6 -0
  37. package/dist/models/AuthUser.js +14 -0
  38. package/dist/routes/auth.js +207 -0
  39. package/dist/routes/health.js +22 -0
  40. package/dist/routes/home.js +16 -0
  41. package/dist/routes/oauth.js +150 -0
  42. package/dist/schemas/auth.js +9 -0
  43. package/dist/server.js +53 -0
  44. package/dist/services/auth.js +54 -0
  45. package/dist/ws/index.js +31 -0
  46. package/package.json +2 -2
package/dist/app.js ADDED
@@ -0,0 +1,175 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { cors } from "hono/cors";
3
+ import { logger } from "hono/logger";
4
+ import { secureHeaders } from "hono/secure-headers";
5
+ import { Scalar } from "@scalar/hono-api-reference";
6
+ import { HttpError } from "./lib/HttpError";
7
+ import { rateLimit } from "./middleware/rateLimit";
8
+ import { bearerAuth } from "./middleware/bearerAuth";
9
+ import { identify } from "./middleware/identify";
10
+ import { HEADER_USER_TOKEN } from "./lib/constants";
11
+ import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig } from "./lib/appConfig";
12
+ import { setEmailVerificationStore } from "./lib/emailVerification";
13
+ import { setAuthRateLimitStore } from "./lib/authRateLimit";
14
+ import { setAuthAdapter } from "./lib/authAdapter";
15
+ import { mongoAuthAdapter } from "./adapters/mongoAuth";
16
+ import { memoryAuthAdapter } from "./adapters/memoryAuth";
17
+ import { initOAuthProviders, getConfiguredOAuthProviders, setOAuthStateStore } from "./lib/oauth";
18
+ import { createOAuthRouter } from "./routes/oauth";
19
+ import { connectMongo, connectAuthMongo, connectAppMongo } from "./lib/mongo";
20
+ import { connectRedis } from "./lib/redis";
21
+ import { setSessionStore } from "./lib/session";
22
+ import { setCacheStore } from "./middleware/cacheResponse";
23
+ export const createApp = async (config) => {
24
+ const { routesDir, app: appConfig = {}, auth: authConfig = {}, security: securityConfig = {}, middleware = [], db = {}, } = config;
25
+ const appName = appConfig.name ?? "Bun Core API";
26
+ const openApiVersion = appConfig.version ?? "1.0.0";
27
+ const corsOrigins = securityConfig.cors ?? "*";
28
+ const rlConfig = securityConfig.rateLimit ?? { windowMs: 60_000, max: 100 };
29
+ const botCfg = securityConfig.botProtection ?? {};
30
+ const enableBearerAuth = securityConfig.bearerAuth !== false;
31
+ const extraBypass = typeof securityConfig.bearerAuth === "object" && securityConfig.bearerAuth !== null
32
+ ? (securityConfig.bearerAuth.bypass ?? [])
33
+ : [];
34
+ const enableAuthRoutes = authConfig.enabled !== false;
35
+ const explicitAuthAdapter = authConfig.adapter;
36
+ const oauthProviders = authConfig.oauth?.providers;
37
+ const postOAuthRedirect = authConfig.oauth?.postRedirect ?? "/";
38
+ const roles = authConfig.roles ?? [];
39
+ const defaultRole = authConfig.defaultRole;
40
+ const primaryField = authConfig.primaryField ?? "email";
41
+ const emailVerification = authConfig.emailVerification;
42
+ const authRateLimit = authConfig.rateLimit;
43
+ const { sqlite, mongo = "single", redis: enableRedis = true } = db;
44
+ // Smart fallback: pick the best available store rather than blindly defaulting to "redis"
45
+ const defaultStore = enableRedis
46
+ ? "redis"
47
+ : sqlite
48
+ ? "sqlite"
49
+ : mongo !== false
50
+ ? "mongo"
51
+ : "memory";
52
+ const sessions = db.sessions ?? defaultStore;
53
+ const oauthState = db.oauthState ?? sessions;
54
+ const cache = db.cache ?? defaultStore;
55
+ const authStore = db.auth ?? (mongo !== false ? "mongo" : sessions);
56
+ if (sqlite || sessions === "sqlite" || oauthState === "sqlite" || authStore === "sqlite") {
57
+ const { setSqliteDb } = await import("./adapters/sqliteAuth");
58
+ setSqliteDb(sqlite ?? "./data.db");
59
+ }
60
+ setSessionStore(sessions);
61
+ setOAuthStateStore(oauthState);
62
+ setCacheStore(cache);
63
+ if (mongo === "single")
64
+ await connectMongo();
65
+ else if (mongo === "separate")
66
+ await Promise.all([connectAuthMongo(), connectAppMongo()]);
67
+ if (enableRedis)
68
+ await connectRedis();
69
+ // Resolve auth adapter: explicit prop wins, then db.auth, then mongo default
70
+ let authAdapter;
71
+ if (explicitAuthAdapter) {
72
+ authAdapter = explicitAuthAdapter;
73
+ }
74
+ else if (authStore === "sqlite") {
75
+ const { sqliteAuthAdapter } = await import("./adapters/sqliteAuth");
76
+ authAdapter = sqliteAuthAdapter;
77
+ }
78
+ else if (authStore === "memory") {
79
+ authAdapter = memoryAuthAdapter;
80
+ }
81
+ else {
82
+ authAdapter = mongoAuthAdapter;
83
+ }
84
+ setAuthAdapter(authAdapter);
85
+ setAppRoles(roles);
86
+ setDefaultRole(defaultRole ?? null);
87
+ setPrimaryField(primaryField);
88
+ setEmailVerificationConfig(emailVerification ?? null);
89
+ setEmailVerificationStore(sessions);
90
+ setAuthRateLimitStore(authRateLimit?.store ?? (enableRedis ? "redis" : "memory"));
91
+ if (defaultRole && !authAdapter.setRoles) {
92
+ throw new Error(`createApp: "defaultRole" is set to "${defaultRole}" but the auth adapter does not implement setRoles. Add setRoles to your adapter or remove defaultRole.`);
93
+ }
94
+ if (oauthProviders)
95
+ initOAuthProviders(oauthProviders);
96
+ const configuredOAuth = getConfiguredOAuthProviders();
97
+ // OAuth paths must bypass bearer auth — initiation and link routes are browser redirects,
98
+ // callbacks come from external providers; none can send a bearer token header.
99
+ const oauthBypass = configuredOAuth.flatMap((p) => [
100
+ `/auth/${p}`,
101
+ `/auth/${p}/callback`,
102
+ `/auth/${p}/link`,
103
+ ]);
104
+ const DEFAULT_BYPASS = ["/docs", "/openapi.json", "/sw.js", "/health", "/"];
105
+ const bearerAuthBypass = [...DEFAULT_BYPASS, ...oauthBypass, ...extraBypass];
106
+ const app = new OpenAPIHono();
107
+ app.use(logger());
108
+ app.use(secureHeaders());
109
+ app.use(cors({ origin: corsOrigins, allowHeaders: ["Content-Type", "Authorization", HEADER_USER_TOKEN], exposeHeaders: ["x-cache"], credentials: true }));
110
+ if ((botCfg.blockList?.length ?? 0) > 0) {
111
+ const { botProtection } = await import("./middleware/botProtection");
112
+ app.use(botProtection({ blockList: botCfg.blockList }));
113
+ }
114
+ app.use(rateLimit({ ...rlConfig, fingerprintLimit: botCfg.fingerprintRateLimit ?? false }));
115
+ if (enableBearerAuth) {
116
+ app.use(async (c, next) => {
117
+ const path = c.req.path;
118
+ if (bearerAuthBypass.includes(path)) {
119
+ return next();
120
+ }
121
+ return bearerAuth(c, next);
122
+ });
123
+ }
124
+ app.use(identify);
125
+ for (const mw of middleware)
126
+ app.use(mw);
127
+ setAppName(appName);
128
+ // Core routes (auth, etc.)
129
+ const coreRoutesDir = import.meta.dir + "/routes";
130
+ const coreGlob = new Bun.Glob("*.ts");
131
+ for await (const file of coreGlob.scan({ cwd: coreRoutesDir })) {
132
+ if (file === "auth.ts")
133
+ continue; // mounted separately below via createAuthRouter
134
+ if (file === "oauth.ts")
135
+ continue; // mounted separately below
136
+ const mod = await import(`${coreRoutesDir}/${file}`);
137
+ if (mod.router)
138
+ app.route("/", mod.router);
139
+ }
140
+ if (enableAuthRoutes) {
141
+ const { createAuthRouter } = await import(`${coreRoutesDir}/auth`);
142
+ app.route("/", createAuthRouter({ primaryField, emailVerification, rateLimit: authRateLimit }));
143
+ }
144
+ if (configuredOAuth.length > 0) {
145
+ app.route("/", createOAuthRouter(configuredOAuth, postOAuthRedirect));
146
+ }
147
+ // Service routes — collect all, sort by optional exported `priority`, then mount
148
+ const serviceGlob = new Bun.Glob("**/*.ts");
149
+ const serviceFiles = [];
150
+ for await (const file of serviceGlob.scan({ cwd: routesDir })) {
151
+ serviceFiles.push(file);
152
+ }
153
+ const serviceMods = await Promise.all(serviceFiles.map(async (file) => ({
154
+ file,
155
+ mod: await import(`${routesDir}/${file}`),
156
+ })));
157
+ serviceMods
158
+ .sort((a, b) => (a.mod.priority ?? Infinity) - (b.mod.priority ?? Infinity))
159
+ .forEach(({ mod }) => {
160
+ if (mod.router)
161
+ app.route("/", mod.router);
162
+ });
163
+ app.onError((err, c) => {
164
+ if (err instanceof HttpError) {
165
+ return c.json({ error: err.message }, err.status);
166
+ }
167
+ console.error(err);
168
+ return c.json({ error: "Internal Server Error" }, 500);
169
+ });
170
+ app.notFound((c) => c.json({ error: "Not Found" }, 404));
171
+ app.doc("/openapi.json", { openapi: "3.0.0", info: { title: appName, version: openApiVersion } });
172
+ app.get("/docs", Scalar({ url: "/openapi.json" }));
173
+ app.get("/sw.js", (c) => c.body("", 200, { "Content-Type": "application/javascript" }));
174
+ return app;
175
+ };
package/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  type DbConfig,
24
24
  type SecurityConfig,
25
25
  } from "@lastshotlabs/bunshot";
26
- import { APP_NAME, APP_VERSION, USER_ROLES } from "./lib/constants";
26
+ import { APP_NAME, APP_VERSION, USER_ROLES } from "@lib/constants";
27
27
 
28
28
  export const app: AppMeta = {
29
29
  name: APP_NAME,
package/dist/index.js CHANGED
@@ -1,27 +1,37 @@
1
- // @bun
2
- var UZ=Object.defineProperty;var LZ=(Q)=>Q;function DZ(Q,J){this[Q]=LZ.bind(null,J)}var RQ=(Q,J)=>{for(var Z in J)UZ(Q,Z,{get:J[Z],enumerable:!0,configurable:!0,set:DZ.bind(J,Z)})};var t=(Q,J)=>()=>(Q&&(J=Q(Q=0)),J);var N;var k=t(()=>{N=class N extends Error{status;constructor(Q,J){super(J);this.status=Q}}});var wJ,wZ,H=(...Q)=>{if(wZ)console.log(...Q)};var d=t(()=>{wJ=process.env.LOGGING_VERBOSE,wZ=wJ!==void 0?wJ==="true":!0});var WQ={};RQ(WQ,{getRedisConnectionOptions:()=>y,getRedis:()=>E,disconnectRedis:()=>SJ,connectRedis:()=>BQ});import SZ from"ioredis";var YQ=!1,y=()=>{let Q=YQ?process.env.REDIS_HOST_PROD:process.env.REDIS_HOST_DEV;if(!Q)throw Error(`Missing env var: ${YQ?"REDIS_HOST_PROD":"REDIS_HOST_DEV"}`);let[J,Z]=Q.split(":");if(!J||!Z)throw Error(`Invalid Redis host format \u2014 expected "host:port", got "${Q}"`);let j=YQ?process.env.REDIS_USER_PROD:process.env.REDIS_USER_DEV,$=YQ?process.env.REDIS_PW_PROD:process.env.REDIS_PW_DEV;return{host:J,port:Number(Z),...j&&{username:j},...$&&{password:$}}},A=null,_Z=()=>{let Q=new SZ(y());return Q.on("error",(J)=>H(`[redis] error: ${J.message}`)),Q},BQ=()=>{if(A)return Promise.resolve();return A=_Z(),new Promise((Q,J)=>{A.once("ready",()=>{H(`[redis] connected to ${y().host}:${y().port} as ${y().username||"default user"}`),Q()}),A.once("error",J)})},SJ=async()=>{if(!A)return;await A.quit(),A=null,H("[redis] disconnected")},E=()=>{if(!A)throw Error("Redis not connected \u2014 call connectRedis() first");return A};var R=t(()=>{d()});var iQ={};RQ(iQ,{startSqliteCleanup:()=>yJ,sqliteStoreOAuthState:()=>gQ,sqliteSetCache:()=>cQ,sqliteGetVerificationToken:()=>dQ,sqliteGetSession:()=>fQ,sqliteGetCache:()=>pQ,sqliteDeleteVerificationToken:()=>aQ,sqliteDeleteSession:()=>mQ,sqliteDelCachePattern:()=>uQ,sqliteDelCache:()=>nQ,sqliteCreateVerificationToken:()=>lQ,sqliteCreateSession:()=>yQ,sqliteConsumeOAuthState:()=>hQ,sqliteAuthAdapter:()=>kJ,setSqliteDb:()=>bJ,isSqliteReady:()=>i});import{Database as cZ}from"bun:sqlite";function W(){if(!g)throw Error("SQLite not initialized \u2014 call setSqliteDb(path) before using sqliteAuthAdapter or sessionStore: 'sqlite'");return g}function nZ(Q){Q.run(`CREATE TABLE IF NOT EXISTS users (
3
- id TEXT PRIMARY KEY,
4
- email TEXT UNIQUE,
5
- passwordHash TEXT,
6
- providerIds TEXT NOT NULL DEFAULT '[]',
7
- roles TEXT NOT NULL DEFAULT '[]',
8
- emailVerified INTEGER NOT NULL DEFAULT 0
9
- )`);try{Q.run("ALTER TABLE users ADD COLUMN emailVerified INTEGER NOT NULL DEFAULT 0")}catch{}Q.run(`CREATE TABLE IF NOT EXISTS sessions (
10
- userId TEXT PRIMARY KEY,
11
- token TEXT NOT NULL,
12
- expiresAt INTEGER NOT NULL
13
- )`),Q.run(`CREATE TABLE IF NOT EXISTS oauth_states (
14
- state TEXT PRIMARY KEY,
15
- codeVerifier TEXT,
16
- linkUserId TEXT,
17
- expiresAt INTEGER NOT NULL
18
- )`),Q.run(`CREATE TABLE IF NOT EXISTS cache_entries (
19
- key TEXT PRIMARY KEY,
20
- value TEXT NOT NULL,
21
- expiresAt INTEGER -- NULL = indefinite
22
- )`),Q.run(`CREATE TABLE IF NOT EXISTS email_verifications (
23
- token TEXT PRIMARY KEY,
24
- userId TEXT NOT NULL,
25
- email TEXT NOT NULL,
26
- expiresAt INTEGER NOT NULL
27
- )`)}var g=null,bJ=(Q)=>{g=new cZ(Q,{create:!0}),g.run("PRAGMA journal_mode = WAL"),g.run("PRAGMA foreign_keys = ON"),nZ(g)},kJ,uZ=604800000,yQ=(Q,J)=>{let Z=Date.now()+uZ;W().run("INSERT INTO sessions (userId, token, expiresAt) VALUES (?, ?, ?) ON CONFLICT(userId) DO UPDATE SET token = excluded.token, expiresAt = excluded.expiresAt",[Q,J,Z])},fQ=(Q)=>{return W().query("SELECT token FROM sessions WHERE userId = ? AND expiresAt > ?").get(Q,Date.now())?.token??null},mQ=(Q)=>{W().run("DELETE FROM sessions WHERE userId = ?",[Q])},lZ=300000,gQ=(Q,J,Z)=>{let j=Date.now()+lZ;W().run("INSERT INTO oauth_states (state, codeVerifier, linkUserId, expiresAt) VALUES (?, ?, ?, ?)",[Q,J??null,Z??null,j])},hQ=(Q)=>{let J=W().query("DELETE FROM oauth_states WHERE state = ? AND expiresAt > ? RETURNING codeVerifier, linkUserId").get(Q,Date.now());if(!J)return null;return{codeVerifier:J.codeVerifier??void 0,linkUserId:J.linkUserId??void 0}},i=()=>g!==null,pQ=(Q)=>{return W().query("SELECT value FROM cache_entries WHERE key = ? AND (expiresAt IS NULL OR expiresAt > ?)").get(Q,Date.now())?.value??null},cQ=(Q,J,Z)=>{let j=Z?Date.now()+Z*1000:null;W().run("INSERT INTO cache_entries (key, value, expiresAt) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expiresAt = excluded.expiresAt",[Q,J,j])},nQ=(Q)=>{W().run("DELETE FROM cache_entries WHERE key = ?",[Q])},uQ=(Q)=>{let J=Q.replace(/%/g,"\\%").replace(/_/g,"\\_").replace(/\*/g,"%");W().run("DELETE FROM cache_entries WHERE key LIKE ? ESCAPE '\\'",[J])},lQ=(Q,J,Z,j)=>{let $=Date.now()+j*1000;W().run("INSERT INTO email_verifications (token, userId, email, expiresAt) VALUES (?, ?, ?, ?)",[Q,J,Z,$])},dQ=(Q)=>{return W().query("SELECT userId, email FROM email_verifications WHERE token = ? AND expiresAt > ?").get(Q,Date.now())??null},aQ=(Q)=>{W().run("DELETE FROM email_verifications WHERE token = ?",[Q])},yJ=(Q=3600000)=>{return setInterval(()=>{let J=W(),Z=Date.now();J.run("DELETE FROM sessions WHERE expiresAt <= ?",[Z]),J.run("DELETE FROM oauth_states WHERE expiresAt <= ?",[Z]),J.run("DELETE FROM cache_entries WHERE expiresAt IS NOT NULL AND expiresAt <= ?",[Z]),J.run("DELETE FROM email_verifications WHERE expiresAt <= ?",[Z])},Q)};var f=t(()=>{k();kJ={async findByEmail(Q){return W().query("SELECT id, passwordHash FROM users WHERE email = ?").get(Q)??null},async create(Q,J){let Z=crypto.randomUUID();try{return W().run("INSERT INTO users (id, email, passwordHash) VALUES (?, ?, ?)",[Z,Q,J]),{id:Z}}catch(j){if(j?.code==="SQLITE_CONSTRAINT_UNIQUE")throw new N(409,"Email already registered");throw j}},async setPassword(Q,J){W().run("UPDATE users SET passwordHash = ? WHERE id = ?",[J,Q])},async findOrCreateByProvider(Q,J,Z){let j=`${Q}:${J}`,$=W(),z=$.query("SELECT u.id FROM users u, json_each(u.providerIds) p WHERE p.value = ?").get(j);if(z)return{id:z.id,created:!1};if(Z.email){if($.query("SELECT id FROM users WHERE email = ?").get(Z.email))throw new N(409,"An account with this email already exists. Sign in with your credentials, then link Google from your account settings.")}let X=crypto.randomUUID();return $.run("INSERT INTO users (id, email, providerIds) VALUES (?, ?, ?)",[X,Z.email??null,JSON.stringify([j])]),{id:X,created:!0}},async linkProvider(Q,J,Z){let j=`${J}:${Z}`,$=W(),z=$.query("SELECT id, providerIds FROM users WHERE id = ?").get(Q);if(!z)throw new N(404,"User not found");let X=JSON.parse(z.providerIds);if(!X.includes(j))$.run("UPDATE users SET providerIds = ? WHERE id = ?",[JSON.stringify([...X,j]),Q])},async getRoles(Q){let J=W().query("SELECT roles FROM users WHERE id = ?").get(Q);return J?JSON.parse(J.roles):[]},async setRoles(Q,J){W().run("UPDATE users SET roles = ? WHERE id = ?",[JSON.stringify(J),Q])},async addRole(Q,J){let Z=W(),j=Z.query("SELECT roles FROM users WHERE id = ?").get(Q);if(!j)return;let $=JSON.parse(j.roles);if(!$.includes(J))Z.run("UPDATE users SET roles = ? WHERE id = ?",[JSON.stringify([...$,J]),Q])},async removeRole(Q,J){let Z=W(),j=Z.query("SELECT roles FROM users WHERE id = ?").get(Q);if(!j)return;let $=JSON.parse(j.roles);Z.run("UPDATE users SET roles = ? WHERE id = ?",[JSON.stringify($.filter((z)=>z!==J)),Q])},async getUser(Q){let J=W().query("SELECT email, providerIds, emailVerified FROM users WHERE id = ?").get(Q);if(!J)return null;return{email:J.email??void 0,providerIds:JSON.parse(J.providerIds),emailVerified:J.emailVerified===1}},async unlinkProvider(Q,J){let Z=W(),j=Z.query("SELECT providerIds FROM users WHERE id = ?").get(Q);if(!j)throw new N(404,"User not found");let $=JSON.parse(j.providerIds);Z.run("UPDATE users SET providerIds = ? WHERE id = ?",[JSON.stringify($.filter((z)=>!z.startsWith(`${J}:`))),Q])},async findByIdentifier(Q){return W().query("SELECT id, passwordHash FROM users WHERE email = ?").get(Q)??null},async setEmailVerified(Q,J){W().run("UPDATE users SET emailVerified = ? WHERE id = ?",[J?1:0,Q])},async getEmailVerified(Q){return W().query("SELECT emailVerified FROM users WHERE id = ?").get(Q)?.emailVerified===1}}});var FZ={};RQ(FZ,{botProtection:()=>WZ});function BZ(Q){let J=Q.split(".").map(Number);return(J[0]<<24|J[1]<<16|J[2]<<8|J[3])>>>0}function q0(Q,J){let Z=Q.indexOf("/"),j=Z===-1?Q:Q.slice(0,Z),$=Z===-1?32:parseInt(Q.slice(Z+1),10),z=$===0?0:-1<<32-$>>>0;return(BZ(j)&z)===(BZ(J)&z)}function L0(Q){if(Q.startsWith("::ffff:"))return Q.slice(7);return Q}function D0(Q,J){let Z=L0(Q),j=U0.test(Z);for(let $ of J)if(j){if(q0($,Z))return!0}else if($===Z)return!0;return!1}var U0,WZ=({blockList:Q=[]})=>{if(Q.length===0)return(J,Z)=>Z();return async(J,Z)=>{let $=(J.req.header("x-forwarded-for")??"").split(",")[0]?.trim()??"unknown";if($!=="unknown"&&D0($,Q))return J.json({error:"Forbidden"},403);await Z()}};var YJ=t(()=>{U0=/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/});k();import{OpenAPIHono as R0}from"@hono/zod-openapi";import{cors as C0}from"hono/cors";import{logger as v0}from"hono/logger";import{secureHeaders as w0}from"hono/secure-headers";import{Scalar as S0}from"@scalar/hono-api-reference";var VJ="Core API",xJ=[],KJ=null,RZ="email",PJ=null,qJ=(Q)=>{VJ=Q},O=()=>VJ,UJ=(Q)=>{xJ=Q},CZ=()=>xJ,LJ=(Q)=>{KJ=Q},DJ=()=>KJ,RJ=(Q)=>{RZ=Q};var CJ=(Q)=>{PJ=Q};var vZ=86400,vJ=()=>PJ?.tokenExpiry??vZ;var FQ=new Map,_J={async get(Q){let J=FQ.get(Q);if(!J)return null;if(J.resetAt<=Date.now())return FQ.delete(Q),null;return J},async set(Q,J){FQ.set(Q,J)},async delete(Q){FQ.delete(Q)}},AZ={async get(Q){let{getRedis:J}=await Promise.resolve().then(() => (R(),WQ)),Z=await J().get(`rl:${O()}:${Q}`);if(!Z)return null;let j=JSON.parse(Z);if(j.resetAt<=Date.now())return null;return j},async set(Q,J,Z){let{getRedis:j}=await Promise.resolve().then(() => (R(),WQ));await j().set(`rl:${O()}:${Q}`,JSON.stringify(J),"PX",Z)},async delete(Q){let{getRedis:J}=await Promise.resolve().then(() => (R(),WQ));await J().del(`rl:${O()}:${Q}`)}},a=_J,AJ=(Q)=>{a=Q==="redis"?AZ:_J},IZ=async(Q,J)=>{let Z=await a.get(Q);if(!Z)return!1;return Z.count>=J.max},GQ=async(Q,J)=>{let Z=Date.now(),j=await a.get(Q);if(!j)return await a.set(Q,{count:1,resetAt:Z+J.windowMs},J.windowMs),1>=J.max;let $={count:j.count+1,resetAt:j.resetAt},z=Math.max(1,j.resetAt-Z);return await a.set(Q,$,z),$.count>=J.max},bZ=async(Q)=>{await a.delete(Q)};var kZ=["sec-fetch-site","sec-fetch-mode","sec-fetch-dest","sec-ch-ua","sec-ch-ua-mobile","sec-ch-ua-platform","origin","referer","x-requested-with"],yZ=new TextEncoder;async function CQ(Q){let J=(X)=>Q.headers.get(X)??"",Z=kZ.map((X)=>Q.headers.has(X)?"1":"0").join(""),j=[J("user-agent"),J("accept"),J("accept-language"),J("accept-encoding"),J("connection"),Z].join("|"),$=await crypto.subtle.digest("SHA-256",yZ.encode(j)),z=new Uint8Array($).slice(0,6);return Array.from(z).map((X)=>X.toString(16).padStart(2,"0")).join("")}var vQ=({windowMs:Q,max:J,fingerprintLimit:Z=!1})=>{let j={windowMs:Q,max:J};return async($,z)=>{let F=($.req.header("x-forwarded-for")??"").split(",")[0]?.trim()||"unknown";if(await GQ(`ip:${F}`,j))return $.json({error:"Too Many Requests"},429);if(Z){let B=await CQ($.req.raw);if(await GQ(`fp:${B}`,j))return $.json({error:"Too Many Requests"},429)}await z()}};var fZ=process.env.BEARER_TOKEN_DEV,wQ=async(Q,J)=>{let Z=Q.req.header("Authorization"),j=Z?.startsWith("Bearer ")?Z.slice(7):null;if(!j||j!==fZ)return Q.json({error:"Unauthorized"},401);await J()};import{getCookie as rZ}from"hono/cookie";import{SignJWT as mZ,jwtVerify as gZ}from"jose";var hZ=!1,IJ=new TextEncoder().encode(hZ?process.env.JWT_SECRET_PROD:process.env.JWT_SECRET_DEV),SQ=async(Q)=>new mZ({sub:Q}).setProtectedHeader({alg:"HS256"}).setExpirationTime("7d").sign(IJ),r=async(Q)=>{let{payload:J}=await gZ(Q,IJ);return J};R();d();import _Q from"mongoose";var L=!1;function AQ(Q,J,Z,j){let[$,z]=Z.split("?");return`mongodb+srv://${Q}:${J}@${$.replace(/\/$/,"")}/${j}${z?`?${z}`:""}`}var m=_Q.createConnection(),V=_Q.createConnection(),IQ=async()=>{let Q=L?process.env.MONGO_AUTH_USER_PROD:process.env.MONGO_AUTH_USER_DEV,J=L?process.env.MONGO_AUTH_PW_PROD:process.env.MONGO_AUTH_PW_DEV,Z=L?process.env.MONGO_AUTH_HOST_PROD:process.env.MONGO_AUTH_HOST_DEV,j=L?process.env.MONGO_AUTH_DB_PROD:process.env.MONGO_AUTH_DB_DEV,$=AQ(Q,J,Z,j);await m.openUri($),H(`[mongo] auth connected to ${Z} as ${Q}`)},bQ=async()=>{let Q=L?process.env.MONGO_USER_PROD:process.env.MONGO_USER_DEV,J=L?process.env.MONGO_PW_PROD:process.env.MONGO_PW_DEV,Z=L?process.env.MONGO_HOST_PROD:process.env.MONGO_HOST_DEV,j=L?process.env.MONGO_DB_PROD:process.env.MONGO_DB_DEV,$=AQ(Q,J,Z,j);await V.openUri($),H(`[mongo] app connected to ${Z} as ${Q}`)},kQ=async()=>{let Q=L?process.env.MONGO_USER_PROD:process.env.MONGO_USER_DEV,J=L?process.env.MONGO_PW_PROD:process.env.MONGO_PW_DEV,Z=L?process.env.MONGO_HOST_PROD:process.env.MONGO_HOST_DEV,j=L?process.env.MONGO_DB_PROD:process.env.MONGO_DB_DEV,$=AQ(Q,J,Z,j);await Promise.all([m.openUri($),V.openUri($)]),H(`[mongo] connected to ${Z} as ${Q}`)},pZ=async()=>{await Promise.all([m.readyState!==0?m.close():Promise.resolve(),V.readyState!==0?V.close():Promise.resolve()]),H("[mongo] disconnected")};f();import{Schema as oZ}from"mongoose";k();var K=new Map,h=new Map,NQ=new Map,e=new Map,p=new Map,QQ=new Map,dZ=()=>{K.clear(),h.clear(),NQ.clear(),e.clear(),p.clear(),QQ.clear()},oQ={async findByEmail(Q){let J=h.get(Q.toLowerCase());if(!J)return null;let Z=K.get(J);if(!Z||!Z.passwordHash)return null;return{id:Z.id,passwordHash:Z.passwordHash}},async create(Q,J){let Z=Q.toLowerCase();if(h.has(Z))throw new N(409,"Email already registered");let j=crypto.randomUUID(),$={id:j,email:Z,passwordHash:J,providerIds:[],roles:[],emailVerified:!1};return K.set(j,$),h.set(Z,j),{id:j}},async setPassword(Q,J){let Z=K.get(Q);if(!Z)return;Z.passwordHash=J},async findOrCreateByProvider(Q,J,Z){let j=`${Q}:${J}`;for(let F of K.values())if(F.providerIds.includes(j))return{id:F.id,created:!1};if(Z.email){if(h.get(Z.email.toLowerCase()))throw new N(409,"An account with this email already exists. Sign in with your credentials, then link Google from your account settings.")}let $=crypto.randomUUID(),z=Z.email?Z.email.toLowerCase():null,X={id:$,email:z,passwordHash:null,providerIds:[j],roles:[],emailVerified:!1};if(K.set($,X),z)h.set(z,$);return{id:$,created:!0}},async linkProvider(Q,J,Z){let j=K.get(Q);if(!j)throw new N(404,"User not found");let $=`${J}:${Z}`;if(!j.providerIds.includes($))j.providerIds.push($)},async getRoles(Q){return K.get(Q)?.roles??[]},async setRoles(Q,J){let Z=K.get(Q);if(!Z)return;Z.roles=[...J]},async addRole(Q,J){let Z=K.get(Q);if(!Z)return;if(!Z.roles.includes(J))Z.roles.push(J)},async removeRole(Q,J){let Z=K.get(Q);if(!Z)return;Z.roles=Z.roles.filter((j)=>j!==J)},async getUser(Q){let J=K.get(Q);if(!J)return null;return{email:J.email??void 0,providerIds:[...J.providerIds],emailVerified:J.emailVerified}},async unlinkProvider(Q,J){let Z=K.get(Q);if(!Z)throw new N(404,"User not found");Z.providerIds=Z.providerIds.filter((j)=>!j.startsWith(`${J}:`))},async findByIdentifier(Q){let J=h.get(Q.toLowerCase());if(!J)return null;let Z=K.get(J);if(!Z||!Z.passwordHash)return null;return{id:Z.id,passwordHash:Z.passwordHash}},async setEmailVerified(Q,J){let Z=K.get(Q);if(Z)Z.emailVerified=J},async getEmailVerified(Q){return K.get(Q)?.emailVerified??!1}},aZ=604800000,fJ=(Q,J)=>{NQ.set(Q,{token:J,expiresAt:Date.now()+aZ})},mJ=(Q)=>{let J=NQ.get(Q);if(!J||J.expiresAt<=Date.now())return null;return J.token},gJ=(Q)=>{NQ.delete(Q)},iZ=300000,hJ=(Q,J,Z)=>{e.set(Q,{codeVerifier:J,linkUserId:Z,expiresAt:Date.now()+iZ})},pJ=(Q)=>{let J=e.get(Q);if(!J||J.expiresAt<=Date.now())return e.delete(Q),null;return e.delete(Q),{codeVerifier:J.codeVerifier,linkUserId:J.linkUserId}},cJ=(Q)=>{let J=p.get(Q);if(!J)return null;if(J.expiresAt!==void 0&&J.expiresAt<=Date.now())return p.delete(Q),null;return J.value},nJ=(Q,J,Z)=>{let j=Z?Date.now()+Z*1000:void 0;p.set(Q,{value:J,expiresAt:j})},uJ=(Q)=>{p.delete(Q)},lJ=(Q)=>{let J=new RegExp("^"+Q.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")+"$");for(let Z of p.keys())if(J.test(Z))p.delete(Z)},dJ=(Q,J,Z,j)=>{QQ.set(Q,{userId:J,email:Z,expiresAt:Date.now()+j*1000})},aJ=(Q)=>{let J=QQ.get(Q);if(!J||J.expiresAt<=Date.now())return QQ.delete(Q),null;return{userId:J.userId,email:J.email}},iJ=(Q)=>{QQ.delete(Q)};var sZ=new oZ({userId:{type:String,required:!0,unique:!0},token:{type:String,required:!0},expiresAt:{type:Date,required:!0,index:{expireAfterSeconds:0}}},{collection:"sessions"});function sQ(){return V.models.Session??V.model("Session",sZ)}var v="redis",tQ=(Q)=>{v=Q},oJ=604800,rQ=async(Q,J)=>{if(v==="memory"){fJ(Q,J);return}if(v==="sqlite"){yQ(Q,J);return}if(v==="mongo"){let Z=new Date(Date.now()+oJ*1000);await sQ().updateOne({userId:Q},{$set:{token:J,expiresAt:Z}},{upsert:!0});return}await E().set(`session:${O()}:${Q}`,J,"EX",oJ)},JQ=async(Q)=>{if(v==="memory")return mJ(Q);if(v==="sqlite")return fQ(Q);if(v==="mongo"){let J=await sQ().findOne({userId:Q,expiresAt:{$gt:new Date}},"token").lean();return J?J.token:null}return E().get(`session:${O()}:${Q}`)},tZ=async(Q)=>{if(v==="memory"){gJ(Q);return}if(v==="sqlite"){mQ(Q);return}if(v==="mongo"){await sQ().deleteOne({userId:Q});return}await E().del(`session:${O()}:${Q}`)};var c="token",ZQ="x-user-token";d();var eQ=async(Q,J)=>{Q.set("authUserId",null),Q.set("roles",null);let Z=rZ(Q,c)??Q.req.header(ZQ)??null;if(H(`[identify] token=${Z?"present":"absent"}`),Z)try{let j=await r(Z),$=await JQ(j.sub);if(H(`[identify] token for authUserId=${j.sub} verified, checking session...`),$===Z)Q.set("authUserId",j.sub),H(`[identify] authUserId=${j.sub}`);else H("[identify] token/session mismatch \u2014 unauthenticated")}catch{H("[identify] invalid token \u2014 unauthenticated")}else H("[identify] no token \u2014 unauthenticated");await J()};R();f();import{Schema as eZ}from"mongoose";var Q0=new eZ({token:{type:String,required:!0,unique:!0},userId:{type:String,required:!0},email:{type:String,required:!0},expiresAt:{type:Date,required:!0,index:{expireAfterSeconds:0}}},{collection:"email_verifications"});function QJ(){return V.models.EmailVerification??V.model("EmailVerification",Q0)}var w="redis",sJ=(Q)=>{w=Q},J0=async(Q,J)=>{let Z=crypto.randomUUID(),j=vJ();if(w==="memory")return dJ(Z,Q,J,j),Z;if(w==="sqlite")return lQ(Z,Q,J,j),Z;if(w==="mongo")return await QJ().create({token:Z,userId:Q,email:J,expiresAt:new Date(Date.now()+j*1000)}),Z;return await E().set(`verify:${O()}:${Z}`,JSON.stringify({userId:Q,email:J}),"EX",j),Z},Z0=async(Q)=>{if(w==="memory")return aJ(Q);if(w==="sqlite")return dQ(Q);if(w==="mongo"){let Z=await QJ().findOne({token:Q,expiresAt:{$gt:new Date}}).lean();if(!Z)return null;return{userId:Z.userId,email:Z.email}}let J=await E().get(`verify:${O()}:${Q}`);if(!J)return null;return JSON.parse(J)},j0=async(Q)=>{if(w==="memory"){iJ(Q);return}if(w==="sqlite"){aQ(Q);return}if(w==="mongo"){await QJ().deleteOne({token:Q});return}await E().del(`verify:${O()}:${Q}`)};var JJ=null,tJ=(Q)=>{JJ=Q},U=()=>{if(!JJ)throw Error("No auth adapter set \u2014 pass authAdapter to createApp/createServer, or call setAuthAdapter()");return JJ};import $0 from"mongoose";var rJ=new $0.Schema({email:{type:String,unique:!0,sparse:!0,lowercase:!0},password:{type:String},providerIds:[{type:String}],roles:[{type:String}],emailVerified:{type:Boolean,default:!1}},{timestamps:!0});rJ.index({providerIds:1});var M=m.model("AuthUser",rJ);k();var ZJ={async findByEmail(Q){let J=await M.findOne({email:Q});if(!J)return null;return{id:String(J._id),passwordHash:J.password}},async create(Q,J){try{let Z=await M.create({email:Q,password:J});return{id:String(Z._id)}}catch(Z){if(Z?.code===11000)throw new N(409,"Email already registered");throw Z}},async setPassword(Q,J){await M.findByIdAndUpdate(Q,{password:J})},async findOrCreateByProvider(Q,J,Z){let j=`${Q}:${J}`,$=await M.findOne({providerIds:j});if($)return{id:String($._id),created:!1};if(Z.email){if(await M.findOne({email:Z.email}))throw new N(409,"An account with this email already exists. Sign in with your credentials, then link Google from your account settings.")}return $=await M.create({email:Z.email,providerIds:[j]}),{id:String($._id),created:!0}},async linkProvider(Q,J,Z){let j=`${J}:${Z}`,$=await M.findById(Q);if(!$)throw new N(404,"User not found");if(!$.providerIds.includes(j))$.providerIds=[...$.providerIds,j],await $.save()},async getRoles(Q){return(await M.findById(Q,"roles").lean())?.roles??[]},async setRoles(Q,J){await M.findByIdAndUpdate(Q,{roles:J})},async addRole(Q,J){await M.findByIdAndUpdate(Q,{$addToSet:{roles:J}})},async removeRole(Q,J){await M.findByIdAndUpdate(Q,{$pull:{roles:J}})},async getUser(Q){let J=await M.findById(Q,"email providerIds emailVerified").lean();if(!J)return null;return{email:J.email,providerIds:J.providerIds,emailVerified:J.emailVerified??!1}},async unlinkProvider(Q,J){let Z=await M.findById(Q);if(!Z)throw new N(404,"User not found");Z.providerIds=Z.providerIds.filter((j)=>!j.startsWith(`${J}:`)),await Z.save()},async findByIdentifier(Q){let J=await M.findOne({email:Q});if(!J)return null;return{id:String(J._id),passwordHash:J.password}},async setEmailVerified(Q,J){await M.findByIdAndUpdate(Q,{emailVerified:J})},async getEmailVerified(Q){return(await M.findById(Q,"emailVerified").lean())?.emailVerified??!1}};R();import{Google as z0,Apple as X0,generateState as jQ,generateCodeVerifier as jJ}from"arctic";f();import{Schema as Y0}from"mongoose";var u={},QZ=(Q)=>{if(Q.google){let{clientId:J,clientSecret:Z,redirectUri:j}=Q.google;u.google=new z0(J,Z,j)}if(Q.apple){let{clientId:J,teamId:Z,keyId:j,privateKey:$,redirectUri:z}=Q.apple;u.apple=new X0(J,Z,j,new TextEncoder().encode($),z)}},EQ=()=>{if(!u.google)throw Error("Google OAuth not configured");return u.google},HQ=()=>{if(!u.apple)throw Error("Apple OAuth not configured");return u.apple},JZ=()=>Object.entries(u).filter(([,Q])=>Q!=null).map(([Q])=>Q),B0=new Y0({state:{type:String,required:!0,unique:!0},codeVerifier:{type:String},linkUserId:{type:String},expiresAt:{type:Date,required:!0,index:{expireAfterSeconds:0}}},{collection:"oauth_states"});function ZZ(){return V.models.OAuthState??V.model("OAuthState",B0)}var n="redis",jZ=(Q)=>{n=Q},eJ=300,$Q=async(Q,J,Z)=>{if(n==="memory"){hJ(Q,J,Z);return}if(n==="sqlite"){gQ(Q,J,Z);return}if(n==="mongo"){let j=new Date(Date.now()+eJ*1000);await ZZ().create({state:Q,codeVerifier:J,linkUserId:Z,expiresAt:j});return}await E().set(`oauth:${O()}:state:${Q}`,JSON.stringify({codeVerifier:J,linkUserId:Z}),"EX",eJ)},$J=async(Q)=>{if(n==="memory")return pJ(Q);if(n==="sqlite")return hQ(Q);if(n==="mongo"){let j=await ZZ().findOneAndDelete({state:Q,expiresAt:{$gt:new Date}}).lean();return j?{codeVerifier:j.codeVerifier,linkUserId:j.linkUserId}:null}let J=`oauth:${O()}:state:${Q}`,Z=await E().get(J);if(!Z)return null;return await E().del(J),JSON.parse(Z)};import{OpenAPIHono as W0}from"@hono/zod-openapi";var F0=(Q,J)=>{if(!Q.success){let Z=Q.error.issues.map((j)=>j.message).join(", ");return J.json({error:Z},400)}},zJ=()=>new W0({defaultHook:F0});import{setCookie as G0}from"hono/cookie";import{decodeIdToken as N0}from"arctic";k();var zQ=async(Q,J)=>{if(!Q.get("authUserId"))return Q.json({error:"Unauthorized"},401);await J()};var E0=!1,H0={httpOnly:!0,secure:E0,sameSite:"Lax",path:"/",maxAge:604800},$Z=async(Q,J,Z,j,$)=>{let z=U();if(!z.findOrCreateByProvider)return Q.json({error:"Auth adapter does not support social login"},500);let X;try{X=await z.findOrCreateByProvider(J,Z,j)}catch(B){let G=B instanceof N?B.message:"Authentication failed",P=$.includes("?")?"&":"?";return Q.redirect(`${$}${P}error=${encodeURIComponent(G)}`)}if(X.created){let B=DJ();if(B&&z.setRoles)await z.setRoles(X.id,[B])}let F=await SQ(X.id);await rQ(X.id,F),G0(Q,c,F,H0);try{let B=new URL($);if(B.searchParams.set("token",F),j.email)B.searchParams.set("user",j.email);return Q.redirect(B.toString())}catch{let B=$.includes("?")?"&":"?",G=j.email?`&user=${encodeURIComponent(j.email)}`:"";return Q.redirect(`${$}${B}token=${F}${G}`)}},zZ=(Q,J)=>{let Z=zJ();if(Q.includes("google"))Z.get("/auth/google",async(j)=>{let $=jQ(),z=jJ();await $Q($,z);let X=EQ().createAuthorizationURL($,z,["openid","profile","email"]);return j.redirect(X.toString())}),Z.get("/auth/google/callback",async(j)=>{let{code:$,state:z}=j.req.query();if(!$||!z)return j.json({error:"Invalid callback"},400);let X=await $J(z);if(!X?.codeVerifier)return j.json({error:"Invalid or expired state"},400);let F=await EQ().validateAuthorizationCode($,X.codeVerifier),B=await fetch("https://openidconnect.googleapis.com/v1/userinfo",{headers:{Authorization:`Bearer ${F.accessToken()}`}}).then((G)=>G.json());if(X.linkUserId){let G=U();if(!G.linkProvider)return j.json({error:"Auth adapter does not support linkProvider"},500);await G.linkProvider(X.linkUserId,"google",B.sub);let P=J.includes("?")?"&":"?";return j.redirect(`${J}${P}linked=google`)}return $Z(j,"google",B.sub,{email:B.email,name:B.name,avatarUrl:B.picture},J)}),Z.get("/auth/google/link",zQ,async(j)=>{let $=jQ(),z=jJ();await $Q($,z,j.get("authUserId"));let X=EQ().createAuthorizationURL($,z,["openid","profile","email"]);return j.redirect(X.toString())}),Z.delete("/auth/google/link",zQ,async(j)=>{let $=U();if(!$.unlinkProvider)return j.json({error:"Auth adapter does not support unlinkProvider"},500);return await $.unlinkProvider(j.get("authUserId"),"google"),j.body(null,204)});if(Q.includes("apple"))Z.get("/auth/apple",async(j)=>{let $=jQ();await $Q($);let z=HQ().createAuthorizationURL($,["name","email"]);return j.redirect(z.toString())}),Z.post("/auth/apple/callback",async(j)=>{let $=await j.req.formData(),z=$.get("code"),X=$.get("state");if(!z||!X)return j.json({error:"Invalid callback"},400);let F=await $J(X);if(!F)return j.json({error:"Invalid or expired state"},400);let B=await HQ().validateAuthorizationCode(z),G=N0(B.idToken());if(F.linkUserId){let b=U();if(!b.linkProvider)return j.json({error:"Auth adapter does not support linkProvider"},500);await b.linkProvider(F.linkUserId,"apple",G.sub);let D=J.includes("?")?"&":"?";return j.redirect(`${J}${D}linked=apple`)}let P=$.get("user"),q=P?JSON.parse(P):{},S=q.name?`${q.name.firstName??""} ${q.name.lastName??""}`.trim()||void 0:void 0;return $Z(j,"apple",G.sub,{email:G.email,name:S},J)}),Z.get("/auth/apple/link",zQ,async(j)=>{let $=jQ();await $Q($,void 0,j.get("authUserId"));let z=HQ().createAuthorizationURL($,["name","email"]);return j.redirect(z.toString())});return Z};R();R();f();import{Schema as M0}from"mongoose";var O0=new M0({key:{type:String,required:!0,unique:!0},value:{type:String,required:!0},expiresAt:{type:Date,index:{expireAfterSeconds:0}}},{collection:"cache_entries"});function TQ(){return V.models.CacheEntry??V.model("CacheEntry",O0)}function VQ(){return V.readyState===1}function XZ(){try{return E(),!0}catch{return!1}}var YZ="redis",XJ=(Q)=>{YZ=Q};async function T0(Q,J){if(Q==="memory")return cJ(J);if(Q==="sqlite"){if(!i())throw Error('cacheResponse: store is "sqlite" but SQLite is not initialized. Call setSqliteDb(path) or pass sqliteDb to createServer.');return pQ(J)}if(Q==="mongo"){if(!VQ())throw Error('cacheResponse: store is "mongo" but appConnection is not connected. Ensure connectMongo() or connectAppMongo() is called before handling requests.');let Z=await TQ().findOne({key:J},"value").lean();return Z?Z.value:null}return E().get(J)}async function V0(Q,J,Z,j){if(Q==="memory"){nJ(J,Z,j);return}if(Q==="sqlite"){if(!i())throw Error('cacheResponse: store is "sqlite" but SQLite is not initialized. Call setSqliteDb(path) or pass sqliteDb to createServer.');cQ(J,Z,j);return}if(Q==="mongo"){if(!VQ())throw Error('cacheResponse: store is "mongo" but appConnection is not connected. Ensure connectMongo() or connectAppMongo() is called before handling requests.');let $=j?new Date(Date.now()+j*1000):void 0;await TQ().updateOne({key:J},{$set:{value:Z,...$?{expiresAt:$}:{}}},{upsert:!0});return}if(j)await E().setex(J,j,Z);else await E().set(J,Z)}async function MQ(Q,J){if(Q==="memory"){uJ(J);return}if(Q==="sqlite"){if(!i())return;nQ(J);return}if(Q==="mongo"){if(!VQ())return;await TQ().deleteOne({key:J});return}if(!XZ())return;await E().del(J)}async function OQ(Q,J){if(Q==="memory"){lJ(J);return}if(Q==="sqlite"){if(!i())return;uQ(J);return}if(Q==="mongo"){if(!VQ())return;let $=new RegExp("^"+J.replace(/\*/g,".*")+"$");await TQ().deleteMany({key:$});return}if(!XZ())return;let Z=E(),j="0";do{let[$,z]=await Z.scan(j,"MATCH",J,"COUNT",100);if(j=$,z.length>0)await Z.del(...z)}while(j!=="0")}var x0=async(Q)=>{let J=`cache:${O()}:${Q}`;await Promise.all([MQ("redis",J),MQ("mongo",J),MQ("sqlite",J),MQ("memory",J)])},K0=async(Q)=>{let J=`cache:${O()}:${Q}`;await Promise.all([OQ("redis",J),OQ("mongo",J),OQ("sqlite",J),OQ("memory",J)])},P0=({ttl:Q,key:J,store:Z=YZ})=>{return async(j,$)=>{let z=O(),X=typeof J==="function"?J(j):J,F=`cache:${z}:${X}`,B=await T0(Z,F);if(B){let{status:P,headers:q,body:S}=JSON.parse(B);return new Response(S,{status:P,headers:{...q,"x-cache":"HIT"}})}await $();let G=j.res;if(G.status>=200&&G.status<300){let P=await G.text(),q={};G.headers.forEach((S,b)=>{q[b]=S}),await V0(Z,F,JSON.stringify({status:G.status,headers:q,body:P}),Q),j.res=new Response(P,{status:G.status,headers:{...q,"x-cache":"MISS"}})}}};var BJ=async(Q)=>{let{routesDir:J,app:Z={},auth:j={},security:$={},middleware:z=[],db:X={}}=Q,F=Z.name??"Bun Core API",B=Z.version??"1.0.0",G=$.cors??"*",P=$.rateLimit??{windowMs:60000,max:100},q=$.botProtection??{},S=$.bearerAuth!==!1,b=typeof $.bearerAuth==="object"&&$.bearerAuth!==null?$.bearerAuth.bypass??[]:[],D=j.enabled!==!1,x=j.adapter,_=j.oauth?.providers,xQ=j.oauth?.postRedirect??"/",OZ=j.roles??[],KQ=j.defaultRole,NJ=j.primaryField??"email",EJ=j.emailVerification,HJ=j.rateLimit,{sqlite:PQ,mongo:XQ="single",redis:qQ=!0}=X,MJ=qQ?"redis":PQ?"sqlite":XQ!==!1?"mongo":"memory",s=X.sessions??MJ,OJ=X.oauthState??s,TZ=X.cache??MJ,UQ=X.auth??(XQ!==!1?"mongo":s);if(PQ||s==="sqlite"||OJ==="sqlite"||UQ==="sqlite"){let{setSqliteDb:Y}=await Promise.resolve().then(() => (f(),iQ));Y(PQ??"./data.db")}if(tQ(s),jZ(OJ),XJ(TZ),XQ==="single")await kQ();else if(XQ==="separate")await Promise.all([IQ(),bQ()]);if(qQ)await BQ();let l;if(x)l=x;else if(UQ==="sqlite"){let{sqliteAuthAdapter:Y}=await Promise.resolve().then(() => (f(),iQ));l=Y}else if(UQ==="memory")l=oQ;else l=ZJ;if(tJ(l),UJ(OZ),LJ(KQ??null),RJ(NJ),CJ(EJ??null),sJ(s),AJ(HJ?.store??(qQ?"redis":"memory")),KQ&&!l.setRoles)throw Error(`createApp: "defaultRole" is set to "${KQ}" but the auth adapter does not implement setRoles. Add setRoles to your adapter or remove defaultRole.`);if(_)QZ(_);let LQ=JZ(),VZ=LQ.flatMap((Y)=>[`/auth/${Y}`,`/auth/${Y}/callback`,`/auth/${Y}/link`]),xZ=[...["/docs","/openapi.json","/sw.js","/health","/"],...VZ,...b],T=new R0;if(T.use(v0()),T.use(w0()),T.use(C0({origin:G,allowHeaders:["Content-Type","Authorization",ZQ],exposeHeaders:["x-cache"],credentials:!0})),(q.blockList?.length??0)>0){let{botProtection:Y}=await Promise.resolve().then(() => (YJ(),FZ));T.use(Y({blockList:q.blockList}))}if(T.use(vQ({...P,fingerprintLimit:q.fingerprintRateLimit??!1})),S)T.use(async(Y,C)=>{let qZ=Y.req.path;if(xZ.includes(qZ))return C();return wQ(Y,C)});T.use(eQ);for(let Y of z)T.use(Y);qJ(F);let DQ=import.meta.dir+"/routes",KZ=new Bun.Glob("*.ts");for await(let Y of KZ.scan({cwd:DQ})){if(Y==="auth.ts")continue;if(Y==="oauth.ts")continue;let C=await import(`${DQ}/${Y}`);if(C.router)T.route("/",C.router)}if(D){let{createAuthRouter:Y}=await import(`${DQ}/auth`);T.route("/",Y({primaryField:NJ,emailVerification:EJ,rateLimit:HJ}))}if(LQ.length>0)T.route("/",zZ(LQ,xQ));let PZ=new Bun.Glob("**/*.ts"),TJ=[];for await(let Y of PZ.scan({cwd:J}))TJ.push(Y);return(await Promise.all(TJ.map(async(Y)=>({file:Y,mod:await import(`${J}/${Y}`)})))).sort((Y,C)=>(Y.mod.priority??1/0)-(C.mod.priority??1/0)).forEach(({mod:Y})=>{if(Y.router)T.route("/",Y.router)}),T.onError((Y,C)=>{if(Y instanceof N)return C.json({error:Y.message},Y.status);return console.error(Y),C.json({error:"Internal Server Error"},500)}),T.notFound((Y)=>Y.json({error:"Not Found"},404)),T.doc("/openapi.json",{openapi:"3.0.0",info:{title:F,version:B}}),T.get("/docs",S0({url:"/openapi.json"})),T.get("/sw.js",(Y)=>Y.body("",200,{"Content-Type":"application/javascript"})),T};var WJ=(Q)=>async(J)=>{let Z=null;try{let $=J.headers.get("cookie")?.match(new RegExp(`(?:^|;\\s*)${c}=([^;]+)`))?.[1]??null;if($){let z=await r($);if(await JQ(z.sub)===$)Z=z.sub}}catch{}return Q.upgrade(J,{data:{id:crypto.randomUUID(),userId:Z,rooms:new Set}})?void 0:Response.json({error:"Upgrade failed"},{status:400})},o={open(Q){console.log(`[ws] connected: ${Q.data.id}`),Q.send(JSON.stringify({event:"connected",id:Q.data.id}))},message(Q,J){Q.send(J)},close(Q){console.log(`[ws] disconnected: ${Q.data.id}`)}};var GZ=null,NZ=(Q)=>{GZ=Q},_0=(Q,J)=>{GZ?.publish(Q,JSON.stringify(J))},I=new Map,A0=()=>[...I.keys()],I0=(Q)=>[...I.get(Q)??[]],FJ=async(Q,J,Z)=>{try{let j=JSON.parse(typeof J==="string"?J:Buffer.from(J).toString());if(j.action==="subscribe"&&typeof j.room==="string"){if(Z&&!await Z(Q,j.room))Q.send(JSON.stringify({event:"subscribe_denied",room:j.room}));else EZ(Q,j.room),Q.send(JSON.stringify({event:"subscribed",room:j.room}));return!0}if(j.action==="unsubscribe"&&typeof j.room==="string")return HZ(Q,j.room),Q.send(JSON.stringify({event:"unsubscribed",room:j.room})),!0}catch{}return!1},EZ=(Q,J)=>{if(Q.subscribe(J),Q.data.rooms.add(J),!I.has(J))I.set(J,new Set);I.get(J).add(Q.data.id)},HZ=(Q,J)=>{Q.unsubscribe(J),Q.data.rooms.delete(J);let Z=I.get(J);if(Z){if(Z.delete(Q.data.id),Z.size===0)I.delete(J)}},b0=(Q)=>[...Q.data.rooms],MZ=(Q,J)=>{for(let Z of J){let j=I.get(Z);if(j){if(j.delete(Q),j.size===0)I.delete(Z)}}};d();var k0=async(Q)=>{let J=await BJ(Q),Z=Number(process.env.PORT??Q.port??3000),{workersDir:j,enableWorkers:$=!0,ws:z={}}=Q,{handler:X,upgradeHandler:F,onRoomSubscribe:B}=z,G=o.open,P=o.message,q=o.close,S=o.drain,b={open:X?.open??G,async message(x,_){if(!await FJ(x,_,B))(X?.message??P)(x,_)},close(x,_,xQ){MZ(x.data.id,x.data.rooms),x.data.rooms.clear(),(X?.close??q)(x,_,xQ)},drain:X?.drain??S},D;if(D=Bun.serve({port:Z,routes:{"/ws":(x)=>F?F(x,D):WJ(D)(x)},fetch:J.fetch,websocket:b,error(x){return console.error(x),Response.json({error:"Internal Server Error"},{status:500})}}),NZ(D),$&&j){let x=new Bun.Glob("**/*.ts");for await(let _ of x.scan({cwd:j}))await import(`${j}/${_}`)}return H(`[server] running at http://localhost:${D.port}`),H(`[server] API docs at http://localhost:${D.port}/docs`),D};k();d();R();R();import{Queue as y0,Worker as f0}from"bullmq";var m0=(Q,J)=>new y0(Q,{connection:y(),...J}),g0=(Q,J,Z)=>new f0(Q,J,{connection:y(),...Z});k();import{z as h0}from"zod";var p0=async(Q,J)=>{try{let Z=await J.json();return Q.parse(Z)}catch(Z){if(Z instanceof h0.ZodError)throw new N(400,Z.issues.map((j)=>j.message).join(", "));throw Z}};YJ();var c0=(...Q)=>async(J,Z)=>{let j=J.get("authUserId");if(!j)return J.json({error:"Unauthorized"},401);let $=J.get("roles");if($===null){let X=U();if(!X.getRoles)throw Error("requireRole used but auth adapter does not implement getRoles");$=await X.getRoles(j),J.set("roles",$)}if(!Q.some((X)=>$.includes(X)))return J.json({error:"Forbidden"},403);await Z()};var n0=async(Q,J)=>{let Z=Q.get("authUserId");if(!Z)return Q.json({error:"Unauthorized"},401);let j=U();if(!j.getEmailVerified)throw Error("requireVerifiedEmail used but auth adapter does not implement getEmailVerified");if(!await j.getEmailVerified(Z))return Q.json({error:"Email not verified"},403);await J()};f();var GJ=(Q)=>{throw Error(`Auth adapter does not implement ${Q} \u2014 add it to your adapter to manage roles`)},u0=async(Q,J)=>{let Z=U();if(!Z.setRoles)GJ("setRoles");await Z.setRoles(Q,J)},l0=async(Q,J)=>{let Z=U();if(!Z.addRole)GJ("addRole");await Z.addRole(Q,J)},d0=async(Q,J)=>{let Z=U();if(!Z.removeRole)GJ("removeRole");await Z.removeRole(Q,J)};export{o as websocket,r as verifyToken,p0 as validate,zQ as userAuth,HZ as unsubscribe,GQ as trackAttempt,EZ as subscribe,yJ as startSqliteCleanup,kJ as sqliteAuthAdapter,SQ as signToken,u0 as setUserRoles,bJ as setSqliteDb,tQ as setSessionStore,XJ as setCacheStore,n0 as requireVerifiedEmail,c0 as requireRole,d0 as removeUserRole,vQ as rateLimit,_0 as publish,_Q as mongoose,ZJ as mongoAuthAdapter,oQ as memoryAuthAdapter,H as log,IZ as isLimited,eQ as identify,FJ as handleRoomActions,Z0 as getVerificationToken,b0 as getSubscriptions,JQ as getSession,A0 as getRooms,I0 as getRoomSubscribers,E as getRedis,CZ as getAppRoles,SJ as disconnectRedis,pZ as disconnectMongo,j0 as deleteVerificationToken,tZ as deleteSession,WJ as createWsUpgradeHandler,g0 as createWorker,J0 as createVerificationToken,rQ as createSession,k0 as createServer,zJ as createRouter,m0 as createQueue,BJ as createApp,BQ as connectRedis,kQ as connectMongo,IQ as connectAuthMongo,bQ as connectAppMongo,dZ as clearMemoryStore,P0 as cacheResponse,K0 as bustCachePattern,x0 as bustCache,bZ as bustAuthLimit,CQ as buildFingerprint,WZ as botProtection,wQ as bearerAuth,m as authConnection,V as appConnection,l0 as addUserRole,N as HttpError,ZQ as HEADER_USER_TOKEN,c as COOKIE_TOKEN,M as AuthUser};
1
+ // App factory
2
+ export { createApp } from "./app";
3
+ export { createServer } from "./server";
4
+ // Lib utilities
5
+ export { getAppRoles } from "./lib/appConfig";
6
+ export { HttpError } from "./lib/HttpError";
7
+ export { COOKIE_TOKEN, HEADER_USER_TOKEN } from "./lib/constants";
8
+ export { createRouter } from "./lib/context";
9
+ export { signToken, verifyToken } from "./lib/jwt";
10
+ export { log } from "./lib/logger";
11
+ export { connectMongo, connectAuthMongo, connectAppMongo, disconnectMongo, authConnection, appConnection, mongoose } from "./lib/mongo";
12
+ export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
13
+ export { createQueue, createWorker } from "./lib/queue";
14
+ export { createSession, getSession, deleteSession, setSessionStore } from "./lib/session";
15
+ export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
16
+ export { bustAuthLimit, trackAttempt, isLimited } from "./lib/authRateLimit";
17
+ export { validate } from "./lib/validate";
18
+ // Middleware
19
+ export { bearerAuth } from "./middleware/bearerAuth";
20
+ export { botProtection } from "./middleware/botProtection";
21
+ export { identify } from "./middleware/identify";
22
+ export { rateLimit } from "./middleware/rateLimit";
23
+ export { userAuth } from "./middleware/userAuth";
24
+ export { requireRole } from "./middleware/requireRole";
25
+ export { requireVerifiedEmail } from "./middleware/requireVerifiedEmail";
26
+ export { cacheResponse, bustCache, bustCachePattern, setCacheStore } from "./middleware/cacheResponse";
27
+ // Lib utilities (bot protection)
28
+ export { buildFingerprint } from "./lib/fingerprint";
29
+ // Models
30
+ export { AuthUser } from "./models/AuthUser";
31
+ export { mongoAuthAdapter } from "./adapters/mongoAuth";
32
+ export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
33
+ export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
34
+ export { setUserRoles, addUserRole, removeUserRole } from "./lib/roles";
35
+ // WebSocket
36
+ export { websocket, createWsUpgradeHandler } from "./ws/index";
37
+ export { publish, subscribe, unsubscribe, getSubscriptions, handleRoomActions, getRooms, getRoomSubscribers } from "./lib/ws";
@@ -0,0 +1,7 @@
1
+ export class HttpError extends Error {
2
+ status;
3
+ constructor(status, message) {
4
+ super(message);
5
+ this.status = status;
6
+ }
7
+ }
@@ -0,0 +1,17 @@
1
+ let appName = "Core API";
2
+ let appRoles = [];
3
+ let defaultRole = null;
4
+ let _primaryField = "email";
5
+ let _emailVerificationConfig = null;
6
+ export const setAppName = (name) => { appName = name; };
7
+ export const getAppName = () => appName;
8
+ export const setAppRoles = (roles) => { appRoles = roles; };
9
+ export const getAppRoles = () => appRoles;
10
+ export const setDefaultRole = (role) => { defaultRole = role; };
11
+ export const getDefaultRole = () => defaultRole;
12
+ export const setPrimaryField = (field) => { _primaryField = field; };
13
+ export const getPrimaryField = () => _primaryField;
14
+ export const setEmailVerificationConfig = (config) => { _emailVerificationConfig = config; };
15
+ export const getEmailVerificationConfig = () => _emailVerificationConfig;
16
+ const DEFAULT_TOKEN_EXPIRY = 60 * 60 * 24; // 24 hours
17
+ export const getTokenExpiry = () => _emailVerificationConfig?.tokenExpiry ?? DEFAULT_TOKEN_EXPIRY;
@@ -0,0 +1,7 @@
1
+ let _adapter = null;
2
+ export const setAuthAdapter = (adapter) => { _adapter = adapter; };
3
+ export const getAuthAdapter = () => {
4
+ if (!_adapter)
5
+ throw new Error("No auth adapter set — pass authAdapter to createApp/createServer, or call setAuthAdapter()");
6
+ return _adapter;
7
+ };
@@ -0,0 +1,77 @@
1
+ import { getAppName } from "./appConfig";
2
+ // ---------------------------------------------------------------------------
3
+ // Memory implementation
4
+ // ---------------------------------------------------------------------------
5
+ const _memoryStore = new Map();
6
+ const memoryStore = {
7
+ async get(key) {
8
+ const entry = _memoryStore.get(key);
9
+ if (!entry)
10
+ return null;
11
+ if (entry.resetAt <= Date.now()) {
12
+ _memoryStore.delete(key);
13
+ return null;
14
+ }
15
+ return entry;
16
+ },
17
+ async set(key, entry) {
18
+ _memoryStore.set(key, entry);
19
+ },
20
+ async delete(key) {
21
+ _memoryStore.delete(key);
22
+ },
23
+ };
24
+ // ---------------------------------------------------------------------------
25
+ // Redis implementation
26
+ // ---------------------------------------------------------------------------
27
+ const redisStore = {
28
+ async get(key) {
29
+ const { getRedis } = await import("./redis");
30
+ const raw = await getRedis().get(`rl:${getAppName()}:${key}`);
31
+ if (!raw)
32
+ return null;
33
+ const entry = JSON.parse(raw);
34
+ if (entry.resetAt <= Date.now())
35
+ return null;
36
+ return entry;
37
+ },
38
+ async set(key, entry, ttlMs) {
39
+ const { getRedis } = await import("./redis");
40
+ await getRedis().set(`rl:${getAppName()}:${key}`, JSON.stringify(entry), "PX", ttlMs);
41
+ },
42
+ async delete(key) {
43
+ const { getRedis } = await import("./redis");
44
+ await getRedis().del(`rl:${getAppName()}:${key}`);
45
+ },
46
+ };
47
+ // ---------------------------------------------------------------------------
48
+ // Active store + setter
49
+ // ---------------------------------------------------------------------------
50
+ let _store = memoryStore;
51
+ export const setAuthRateLimitStore = (store) => {
52
+ _store = store === "redis" ? redisStore : memoryStore;
53
+ };
54
+ /** Returns true if the key is currently over the limit (read-only, no increment). */
55
+ export const isLimited = async (key, opts) => {
56
+ const entry = await _store.get(key);
57
+ if (!entry)
58
+ return false;
59
+ return entry.count >= opts.max;
60
+ };
61
+ /** Increments the counter and returns true if now over the limit. */
62
+ export const trackAttempt = async (key, opts) => {
63
+ const now = Date.now();
64
+ const existing = await _store.get(key);
65
+ if (!existing) {
66
+ await _store.set(key, { count: 1, resetAt: now + opts.windowMs }, opts.windowMs);
67
+ return 1 >= opts.max;
68
+ }
69
+ const updated = { count: existing.count + 1, resetAt: existing.resetAt };
70
+ const remaining = Math.max(1, existing.resetAt - now);
71
+ await _store.set(key, updated, remaining);
72
+ return updated.count >= opts.max;
73
+ };
74
+ /** Resets a rate limit key. Use on login success or for admin unlock. */
75
+ export const bustAuthLimit = async (key) => {
76
+ await _store.delete(key);
77
+ };
@@ -0,0 +1,2 @@
1
+ export const COOKIE_TOKEN = "token";
2
+ export const HEADER_USER_TOKEN = "x-user-token";
@@ -0,0 +1,8 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ const defaultHook = (result, c) => {
3
+ if (!result.success) {
4
+ const message = result.error.issues.map((i) => i.message).join(", ");
5
+ return c.json({ error: message }, 400);
6
+ }
7
+ };
8
+ export const createRouter = () => new OpenAPIHono({ defaultHook });
@@ -0,0 +1,77 @@
1
+ import { getRedis } from "./redis";
2
+ import { appConnection } from "./mongo";
3
+ import { getAppName, getTokenExpiry } from "./appConfig";
4
+ import { Schema } from "mongoose";
5
+ import { sqliteCreateVerificationToken, sqliteGetVerificationToken, sqliteDeleteVerificationToken, } from "../adapters/sqliteAuth";
6
+ import { memoryCreateVerificationToken, memoryGetVerificationToken, memoryDeleteVerificationToken, } from "../adapters/memoryAuth";
7
+ const verificationSchema = new Schema({
8
+ token: { type: String, required: true, unique: true },
9
+ userId: { type: String, required: true },
10
+ email: { type: String, required: true },
11
+ expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
12
+ }, { collection: "email_verifications" });
13
+ function getVerificationModel() {
14
+ return appConnection.models["EmailVerification"] ??
15
+ appConnection.model("EmailVerification", verificationSchema);
16
+ }
17
+ let _store = "redis";
18
+ export const setEmailVerificationStore = (store) => { _store = store; };
19
+ // ---------------------------------------------------------------------------
20
+ // Public API
21
+ // ---------------------------------------------------------------------------
22
+ export const createVerificationToken = async (userId, email) => {
23
+ const token = crypto.randomUUID();
24
+ const ttl = getTokenExpiry();
25
+ if (_store === "memory") {
26
+ memoryCreateVerificationToken(token, userId, email, ttl);
27
+ return token;
28
+ }
29
+ if (_store === "sqlite") {
30
+ sqliteCreateVerificationToken(token, userId, email, ttl);
31
+ return token;
32
+ }
33
+ if (_store === "mongo") {
34
+ await getVerificationModel().create({
35
+ token,
36
+ userId,
37
+ email,
38
+ expiresAt: new Date(Date.now() + ttl * 1000),
39
+ });
40
+ return token;
41
+ }
42
+ await getRedis().set(`verify:${getAppName()}:${token}`, JSON.stringify({ userId, email }), "EX", ttl);
43
+ return token;
44
+ };
45
+ export const getVerificationToken = async (token) => {
46
+ if (_store === "memory")
47
+ return memoryGetVerificationToken(token);
48
+ if (_store === "sqlite")
49
+ return sqliteGetVerificationToken(token);
50
+ if (_store === "mongo") {
51
+ const doc = await getVerificationModel()
52
+ .findOne({ token, expiresAt: { $gt: new Date() } })
53
+ .lean();
54
+ if (!doc)
55
+ return null;
56
+ return { userId: doc.userId, email: doc.email };
57
+ }
58
+ const raw = await getRedis().get(`verify:${getAppName()}:${token}`);
59
+ if (!raw)
60
+ return null;
61
+ return JSON.parse(raw);
62
+ };
63
+ export const deleteVerificationToken = async (token) => {
64
+ if (_store === "memory") {
65
+ memoryDeleteVerificationToken(token);
66
+ return;
67
+ }
68
+ if (_store === "sqlite") {
69
+ sqliteDeleteVerificationToken(token);
70
+ return;
71
+ }
72
+ if (_store === "mongo") {
73
+ await getVerificationModel().deleteOne({ token });
74
+ return;
75
+ }
76
+ await getRedis().del(`verify:${getAppName()}:${token}`);
77
+ };
@@ -0,0 +1,36 @@
1
+ const BROWSER_HEADERS = [
2
+ "sec-fetch-site",
3
+ "sec-fetch-mode",
4
+ "sec-fetch-dest",
5
+ "sec-ch-ua",
6
+ "sec-ch-ua-mobile",
7
+ "sec-ch-ua-platform",
8
+ "origin",
9
+ "referer",
10
+ "x-requested-with",
11
+ ];
12
+ const encoder = new TextEncoder();
13
+ /**
14
+ * Builds a 12-hex-char fingerprint from stable HTTP headers.
15
+ * IP-independent: bots that rotate IPs but use the same HTTP client
16
+ * will produce the same fingerprint and share a rate-limit bucket.
17
+ */
18
+ export async function buildFingerprint(req) {
19
+ const h = (name) => req.headers.get(name) ?? "";
20
+ // Encode which browser-only headers are present as a bitmask string.
21
+ // Real browsers send most of these; raw HTTP clients send none.
22
+ const bitmap = BROWSER_HEADERS.map((name) => req.headers.has(name) ? "1" : "0").join("");
23
+ const raw = [
24
+ h("user-agent"),
25
+ h("accept"),
26
+ h("accept-language"),
27
+ h("accept-encoding"),
28
+ h("connection"),
29
+ bitmap,
30
+ ].join("|");
31
+ const buf = await crypto.subtle.digest("SHA-256", encoder.encode(raw));
32
+ const bytes = new Uint8Array(buf).slice(0, 6);
33
+ return Array.from(bytes)
34
+ .map((b) => b.toString(16).padStart(2, "0"))
35
+ .join("");
36
+ }
@@ -0,0 +1,11 @@
1
+ import { SignJWT, jwtVerify } from "jose";
2
+ const isProd = process.env.NODE_ENV === "production";
3
+ const secret = new TextEncoder().encode(isProd ? process.env.JWT_SECRET_PROD : process.env.JWT_SECRET_DEV);
4
+ export const signToken = async (userId) => new SignJWT({ sub: userId })
5
+ .setProtectedHeader({ alg: "HS256" })
6
+ .setExpirationTime("7d")
7
+ .sign(secret);
8
+ export const verifyToken = async (token) => {
9
+ const { payload } = await jwtVerify(token, secret);
10
+ return payload;
11
+ };
@@ -0,0 +1,7 @@
1
+ const isDev = process.env.NODE_ENV !== "production";
2
+ const verboseEnv = process.env.LOGGING_VERBOSE;
3
+ const verbose = verboseEnv !== undefined ? verboseEnv === "true" : isDev;
4
+ export const log = (...args) => {
5
+ if (verbose)
6
+ console.log(...args);
7
+ };