@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.
- package/README.md +37 -6
- package/dist/adapters/memoryAuth.js +207 -0
- package/dist/adapters/mongoAuth.js +93 -0
- package/dist/adapters/sqliteAuth.js +242 -0
- package/dist/app.js +175 -0
- package/dist/cli.js +92 -48
- package/dist/index.js +37 -2
- package/dist/lib/HttpError.js +7 -0
- package/dist/lib/appConfig.js +17 -0
- package/dist/lib/authAdapter.js +7 -0
- package/dist/lib/authRateLimit.js +77 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/context.js +8 -0
- package/dist/lib/emailVerification.js +77 -0
- package/dist/lib/fingerprint.js +36 -0
- package/dist/lib/jwt.js +11 -0
- package/dist/lib/logger.js +7 -0
- package/dist/lib/mongo.js +73 -0
- package/dist/lib/oauth.js +82 -0
- package/dist/lib/queue.js +4 -0
- package/dist/lib/redis.js +50 -0
- package/dist/lib/roles.js +22 -0
- package/dist/lib/session.js +68 -0
- package/dist/lib/validate.js +14 -0
- package/dist/lib/ws.js +64 -0
- package/dist/middleware/bearerAuth.js +10 -0
- package/dist/middleware/botProtection.js +50 -0
- package/dist/middleware/cacheResponse.js +158 -0
- package/dist/middleware/cors.js +17 -0
- package/dist/middleware/errorHandler.js +13 -0
- package/dist/middleware/identify.js +33 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/logger.js +7 -0
- package/dist/middleware/rateLimit.js +20 -0
- package/dist/middleware/requireRole.js +36 -0
- package/dist/middleware/requireVerifiedEmail.js +25 -0
- package/dist/middleware/userAuth.js +6 -0
- package/dist/models/AuthUser.js +14 -0
- package/dist/routes/auth.js +206 -0
- package/dist/routes/health.js +22 -0
- package/dist/routes/home.js +16 -0
- package/dist/routes/oauth.js +150 -0
- package/dist/schemas/auth.js +9 -0
- package/dist/server.js +53 -0
- package/dist/services/auth.js +54 -0
- package/dist/ws/index.js +31 -0
- 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
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
31
|
-
`,
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`,"utf-8");
|
|
144
|
-
|
|
145
|
-
|
|
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 ${
|
|
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
|
-
//
|
|
2
|
-
|
|
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,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,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
|
+
};
|