@lastshotlabs/bunshot 0.0.5 → 0.0.7

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 (47) hide show
  1. package/README.md +37 -6
  2. package/dist/adapters/memoryAuth.js +207 -0
  3. package/dist/adapters/mongoAuth.js +93 -0
  4. package/dist/adapters/sqliteAuth.js +242 -0
  5. package/dist/app.js +175 -0
  6. package/dist/cli.js +92 -48
  7. package/dist/index.js +37 -2
  8. package/dist/lib/HttpError.js +7 -0
  9. package/dist/lib/appConfig.js +17 -0
  10. package/dist/lib/authAdapter.js +7 -0
  11. package/dist/lib/authRateLimit.js +77 -0
  12. package/dist/lib/constants.js +2 -0
  13. package/dist/lib/context.js +8 -0
  14. package/dist/lib/emailVerification.js +77 -0
  15. package/dist/lib/fingerprint.js +36 -0
  16. package/dist/lib/jwt.js +11 -0
  17. package/dist/lib/logger.js +7 -0
  18. package/dist/lib/mongo.js +73 -0
  19. package/dist/lib/oauth.js +82 -0
  20. package/dist/lib/queue.js +4 -0
  21. package/dist/lib/redis.js +50 -0
  22. package/dist/lib/roles.js +22 -0
  23. package/dist/lib/session.js +68 -0
  24. package/dist/lib/validate.js +14 -0
  25. package/dist/lib/ws.js +64 -0
  26. package/dist/middleware/bearerAuth.js +10 -0
  27. package/dist/middleware/botProtection.js +50 -0
  28. package/dist/middleware/cacheResponse.js +158 -0
  29. package/dist/middleware/cors.js +17 -0
  30. package/dist/middleware/errorHandler.js +13 -0
  31. package/dist/middleware/identify.js +33 -0
  32. package/dist/middleware/index.js +1 -0
  33. package/dist/middleware/logger.js +7 -0
  34. package/dist/middleware/rateLimit.js +20 -0
  35. package/dist/middleware/requireRole.js +36 -0
  36. package/dist/middleware/requireVerifiedEmail.js +25 -0
  37. package/dist/middleware/userAuth.js +6 -0
  38. package/dist/models/AuthUser.js +14 -0
  39. package/dist/routes/auth.js +206 -0
  40. package/dist/routes/health.js +22 -0
  41. package/dist/routes/home.js +16 -0
  42. package/dist/routes/oauth.js +150 -0
  43. package/dist/schemas/auth.js +9 -0
  44. package/dist/server.js +53 -0
  45. package/dist/services/auth.js +54 -0
  46. package/dist/ws/index.js +31 -0
  47. 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
@@ -1,34 +1,66 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var R=import.meta.require;import{existsSync as V,mkdirSync as E,writeFileSync as J,readSync as v,rmSync as I}from"fs";import{join as z}from"path";import{spawnSync as Q}from"child_process";function W($){process.stdout.write($);let L=Buffer.alloc(1024),_=v(0,L,0,L.length,null);return L.subarray(0,_).toString().trim().replace(/\r/g,"")}var X=process.argv[2],q=process.argv[3],K=X||W("App name: ");if(!K)console.error("App name is required."),process.exit(1);var M=K.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),A=q||(X?M:W(`Directory (${M}): `))||M,B=z(process.cwd(),A),G=z(B,"src"),b=z(G,"routes"),C=z(G,"workers"),x=z(G,"queues"),F=z(G,"ws"),P=z(G,"services");if(V(B))console.error(`Directory "${A}" already exists.`),process.exit(1);var T=`import { createServer, type CreateServerConfig } from "@lastshotlabs/bunshot";
3
+ var D=import.meta.require;import{existsSync as N,mkdirSync as X,writeFileSync as q,readSync as b,rmSync as h}from"fs";import{join as B}from"path";import{spawnSync as M}from"child_process";function w(z){process.stdout.write(z);let H=Buffer.alloc(1024),Q=b(0,H,0,H.length,null);return H.subarray(0,Q).toString().trim().replace(/\r/g,"")}function E(z,H,Q=0){let J=Q;function Z($=!1){if(!$)process.stdout.write(`\x1B[${H.length}A`);for(let A=0;A<H.length;A++){let V=A===J,R=V?"\x1B[36m>\x1B[0m":" ",g=V?`\x1B[1m${H[A]}\x1B[0m`:`\x1B[2m${H[A]}\x1B[0m`;process.stdout.write(`\x1B[2K ${R} ${g}
4
+ `)}}if(!process.stdin.isTTY){console.log(z),H.forEach((V,R)=>console.log(` ${R+1}) ${V}`));let $=w(` Choose [${Q+1}]: `);if(!$)return Q;let A=parseInt($);if(A>=1&&A<=H.length)return A-1;return Q}console.log(z),process.stdout.write("\x1B[?25l"),Z(!0),process.stdin.setRawMode(!0);let _=Buffer.alloc(16);try{while(!0){let $=b(0,_,0,_.length,null),A=_.subarray(0,$).toString();if(A==="\r"||A===`
5
+ `)break;else if(A==="\x1B[A"||A==="\x1BOA")J=(J-1+H.length)%H.length,Z();else if(A==="\x1B[B"||A==="\x1BOB")J=(J+1)%H.length,Z();else if(A==="\x03")process.stdout.write(`\x1B[?25h
6
+ `),process.stdin.setRawMode(!1),process.exit(0);else{let V=parseInt(A);if(V>=1&&V<=H.length){J=V-1,Z();break}}}}finally{process.stdin.setRawMode(!1),process.stdout.write("\x1B[?25h")}return J}var f=process.argv[2],m=process.argv[3],O=f||w("App name: ");if(!O)console.error("App name is required."),process.exit(1);var I=O.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),G=m||(f?I:w(`Directory (${I}): `))||I,K=!1,W=!1,P="mongo",v="redis",T="redis",F="redis";console.log("");var x=E("Database setup:",["Full stack (MongoDB + Redis \u2014 production ready)","SQLite (single file, no external services)","Memory (ephemeral, great for prototyping/tests)","Custom (choose each store individually)"]);if(x===0)K=E("MongoDB connection mode:",["Single (auth + app data share one connection)","Separate (auth on its own cluster)"])===0?"single":"separate",W=!0,P="mongo",v="redis",T="redis",F="redis";else if(x===1)K=!1,W=!1,P="sqlite",v="sqlite",T="sqlite",F="sqlite";else if(x===2)K=!1,W=!1,P="memory",v="memory",T="memory",F="memory";else{console.log(`
7
+ Configure each store:
8
+ `);let z=E("MongoDB:",["Single (one connection for auth + app data)","Separate (auth on its own cluster)","None (no MongoDB)"]);if(z===0)K="single";else if(z===1)K="separate";else K=!1;W=E("Redis:",["Yes","No"])===0;let Q=[],J=[];if(W)Q.push("redis"),J.push("Redis");if(K)Q.push("mongo"),J.push("MongoDB");Q.push("sqlite","memory"),J.push("SQLite","Memory");let Z=[],_=[];if(K)Z.push("mongo"),_.push("MongoDB");Z.push("sqlite","memory"),_.push("SQLite","Memory");let $=E("Auth store:",_);P=Z[$];let A=E("Sessions store:",J);v=Q[A];let V=E("Cache store:",J);T=Q[V];let R=E("OAuth state store:",J);F=Q[R]}var S=P==="sqlite"||v==="sqlite"||T==="sqlite"||F==="sqlite",U=B(process.cwd(),G),Y=B(U,"src"),k=B(Y,"config"),j=B(Y,"lib"),p=B(Y,"routes"),l=B(Y,"workers"),u=B(Y,"queues"),d=B(Y,"ws"),c=B(Y,"services"),a=B(Y,"middleware"),r=B(Y,"models");if(N(U))console.error(`Directory "${G}" already exists.`),process.exit(1);function n(){let z=[];if(K)z.push(` mongo: "${K}",`);else z.push(" mongo: false,");if(z.push(` redis: ${W},`),z.push(` auth: "${P}",`),z.push(` sessions: "${v}",`),z.push(` oauthState: "${F}",`),z.push(` cache: "${T}",`),S)z.push(' sqlite: path.join(import.meta.dir, "../../data.db"),');return`{
9
+ ${z.join(`
10
+ `)}
11
+ }`}var s=`export const APP_NAME = "${O}";
12
+ export const APP_VERSION = "1.0.0";
13
+
14
+ export const USER_ROLES = {
15
+ ADMIN: "admin",
16
+ USER: "user",
17
+ };
18
+ `,t=`import path from "path";
19
+ import {
20
+ type AppMeta,
21
+ type AuthConfig,
22
+ type CreateServerConfig,
23
+ type DbConfig,
24
+ type SecurityConfig,
25
+ } from "@lastshotlabs/bunshot";
26
+ import { APP_NAME, APP_VERSION, USER_ROLES } from "@lib/constants";
27
+
28
+ export const app: AppMeta = {
29
+ name: APP_NAME,
30
+ version: APP_VERSION,
31
+ };
32
+
33
+ export const routesDir = path.join(import.meta.dir, "../routes");
34
+
35
+ export const workersDir = path.join(import.meta.dir, "../workers");
36
+
37
+ export const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
4
38
 
5
- const roles = {
6
- admin: "admin",
7
- user: "user",
39
+ export const db: DbConfig = ${n()};
40
+
41
+ export const auth: AuthConfig = {
42
+ roles: Object.values(USER_ROLES),
43
+ defaultRole: USER_ROLES.USER,
44
+ };
45
+
46
+ export const security: SecurityConfig = {
47
+ cors: ["*"],
8
48
  };
9
49
 
10
- const config: CreateServerConfig = {
11
- routesDir: import.meta.dir + "/routes",
12
- workersDir: import.meta.dir + "/workers",
13
- app: {
14
- name: "${K}",
15
- version: "1.0.0",
16
- },
17
- auth: {
18
- roles: Object.values(roles),
19
- defaultRole: roles.user,
20
- },
21
- security: {
22
- cors: ["*"],
23
- },
24
- db: {
25
- mongo: "single",
26
- },
27
- port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
50
+ export const appConfig: CreateServerConfig = {
51
+ app,
52
+ routesDir,
53
+ workersDir,
54
+ port,
55
+ db,
56
+ auth,
57
+ security,
28
58
  };
59
+ `,i=`import { createServer } from "@lastshotlabs/bunshot";
60
+ import { appConfig } from "@config/index";
29
61
 
30
- await createServer(config);
31
- `,h=`# ${K}
62
+ await createServer(appConfig);
63
+ `,o=`# ${O}
32
64
 
33
65
  Built with [@lastshotlabs/bunshot](https://github.com/Last-Shot-Labs/bunshot).
34
66
 
@@ -50,9 +82,14 @@ bun dev
50
82
 
51
83
  \`\`\`
52
84
  src/
53
- index.ts # server entry point
54
- routes/ # file-based routing (each file = a router)
55
- workers/ # BullMQ workers (auto-imported on start)
85
+ index.ts # server entry point
86
+ config/index.ts # centralized app configuration
87
+ lib/constants.ts # app name, version, roles
88
+ routes/ # file-based routing (each file = a router)
89
+ workers/ # BullMQ workers (auto-imported on start)
90
+ middleware/ # custom middleware
91
+ models/ # data models
92
+ services/ # business logic
56
93
  \`\`\`
57
94
 
58
95
  ## Adding routes
@@ -68,7 +105,7 @@ export const router = createRouter();
68
105
 
69
106
  router.get("/products", (c) => c.json({ products: [] }));
70
107
  \`\`\`
71
-
108
+ ${K?`
72
109
  ## Adding models
73
110
 
74
111
  \`\`\`ts
@@ -82,14 +119,21 @@ const ProductSchema = new mongoose.Schema({
82
119
 
83
120
  export const Product = appConnection.model("Product", ProductSchema);
84
121
  \`\`\`
85
-
122
+ `:""}
86
123
  ## Environment variables
87
124
 
88
- See \`.env\` \u2014 fill in MongoDB, Redis, JWT, Bearer token, and OAuth provider values before running.
89
- `,w=`NODE_ENV=development
90
- PORT=3000
91
-
92
- # MongoDB (single connection)
125
+ See \`.env\` \u2014 fill in the values before running.
126
+ `;function e(){let z=["NODE_ENV=development","PORT=3000"];if(K==="single")z.push(`
127
+ # MongoDB
128
+ MONGO_USER_DEV=
129
+ MONGO_PW_DEV=
130
+ MONGO_HOST_DEV=
131
+ MONGO_DB_DEV=
132
+ MONGO_USER_PROD=
133
+ MONGO_PW_PROD=
134
+ MONGO_HOST_PROD=
135
+ MONGO_DB_PROD=`);else if(K==="separate")z.push(`
136
+ # MongoDB (app data)
93
137
  MONGO_USER_DEV=
94
138
  MONGO_PW_DEV=
95
139
  MONGO_HOST_DEV=
@@ -99,7 +143,7 @@ MONGO_PW_PROD=
99
143
  MONGO_HOST_PROD=
100
144
  MONGO_DB_PROD=
101
145
 
102
- # MongoDB auth connection (only needed if mongo: "separate")
146
+ # MongoDB (auth \u2014 separate cluster)
103
147
  MONGO_AUTH_USER_DEV=
104
148
  MONGO_AUTH_PW_DEV=
105
149
  MONGO_AUTH_HOST_DEV=
@@ -107,16 +151,14 @@ MONGO_AUTH_DB_DEV=
107
151
  MONGO_AUTH_USER_PROD=
108
152
  MONGO_AUTH_PW_PROD=
109
153
  MONGO_AUTH_HOST_PROD=
110
- MONGO_AUTH_DB_PROD=
111
-
154
+ MONGO_AUTH_DB_PROD=`);if(W)z.push(`
112
155
  # Redis
113
156
  REDIS_HOST_DEV=
114
157
  REDIS_USER_DEV=
115
158
  REDIS_PW_DEV=
116
159
  REDIS_HOST_PROD=
117
160
  REDIS_USER_PROD=
118
- REDIS_PW_PROD=
119
-
161
+ REDIS_PW_PROD=`);return z.push(`
120
162
  # JWT
121
163
  JWT_SECRET_DEV=
122
164
  JWT_SECRET_PROD=
@@ -135,15 +177,17 @@ APPLE_CLIENT_ID=
135
177
  APPLE_TEAM_ID=
136
178
  APPLE_KEY_ID=
137
179
  APPLE_PRIVATE_KEY=
138
- APPLE_REDIRECT_URI=
139
- `;console.log(`
140
- @lastshotlabs/bunshot \u2014 creating ${A}
141
- `);E(B,{recursive:!0});console.log(" Running bun init...");Q("bun",["init","-y"],{cwd:B,stdio:"inherit"});var U=z(B,"index.ts");if(V(U))I(U);var Y=z(B,"package.json"),H=JSON.parse(R("fs").readFileSync(Y,"utf-8"));H.module="src/index.ts";H.scripts={dev:"bun --watch src/index.ts",start:"bun src/index.ts"};H.dependencies={...H.dependencies,"@lastshotlabs/bunshot":"*"};J(Y,JSON.stringify(H,null,2)+`
142
- `,"utf-8");var Z=z(B,"tsconfig.json"),O=JSON.parse(R("fs").readFileSync(Z,"utf-8"));O.compilerOptions={...O.compilerOptions,paths:{"@lib/*":["./src/lib/*"],"@middleware/*":["./src/middleware/*"],"@models/*":["./src/models/*"],"@queues/*":["./src/queues/*"],"@routes/*":["./src/routes/*"],"@scripts/*":["./src/scripts/*"],"@services/*":["./src/services/*"],"@workers/*":["./src/workers/*"],"@queues":["./src/queues/index.ts"],"@jobs":["./src/queues/jobs.ts"],"@ws":["./src/ws/index.ts"]}};J(Z,JSON.stringify(O,null,2)+`
143
- `,"utf-8");E(b,{recursive:!0});E(C,{recursive:!0});E(x,{recursive:!0});E(F,{recursive:!0});E(P,{recursive:!0});J(z(G,"index.ts"),T,"utf-8");J(z(B,".env"),w,"utf-8");J(z(B,"README.md"),h,"utf-8");console.log(" Created:");console.log(` + ${A}/src/index.ts`);console.log(` + ${A}/src/routes/`);console.log(` + ${A}/src/workers/`);console.log(` + ${A}/src/queues/`);console.log(` + ${A}/src/ws/`);console.log(` + ${A}/src/services/`);console.log(` + ${A}/.env`);console.log(` + ${A}/README.md`);console.log(`
144
- Initializing git...`);var N=Q("git",["init"],{cwd:B,stdio:"inherit"});if(N.status!==0)console.error(" git init failed \u2014 skipping.");console.log(`
145
- Installing dependencies...`);var u=Q("bun",["install"],{cwd:B,stdio:"inherit"});if(u.status!==0)console.error(`
180
+ APPLE_REDIRECT_URI=`),z.join(`
181
+ `)+`
182
+ `}console.log(`
183
+ @lastshotlabs/bunshot \u2014 creating ${G}
184
+ `);X(U,{recursive:!0});console.log(" Running bun init...");M("bun",["init","-y"],{cwd:U,stdio:"inherit"});var C=B(U,"index.ts");if(N(C))h(C);var y=B(U,"package.json"),L=JSON.parse(D("fs").readFileSync(y,"utf-8"));L.module="src/index.ts";L.scripts={dev:"bun --watch src/index.ts",start:"bun src/index.ts"};L.dependencies={...L.dependencies,"@lastshotlabs/bunshot":"*"};q(y,JSON.stringify(L,null,2)+`
185
+ `,"utf-8");var zz=B(U,"tsconfig.json"),Az={compilerOptions:{lib:["ESNext"],target:"ESNext",module:"Preserve",moduleDetection:"force",jsx:"react-jsx",allowJs:!0,moduleResolution:"bundler",allowImportingTsExtensions:!0,verbatimModuleSyntax:!0,noEmit:!0,strict:!0,skipLibCheck:!0,noFallthroughCasesInSwitch:!0,noUncheckedIndexedAccess:!0,noImplicitOverride:!0,noUnusedLocals:!1,noUnusedParameters:!1,noPropertyAccessFromIndexSignature:!1,paths:{"@lib/*":["./src/lib/*"],"@middleware/*":["./src/middleware/*"],"@models/*":["./src/models/*"],"@queues/*":["./src/queues/*"],"@routes/*":["./src/routes/*"],"@scripts/*":["./src/scripts/*"],"@services/*":["./src/services/*"],"@workers/*":["./src/workers/*"],"@service-facades/*":["./src/service-facades/*"],"@config/*":["./src/config/*"],"@constants/*":["./src/lib/constants/*"]}}};q(zz,JSON.stringify(Az,null,2)+`
186
+ `,"utf-8");X(k,{recursive:!0});X(j,{recursive:!0});X(p,{recursive:!0});X(l,{recursive:!0});X(u,{recursive:!0});X(d,{recursive:!0});X(c,{recursive:!0});X(a,{recursive:!0});X(r,{recursive:!0});q(B(j,"constants.ts"),s,"utf-8");q(B(k,"index.ts"),t,"utf-8");q(B(Y,"index.ts"),i,"utf-8");q(B(U,".env"),e(),"utf-8");q(B(U,"README.md"),o,"utf-8");console.log(" Created:");console.log(` + ${G}/src/index.ts`);console.log(` + ${G}/src/config/index.ts`);console.log(` + ${G}/src/lib/constants.ts`);console.log(` + ${G}/src/routes/`);console.log(` + ${G}/src/workers/`);console.log(` + ${G}/src/queues/`);console.log(` + ${G}/src/ws/`);console.log(` + ${G}/src/services/`);console.log(` + ${G}/src/middleware/`);console.log(` + ${G}/src/models/`);console.log(` + ${G}/.env`);console.log(` + ${G}/README.md`);console.log(`
187
+ DB config:`);console.log(` mongo: ${K||"none"} | redis: ${W}`);console.log(` auth: ${P} | sessions: ${v} | cache: ${T} | oauthState: ${F}`);console.log(`
188
+ Initializing git...`);var Bz=M("git",["init"],{cwd:U,stdio:"inherit"});if(Bz.status!==0)console.error(" git init failed \u2014 skipping.");console.log(`
189
+ Installing dependencies...`);var Gz=M("bun",["install"],{cwd:U,stdio:"inherit"});if(Gz.status!==0)console.error(`
146
190
  bun install failed. Run it manually inside the directory.`),process.exit(1);console.log(`
147
191
  Done! Next steps:
148
- `);console.log(` cd ${A}`);console.log(" # fill in .env");console.log(` bun dev
192
+ `);console.log(` cd ${G}`);console.log(" # fill in .env");console.log(` bun dev
149
193
  `);
package/dist/index.js CHANGED
@@ -1,2 +1,37 @@
1
- // @bun
2
- import{createApp as v}from"./app";import{createServer as B}from"./server";import{getAppRoles as D}from"./lib/appConfig";import{HttpError as G}from"./lib/HttpError";import{COOKIE_TOKEN as I,HEADER_USER_TOKEN as J}from"./lib/constants";import{createRouter as L}from"./lib/context";import{signToken as N,verifyToken as O}from"./lib/jwt";import{log as Q}from"./lib/logger";import{connectMongo as T,connectAuthMongo as W,connectAppMongo as X,disconnectMongo as Y,authConnection as Z,appConnection as _,mongoose as $}from"./lib/mongo";import{connectRedis as y,disconnectRedis as E,getRedis as R}from"./lib/redis";import{createQueue as V,createWorker as c}from"./lib/queue";import{createSession as g,getSession as q,deleteSession as b,setSessionStore as w}from"./lib/session";import{createVerificationToken as h,getVerificationToken as n,deleteVerificationToken as p}from"./lib/emailVerification";import{bustAuthLimit as o,trackAttempt as m,isLimited as l}from"./lib/authRateLimit";import{validate as a}from"./lib/validate";import{bearerAuth as s}from"./middleware/bearerAuth";import{botProtection as r}from"./middleware/botProtection";import{identify as jj}from"./middleware/identify";import{rateLimit as vj}from"./middleware/rateLimit";import{userAuth as Bj}from"./middleware/userAuth";import{requireRole as Dj}from"./middleware/requireRole";import{requireVerifiedEmail as Gj}from"./middleware/requireVerifiedEmail";import{cacheResponse as Ij,bustCache as Jj,bustCachePattern as Kj,setCacheStore as Lj}from"./middleware/cacheResponse";import{buildFingerprint as Nj}from"./lib/fingerprint";import{AuthUser as Pj}from"./models/AuthUser";import{mongoAuthAdapter as Sj}from"./adapters/mongoAuth";import{sqliteAuthAdapter as Wj,setSqliteDb as Xj,startSqliteCleanup as Yj}from"./adapters/sqliteAuth";import{memoryAuthAdapter as _j,clearMemoryStore as $j}from"./adapters/memoryAuth";import{setUserRoles as yj,addUserRole as Ej,removeUserRole as Rj}from"./lib/roles";import{websocket as Vj,createWsUpgradeHandler as cj}from"./ws/index";import{publish as gj,subscribe as qj,unsubscribe as bj,getSubscriptions as wj,handleRoomActions as Aj,getRooms as hj,getRoomSubscribers as nj}from"./lib/ws";export{Vj as websocket,O as verifyToken,a as validate,Bj as userAuth,bj as unsubscribe,m as trackAttempt,qj as subscribe,Yj as startSqliteCleanup,Wj as sqliteAuthAdapter,N as signToken,yj as setUserRoles,Xj as setSqliteDb,w as setSessionStore,Lj as setCacheStore,Gj as requireVerifiedEmail,Dj as requireRole,Rj as removeUserRole,vj as rateLimit,gj as publish,$ as mongoose,Sj as mongoAuthAdapter,_j as memoryAuthAdapter,Q as log,l as isLimited,jj as identify,Aj as handleRoomActions,n as getVerificationToken,wj as getSubscriptions,q as getSession,hj as getRooms,nj as getRoomSubscribers,R as getRedis,D as getAppRoles,E as disconnectRedis,Y as disconnectMongo,p as deleteVerificationToken,b as deleteSession,cj as createWsUpgradeHandler,c as createWorker,h as createVerificationToken,g as createSession,B as createServer,L as createRouter,V as createQueue,v as createApp,y as connectRedis,T as connectMongo,W as connectAuthMongo,X as connectAppMongo,$j as clearMemoryStore,Ij as cacheResponse,Kj as bustCachePattern,Jj as bustCache,o as bustAuthLimit,Nj as buildFingerprint,r as botProtection,s as bearerAuth,Z as authConnection,_ as appConnection,Ej as addUserRole,G as HttpError,J as HEADER_USER_TOKEN,I as COOKIE_TOKEN,Pj 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
+ };