@lastshotlabs/bunshot 0.0.5 → 0.0.6
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/cli.js +92 -48
- package/dist/index.js +26 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,39 @@ A personal Bun + Hono API framework. Install it in any app and get auth, session
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## CLI — Scaffold a New Project
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bunx @lastshotlabs/bunshot "My App"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You can also pass a custom directory name:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bunx @lastshotlabs/bunshot "My App" my-app-dir
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This creates a ready-to-run project with:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
my-app/
|
|
34
|
+
src/
|
|
35
|
+
index.ts # entry point
|
|
36
|
+
config/index.ts # centralized app configuration
|
|
37
|
+
lib/constants.ts # app name, version, roles
|
|
38
|
+
routes/ # add your route files here
|
|
39
|
+
workers/ # BullMQ workers (auto-discovered)
|
|
40
|
+
middleware/ # custom middleware
|
|
41
|
+
models/ # data models
|
|
42
|
+
services/ # business logic
|
|
43
|
+
tsconfig.json # pre-configured with path aliases
|
|
44
|
+
.env # environment variables template
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Path aliases like `@config/*`, `@lib/*`, `@middleware/*`, `@models/*`, `@routes/*`, `@services/*`, and `@workers/*` are set up automatically in `tsconfig.json`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
18
51
|
## Installation
|
|
19
52
|
|
|
20
53
|
```bash
|
|
@@ -32,15 +65,13 @@ bun add @lastshotlabs/bunshot
|
|
|
32
65
|
```ts
|
|
33
66
|
// src/index.ts
|
|
34
67
|
import { createServer } from "@lastshotlabs/bunshot";
|
|
68
|
+
import { appConfig } from "@config/index";
|
|
35
69
|
|
|
36
|
-
await createServer(
|
|
37
|
-
routesDir: import.meta.dir + "/routes",
|
|
38
|
-
workersDir: import.meta.dir + "/workers",
|
|
39
|
-
app: { name: "My App", version: "1.0.0" },
|
|
40
|
-
// db: { mongo: "single", redis: true } — defaults, connects automatically
|
|
41
|
-
});
|
|
70
|
+
await createServer(appConfig);
|
|
42
71
|
```
|
|
43
72
|
|
|
73
|
+
All configuration lives in `src/config/index.ts` — see the CLI-generated scaffold for the full setup.
|
|
74
|
+
|
|
44
75
|
That's it. Your app gets:
|
|
45
76
|
|
|
46
77
|
| Endpoint | Description |
|
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,27 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lastshotlabs/bunshot",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Batteries-included Bun + Hono API framework — auth, sessions, rate limiting, WebSocket, queues, and OpenAPI docs out of the box",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
|
-
"build": "bun build src/index.ts --outdir dist --minify --target bun --external
|
|
34
|
+
"build": "bun build src/index.ts --outdir dist --minify --target bun --external @hono/zod-openapi --external @scalar/hono-api-reference --external arctic --external bullmq --external hono --external ioredis --external jose --external mongoose --external zod && bun build src/cli.ts --outdir dist --minify --target bun --external @hono/zod-openapi --external @scalar/hono-api-reference --external arctic --external bullmq --external hono --external ioredis --external jose --external mongoose --external zod && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
35
35
|
"prepublishOnly": "bun run build",
|
|
36
36
|
"release": "npm version patch && npm publish",
|
|
37
37
|
"dev": "bun --watch src/index.ts",
|