@lastshotlabs/bunshot 0.0.13 → 0.0.18
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 +2816 -1747
- package/dist/adapters/memoryAuth.d.ts +7 -0
- package/dist/adapters/memoryAuth.js +177 -2
- package/dist/adapters/mongoAuth.js +94 -0
- package/dist/adapters/sqliteAuth.d.ts +9 -0
- package/dist/adapters/sqliteAuth.js +190 -2
- package/dist/app.d.ts +120 -2
- package/dist/app.js +104 -4
- package/dist/entrypoints/queue.d.ts +2 -2
- package/dist/entrypoints/queue.js +1 -1
- package/dist/index.d.ts +24 -8
- package/dist/index.js +15 -5
- package/dist/lib/appConfig.d.ts +81 -0
- package/dist/lib/appConfig.js +30 -0
- package/dist/lib/authAdapter.d.ts +54 -0
- package/dist/lib/authRateLimit.d.ts +2 -0
- package/dist/lib/authRateLimit.js +4 -0
- package/dist/lib/clientIp.d.ts +14 -0
- package/dist/lib/clientIp.js +52 -0
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/context.d.ts +2 -0
- package/dist/lib/createDtoMapper.d.ts +33 -0
- package/dist/lib/createDtoMapper.js +69 -0
- package/dist/lib/crypto.d.ts +11 -0
- package/dist/lib/crypto.js +22 -0
- package/dist/lib/emailVerification.d.ts +4 -0
- package/dist/lib/emailVerification.js +20 -12
- package/dist/lib/jwt.d.ts +1 -1
- package/dist/lib/jwt.js +19 -6
- package/dist/lib/mfaChallenge.d.ts +42 -0
- package/dist/lib/mfaChallenge.js +293 -0
- package/dist/lib/oauth.d.ts +14 -1
- package/dist/lib/oauth.js +19 -1
- package/dist/lib/oauthCode.d.ts +15 -0
- package/dist/lib/oauthCode.js +90 -0
- package/dist/lib/queue.d.ts +33 -0
- package/dist/lib/queue.js +98 -0
- package/dist/lib/resetPassword.js +12 -16
- package/dist/lib/roles.d.ts +4 -0
- package/dist/lib/roles.js +27 -0
- package/dist/lib/session.d.ts +12 -0
- package/dist/lib/session.js +165 -5
- package/dist/lib/tenant.d.ts +15 -0
- package/dist/lib/tenant.js +65 -0
- package/dist/lib/ws.js +5 -1
- package/dist/lib/zodToMongoose.d.ts +38 -0
- package/dist/lib/zodToMongoose.js +84 -0
- package/dist/middleware/bearerAuth.js +4 -3
- package/dist/middleware/botProtection.js +2 -2
- package/dist/middleware/cacheResponse.d.ts +1 -0
- package/dist/middleware/cacheResponse.js +18 -3
- package/dist/middleware/cors.d.ts +2 -0
- package/dist/middleware/cors.js +22 -8
- package/dist/middleware/csrf.d.ts +18 -0
- package/dist/middleware/csrf.js +115 -0
- package/dist/middleware/rateLimit.d.ts +2 -1
- package/dist/middleware/rateLimit.js +7 -5
- package/dist/middleware/requireRole.d.ts +14 -3
- package/dist/middleware/requireRole.js +46 -6
- package/dist/middleware/tenant.d.ts +5 -0
- package/dist/middleware/tenant.js +116 -0
- package/dist/models/AuthUser.d.ts +17 -0
- package/dist/models/AuthUser.js +17 -0
- package/dist/models/TenantRole.d.ts +15 -0
- package/dist/models/TenantRole.js +23 -0
- package/dist/routes/auth.d.ts +5 -3
- package/dist/routes/auth.js +173 -30
- package/dist/routes/jobs.d.ts +2 -0
- package/dist/routes/jobs.js +270 -0
- package/dist/routes/mfa.d.ts +5 -0
- package/dist/routes/mfa.js +616 -0
- package/dist/routes/oauth.js +378 -23
- package/dist/schemas/auth.d.ts +2 -0
- package/dist/schemas/auth.js +22 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +19 -3
- package/dist/services/auth.d.ts +18 -5
- package/dist/services/auth.js +112 -18
- package/dist/services/mfa.d.ts +84 -0
- package/dist/services/mfa.js +543 -0
- package/dist/ws/index.js +3 -2
- package/docs/sections/adding-middleware/full.md +35 -0
- package/docs/sections/adding-models/full.md +125 -0
- package/docs/sections/adding-models/overview.md +13 -0
- package/docs/sections/adding-routes/full.md +182 -0
- package/docs/sections/adding-routes/overview.md +23 -0
- package/docs/sections/auth-flow/full.md +634 -0
- package/docs/sections/auth-flow/overview.md +10 -0
- package/docs/sections/cli/full.md +30 -0
- package/docs/sections/configuration/full.md +155 -0
- package/docs/sections/configuration/overview.md +17 -0
- package/docs/sections/configuration-example/full.md +117 -0
- package/docs/sections/configuration-example/overview.md +30 -0
- package/docs/sections/documentation/full.md +171 -0
- package/docs/sections/environment-variables/full.md +55 -0
- package/docs/sections/exports/full.md +92 -0
- package/docs/sections/extending-context/full.md +59 -0
- package/docs/sections/header.md +3 -0
- package/docs/sections/installation/full.md +6 -0
- package/docs/sections/jobs/full.md +140 -0
- package/docs/sections/jobs/overview.md +15 -0
- package/docs/sections/mongodb-connections/full.md +45 -0
- package/docs/sections/mongodb-connections/overview.md +7 -0
- package/docs/sections/multi-tenancy/full.md +66 -0
- package/docs/sections/multi-tenancy/overview.md +15 -0
- package/docs/sections/oauth/full.md +189 -0
- package/docs/sections/oauth/overview.md +16 -0
- package/docs/sections/package-development/full.md +7 -0
- package/docs/sections/peer-dependencies/full.md +47 -0
- package/docs/sections/quick-start/full.md +43 -0
- package/docs/sections/response-caching/full.md +117 -0
- package/docs/sections/response-caching/overview.md +13 -0
- package/docs/sections/roles/full.md +136 -0
- package/docs/sections/roles/overview.md +12 -0
- package/docs/sections/running-without-redis/full.md +16 -0
- package/docs/sections/running-without-redis-or-mongodb/full.md +60 -0
- package/docs/sections/stack/full.md +10 -0
- package/docs/sections/websocket/full.md +101 -0
- package/docs/sections/websocket/overview.md +5 -0
- package/docs/sections/websocket-rooms/full.md +97 -0
- package/docs/sections/websocket-rooms/overview.md +5 -0
- package/package.json +30 -9
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
## Configuration
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
await createServer({
|
|
5
|
+
// Required
|
|
6
|
+
routesDir: import.meta.dir + "/routes",
|
|
7
|
+
|
|
8
|
+
// Shared schemas (imported before routes; see "Shared schemas across routes" above)
|
|
9
|
+
modelSchemas: import.meta.dir + "/schemas", // string shorthand — registration: "auto"
|
|
10
|
+
// modelSchemas: [dir + "/schemas", dir + "/models"], // multiple dirs
|
|
11
|
+
// modelSchemas: { paths: dir + "/schemas", registration: "explicit" }, // full object
|
|
12
|
+
|
|
13
|
+
// App metadata (shown in root endpoint + OpenAPI docs)
|
|
14
|
+
app: {
|
|
15
|
+
name: "My App", // default: "Bun Core API"
|
|
16
|
+
version: "1.0.0", // default: "1.0.0"
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// Auth, roles, and OAuth
|
|
20
|
+
auth: {
|
|
21
|
+
enabled: true, // default: true — set false to disable /auth/* routes
|
|
22
|
+
adapter: pgAuthAdapter, // custom adapter — overrides db.auth (use for Postgres etc.)
|
|
23
|
+
roles: ["admin", "editor", "user"], // valid roles — required to use requireRole
|
|
24
|
+
defaultRole: "user", // assigned to every new user on /auth/register
|
|
25
|
+
primaryField: "email", // default: "email" — use "username" or "phone" to change the login identifier
|
|
26
|
+
emailVerification: { // optional — only active when primaryField is "email"
|
|
27
|
+
required: true, // default: false (soft gate) — set true to block login until verified
|
|
28
|
+
tokenExpiry: 60 * 60, // default: 86400 (24 hours) — token TTL in seconds
|
|
29
|
+
onSend: async (email, token) => { // called after registration and resend — use any email provider
|
|
30
|
+
await resend.emails.send({ to: email, subject: "Verify your email", text: `Token: ${token}` });
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
passwordReset: { // optional — only active when primaryField is "email"
|
|
34
|
+
tokenExpiry: 60 * 60, // default: 3600 (1 hour) — token TTL in seconds
|
|
35
|
+
onSend: async (email, token) => { // called by POST /auth/forgot-password — use any email provider
|
|
36
|
+
await resend.emails.send({ to: email, subject: "Reset your password", text: `Token: ${token}` });
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
rateLimit: { // optional — built-in auth endpoint rate limiting
|
|
40
|
+
login: { windowMs: 15 * 60 * 1000, max: 10 }, // default: 10 failures / 15 min
|
|
41
|
+
register: { windowMs: 60 * 60 * 1000, max: 5 }, // default: 5 attempts / hour (per IP)
|
|
42
|
+
verifyEmail: { windowMs: 15 * 60 * 1000, max: 10 }, // default: 10 attempts / 15 min (per IP)
|
|
43
|
+
resendVerification: { windowMs: 60 * 60 * 1000, max: 3 }, // default: 3 attempts / hour (per user)
|
|
44
|
+
forgotPassword: { windowMs: 15 * 60 * 1000, max: 5 }, // default: 5 attempts / 15 min (per IP)
|
|
45
|
+
resetPassword: { windowMs: 15 * 60 * 1000, max: 10 }, // default: 10 attempts / 15 min (per IP)
|
|
46
|
+
mfaVerify: { windowMs: 15 * 60 * 1000, max: 10 }, // default: 10 attempts / 15 min (per IP)
|
|
47
|
+
mfaResend: { windowMs: 60 * 1000, max: 5 }, // default: 5 attempts / minute (per IP)
|
|
48
|
+
store: "redis", // default: "redis" when Redis is enabled, else "memory"
|
|
49
|
+
},
|
|
50
|
+
sessionPolicy: { // optional — session concurrency and metadata
|
|
51
|
+
maxSessions: 6, // default: 6 — max simultaneous sessions per user; oldest evicted when exceeded
|
|
52
|
+
persistSessionMetadata: true, // default: true — keep IP/UA/timestamp row after session expires (for device detection)
|
|
53
|
+
includeInactiveSessions: false, // default: false — include expired/deleted sessions in GET /auth/sessions
|
|
54
|
+
trackLastActive: false, // default: false — update lastActiveAt on every auth'd request (adds one DB write)
|
|
55
|
+
},
|
|
56
|
+
passwordPolicy: { // optional — password complexity rules (applies to register + reset, not login)
|
|
57
|
+
minLength: 8, // default: 8
|
|
58
|
+
requireLetter: true, // default: true — at least one a–z or A–Z
|
|
59
|
+
requireDigit: true, // default: true — at least one 0–9
|
|
60
|
+
requireSpecial: false, // default: false — at least one non-alphanumeric character
|
|
61
|
+
},
|
|
62
|
+
oauth: {
|
|
63
|
+
providers: { google: { ... }, apple: { ... } }, // omit a provider to disable it
|
|
64
|
+
postRedirect: "/dashboard", // default: "/"
|
|
65
|
+
allowedRedirectUrls: ["https://myapp.com"], // optional — validate postRedirect against allowlist at startup
|
|
66
|
+
},
|
|
67
|
+
refreshTokens: { // optional — short-lived access + long-lived refresh tokens
|
|
68
|
+
accessTokenExpiry: 900, // default: 900 (15 min)
|
|
69
|
+
refreshTokenExpiry: 2_592_000, // default: 2_592_000 (30 days)
|
|
70
|
+
rotationGraceSeconds: 30, // default: 30 — old token still works briefly after rotation
|
|
71
|
+
},
|
|
72
|
+
mfa: { // optional — TOTP/MFA support (requires otpauth peer dep)
|
|
73
|
+
issuer: "My App", // shown in authenticator apps (default: app name)
|
|
74
|
+
recoveryCodes: 10, // default: 10
|
|
75
|
+
challengeTtlSeconds: 300, // default: 300 (5 min)
|
|
76
|
+
emailOtp: { // optional — email OTP as alternative MFA method
|
|
77
|
+
onSend: async (email, code) => {}, // called to deliver the OTP code — use any email provider
|
|
78
|
+
codeLength: 6, // default: 6
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
accountDeletion: { // optional — enables DELETE /auth/me
|
|
82
|
+
onBeforeDelete: async (userId) => {}, // throw to abort
|
|
83
|
+
onAfterDelete: async (userId) => {}, // cleanup callback
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Multi-tenancy
|
|
88
|
+
tenancy: {
|
|
89
|
+
resolution: "header", // "header" | "subdomain" | "path"
|
|
90
|
+
headerName: "x-tenant-id", // header name (when resolution is "header")
|
|
91
|
+
onResolve: async (tenantId) => ({}), // validate/load tenant — return null to reject
|
|
92
|
+
cacheTtlMs: 60_000, // LRU cache TTL (default: 60s, 0 to disable)
|
|
93
|
+
cacheMaxSize: 500, // max cached entries (default: 500)
|
|
94
|
+
exemptPaths: [], // extra paths that skip tenant resolution
|
|
95
|
+
rejectionStatus: 403, // 403 (default) or 404
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Job status endpoint
|
|
99
|
+
jobs: {
|
|
100
|
+
statusEndpoint: true, // default: false
|
|
101
|
+
auth: "userAuth", // "userAuth" | "none" | MiddlewareHandler[]
|
|
102
|
+
roles: ["admin"], // require roles (works with userAuth)
|
|
103
|
+
allowedQueues: ["export"], // whitelist — empty = nothing exposed
|
|
104
|
+
scopeToUser: false, // when true with userAuth, users see only their own jobs
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Security
|
|
108
|
+
security: {
|
|
109
|
+
cors: ["https://myapp.com"], // default: "*"
|
|
110
|
+
rateLimit: { windowMs: 60_000, max: 100 }, // default: 100 req/min
|
|
111
|
+
bearerAuth: true, // default: true — set false to disable, or { bypass: ["/my-public-route"] }
|
|
112
|
+
botProtection: {
|
|
113
|
+
fingerprintRateLimit: true, // rate-limit by HTTP fingerprint (IP-rotation resistant). default: false
|
|
114
|
+
blockList: ["198.51.100.0/24"], // IPv4 CIDRs or exact IPs to block with 403. default: []
|
|
115
|
+
},
|
|
116
|
+
headers: { // optional — additional security headers via Hono secureHeaders
|
|
117
|
+
contentSecurityPolicy: "default-src 'self'", // CSP header value
|
|
118
|
+
permissionsPolicy: "camera=(), microphone=()", // Permissions-Policy header value
|
|
119
|
+
},
|
|
120
|
+
trustProxy: 1, // default: false — see "Trusted Proxy" section below
|
|
121
|
+
csrf: { // opt-in CSRF protection for cookie-based auth
|
|
122
|
+
enabled: true, // default: false
|
|
123
|
+
exemptPaths: ["/webhooks/*"], // additional exempt paths (OAuth callbacks auto-exempt)
|
|
124
|
+
checkOrigin: true, // validate Origin header against CORS origins (default: true)
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Extra middleware injected after identify, before route matching
|
|
129
|
+
middleware: [],
|
|
130
|
+
|
|
131
|
+
// Connections & store routing (all optional — shown with defaults)
|
|
132
|
+
db: {
|
|
133
|
+
mongo: "single", // "single" | "separate" | false
|
|
134
|
+
redis: true, // false to skip auto-connect
|
|
135
|
+
sqlite: undefined, // absolute path to .db file — required when any store is "sqlite"
|
|
136
|
+
auth: "mongo", // "mongo" | "sqlite" | "memory" — which built-in auth adapter to use
|
|
137
|
+
sessions: "redis", // "redis" | "mongo" | "sqlite" | "memory"
|
|
138
|
+
oauthState: "redis", // default: follows sessions
|
|
139
|
+
cache: "redis", // global default for cacheResponse (overridable per-route)
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Server
|
|
143
|
+
port: 3000, // default: process.env.PORT ?? 3000
|
|
144
|
+
workersDir: import.meta.dir + "/workers", // auto-imports all .ts files after server starts
|
|
145
|
+
enableWorkers: true, // default: true — set false to disable auto-loading
|
|
146
|
+
|
|
147
|
+
// WebSocket (see WebSocket section for full examples)
|
|
148
|
+
ws: {
|
|
149
|
+
handler: { ... }, // override open/message/close/drain handlers
|
|
150
|
+
upgradeHandler: async (req, server) => { ... }, // replace default cookie-JWT upgrade logic
|
|
151
|
+
onRoomSubscribe(ws, room) { return true; }, // gate room subscriptions; can be async
|
|
152
|
+
maxMessageSize: 65_536, // default: 65536 (64 KB) — close connection on oversized messages
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## Configuration
|
|
2
|
+
|
|
3
|
+
`createServer` / `createApp` accept a config object with these top-level keys:
|
|
4
|
+
|
|
5
|
+
| Key | Purpose |
|
|
6
|
+
|-----|---------|
|
|
7
|
+
| `routesDir` | **(required)** Path to auto-discovered route files |
|
|
8
|
+
| `app` | App name and version (shown in docs) |
|
|
9
|
+
| `auth` | Roles, OAuth, email verification, MFA, refresh tokens, rate limiting, account deletion |
|
|
10
|
+
| `db` | Connection and store routing — mongo, redis, sqlite, sessions, cache, auth adapter |
|
|
11
|
+
| `security` | CORS, bearer auth, rate limiting, bot protection, CSRF |
|
|
12
|
+
| `tenancy` | Multi-tenant resolution (header/subdomain/path) |
|
|
13
|
+
| `jobs` | Job status REST endpoint config |
|
|
14
|
+
| `ws` | WebSocket handler and upgrade overrides |
|
|
15
|
+
| `middleware` | Extra global middleware array |
|
|
16
|
+
| `modelSchemas` | Schema auto-discovery paths |
|
|
17
|
+
| `port`, `workersDir`, `enableWorkers` | Server options |
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
## Full Configuration Example
|
|
2
|
+
|
|
3
|
+
For production apps, break config into its own file. Here's a real-world setup with MongoDB, Redis, OAuth, and email verification:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// src/config/index.ts
|
|
7
|
+
import path from "path";
|
|
8
|
+
import {
|
|
9
|
+
type CreateServerConfig,
|
|
10
|
+
type AppMeta,
|
|
11
|
+
type AuthConfig,
|
|
12
|
+
type DbConfig,
|
|
13
|
+
type SecurityConfig,
|
|
14
|
+
type ModelSchemasConfig,
|
|
15
|
+
} from "@lastshotlabs/bunshot";
|
|
16
|
+
|
|
17
|
+
const app: AppMeta = {
|
|
18
|
+
name: "My App",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const db: DbConfig = {
|
|
23
|
+
mongo: "single", // "single" | "separate" | false
|
|
24
|
+
redis: true, // false to skip Redis
|
|
25
|
+
sessions: "redis", // "redis" | "mongo" | "sqlite" | "memory"
|
|
26
|
+
cache: "memory", // default store for cacheResponse
|
|
27
|
+
auth: "mongo", // "mongo" | "sqlite" | "memory"
|
|
28
|
+
oauthState: "memory", // where to store OAuth state tokens
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const auth: AuthConfig = {
|
|
32
|
+
roles: ["admin", "user"],
|
|
33
|
+
defaultRole: "user",
|
|
34
|
+
primaryField: "email",
|
|
35
|
+
rateLimit: { store: "redis" },
|
|
36
|
+
emailVerification: {
|
|
37
|
+
required: true,
|
|
38
|
+
tokenExpiry: 60 * 60, // 1 hour
|
|
39
|
+
onSend: async (email, token) => {
|
|
40
|
+
// send verification email using any provider (Resend, SES, etc.)
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
oauth: {
|
|
44
|
+
postRedirect: "http://localhost:5175/oauth/callback",
|
|
45
|
+
providers: {
|
|
46
|
+
google: {
|
|
47
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
48
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
49
|
+
redirectUri: `http://localhost:${process.env.PORT ?? 3000}/auth/google/callback`,
|
|
50
|
+
},
|
|
51
|
+
apple: {
|
|
52
|
+
clientId: process.env.APPLE_CLIENT_ID!,
|
|
53
|
+
teamId: process.env.APPLE_TEAM_ID!,
|
|
54
|
+
keyId: process.env.APPLE_KEY_ID!,
|
|
55
|
+
privateKey: process.env.APPLE_PRIVATE_KEY!,
|
|
56
|
+
redirectUri: `http://localhost:${process.env.PORT ?? 3000}/auth/apple/callback`,
|
|
57
|
+
},
|
|
58
|
+
microsoft: {
|
|
59
|
+
tenantId: process.env.MICROSOFT_TENANT_ID!,
|
|
60
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
61
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
62
|
+
redirectUri: `http://localhost:${process.env.PORT ?? 3000}/auth/microsoft/callback`,
|
|
63
|
+
},
|
|
64
|
+
github: {
|
|
65
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
66
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
67
|
+
redirectUri: `http://localhost:${process.env.PORT ?? 3000}/auth/github/callback`,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const security: SecurityConfig = {
|
|
74
|
+
bearerAuth: true,
|
|
75
|
+
cors: ["*", "http://localhost:5173"],
|
|
76
|
+
botProtection: { fingerprintRateLimit: true },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const modelSchemas: ModelSchemasConfig = {
|
|
80
|
+
registration: "auto",
|
|
81
|
+
paths: [path.join(import.meta.dir, "../schemas/*.ts")],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const appConfig: CreateServerConfig = {
|
|
85
|
+
app,
|
|
86
|
+
routesDir: path.join(import.meta.dir, "../routes"),
|
|
87
|
+
workersDir: path.join(import.meta.dir, "../workers"),
|
|
88
|
+
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
|
|
89
|
+
db,
|
|
90
|
+
auth,
|
|
91
|
+
security,
|
|
92
|
+
modelSchemas,
|
|
93
|
+
middleware: [/* your global middleware here */],
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Every field above is optional except `routesDir`. See the [Configuration](#configuration) section for the full reference.
|
|
98
|
+
|
|
99
|
+
### Built-in endpoints
|
|
100
|
+
|
|
101
|
+
| Endpoint | Description |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `POST /auth/register` | Create account, returns JWT |
|
|
104
|
+
| `POST /auth/login` | Login, returns JWT (includes `emailVerified` when verification is configured) |
|
|
105
|
+
| `POST /auth/logout` | Invalidates the current session only |
|
|
106
|
+
| `GET /auth/me` | Returns current user's `userId`, `email`, `emailVerified`, and `googleLinked` (requires login) |
|
|
107
|
+
| `POST /auth/set-password` | Set or update password (requires login) |
|
|
108
|
+
| `GET /auth/sessions` | List active sessions with metadata — IP, user-agent, timestamps (requires login) |
|
|
109
|
+
| `DELETE /auth/sessions/:sessionId` | Revoke a specific session by ID (requires login) |
|
|
110
|
+
| `POST /auth/verify-email` | Verify email with token (when `emailVerification` is configured) |
|
|
111
|
+
| `POST /auth/resend-verification` | Resend verification email (requires credentials, when `emailVerification` is configured) |
|
|
112
|
+
| `POST /auth/forgot-password` | Request a password reset email (when `passwordReset` is configured) |
|
|
113
|
+
| `POST /auth/reset-password` | Reset password using a token from the reset email (when `passwordReset` is configured) |
|
|
114
|
+
| `GET /health` | Health check |
|
|
115
|
+
| `GET /docs` | Scalar API docs UI |
|
|
116
|
+
| `GET /openapi.json` | OpenAPI spec |
|
|
117
|
+
| `WS /ws` | WebSocket endpoint (cookie-JWT auth) |
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
## Full Configuration Example
|
|
2
|
+
|
|
3
|
+
For production apps, break config into its own file with MongoDB, Redis, OAuth, and email verification. See the [Configuration](#configuration) section for the full reference.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// src/config/index.ts
|
|
7
|
+
import { type CreateServerConfig } from "@lastshotlabs/bunshot";
|
|
8
|
+
|
|
9
|
+
export const appConfig: CreateServerConfig = {
|
|
10
|
+
app: { name: "My App", version: "1.0.0" },
|
|
11
|
+
routesDir: import.meta.dir + "/routes",
|
|
12
|
+
workersDir: import.meta.dir + "/workers",
|
|
13
|
+
db: { mongo: "single", redis: true, sessions: "redis", cache: "memory", auth: "mongo" },
|
|
14
|
+
auth: { roles: ["admin", "user"], defaultRole: "user", primaryField: "email" },
|
|
15
|
+
security: { bearerAuth: true, cors: ["*"] },
|
|
16
|
+
};
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Built-in endpoints
|
|
20
|
+
|
|
21
|
+
| Endpoint | Description |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `POST /auth/register` | Create account, returns JWT |
|
|
24
|
+
| `POST /auth/login` | Login, returns JWT |
|
|
25
|
+
| `POST /auth/logout` | Invalidates the current session |
|
|
26
|
+
| `GET /auth/me` | Current user profile |
|
|
27
|
+
| `GET /health` | Health check |
|
|
28
|
+
| `GET /docs` | Scalar API docs UI |
|
|
29
|
+
| `GET /openapi.json` | OpenAPI spec |
|
|
30
|
+
| `WS /ws` | WebSocket endpoint |
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
## Documentation Generation
|
|
2
|
+
|
|
3
|
+
Bunshot ships its documentation as modular markdown sections that you can pull into your own project's README.
|
|
4
|
+
|
|
5
|
+
### Setup
|
|
6
|
+
|
|
7
|
+
Create a `docs/` directory in your project with a config and build script:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
my-app/
|
|
11
|
+
docs/
|
|
12
|
+
readme.config.json
|
|
13
|
+
build-readme.ts
|
|
14
|
+
sections/
|
|
15
|
+
intro/
|
|
16
|
+
full.md
|
|
17
|
+
my-api/
|
|
18
|
+
full.md
|
|
19
|
+
overview.md
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Config — `docs/readme.config.json`
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"output": "../README.md",
|
|
27
|
+
"separator": "---",
|
|
28
|
+
"sections": [
|
|
29
|
+
{ "topic": "intro", "default": "full", "separator": false },
|
|
30
|
+
{ "topic": "my-api", "default": "full" },
|
|
31
|
+
{ "topic": "bunshot-auth", "file": "@lastshotlabs/bunshot/docs/auth-flow/overview.md" },
|
|
32
|
+
{ "topic": "bunshot-config", "file": "@lastshotlabs/bunshot/docs/configuration/full.md" }
|
|
33
|
+
],
|
|
34
|
+
"profiles": {
|
|
35
|
+
"short": {
|
|
36
|
+
"my-api": "overview"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Section entries:**
|
|
43
|
+
|
|
44
|
+
| Field | Description |
|
|
45
|
+
|-------|-------------|
|
|
46
|
+
| `topic` | Section identifier. Maps to `sections/{topic}/` directory when no `file` is specified. |
|
|
47
|
+
| `default` | Variant to use: `"full"` or `"overview"`. Falls back to `"full"` if the requested variant doesn't exist. |
|
|
48
|
+
| `file` | Explicit file path. Supports relative paths (`sections/header.md`) and package paths (`@lastshotlabs/bunshot/docs/auth-flow/overview.md`). |
|
|
49
|
+
| `separator` | `true`/`false` — whether to insert `---` before this section. Defaults to `true` (except the first section). |
|
|
50
|
+
|
|
51
|
+
**Profiles** override specific sections' variants. Only list sections you want to change:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
"profiles": {
|
|
55
|
+
"short": { "my-api": "overview", "bunshot-auth": "overview" }
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Build script — `docs/build-readme.ts`
|
|
60
|
+
|
|
61
|
+
Copy this into your project:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const configPath = import.meta.dir + "/readme.config.json";
|
|
65
|
+
const config = await Bun.file(configPath).json();
|
|
66
|
+
const profile = Bun.argv[2];
|
|
67
|
+
const overrides: Record<string, string> = profile
|
|
68
|
+
? config.profiles?.[profile] ?? {}
|
|
69
|
+
: {};
|
|
70
|
+
const separator: string = config.separator ?? "---";
|
|
71
|
+
|
|
72
|
+
if (profile && !config.profiles?.[profile]) {
|
|
73
|
+
console.error(`Unknown profile: "${profile}". Available: ${Object.keys(config.profiles ?? {}).join(", ")}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveFilePath(file: string): string {
|
|
78
|
+
if (file.startsWith("./") || file.startsWith("/") || file.startsWith("../")) {
|
|
79
|
+
return import.meta.dir + "/" + file;
|
|
80
|
+
}
|
|
81
|
+
if (file.includes("/") && !file.startsWith("sections")) {
|
|
82
|
+
const resolved = import.meta.resolve(file);
|
|
83
|
+
return resolved.replace(/^file:\/\/\//, "");
|
|
84
|
+
}
|
|
85
|
+
return import.meta.dir + "/" + file;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const parts: string[] = [
|
|
89
|
+
"<!-- AUTO-GENERATED — edit docs/sections/, not this file. Run: bun run readme -->",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < config.sections.length; i++) {
|
|
93
|
+
const section = config.sections[i];
|
|
94
|
+
|
|
95
|
+
let filePath: string;
|
|
96
|
+
if (section.file) {
|
|
97
|
+
filePath = resolveFilePath(section.file);
|
|
98
|
+
} else {
|
|
99
|
+
const variant = overrides[section.topic] ?? section.default ?? "full";
|
|
100
|
+
const candidate = `${import.meta.dir}/sections/${section.topic}/${variant}.md`;
|
|
101
|
+
filePath = (await Bun.file(candidate).exists())
|
|
102
|
+
? candidate
|
|
103
|
+
: `${import.meta.dir}/sections/${section.topic}/full.md`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const content = (await Bun.file(filePath).text()).replace(/\r\n/g, "\n");
|
|
107
|
+
|
|
108
|
+
const useSeparator = section.separator !== undefined ? section.separator : i > 0;
|
|
109
|
+
if (useSeparator) parts.push(separator);
|
|
110
|
+
|
|
111
|
+
parts.push(content.trimEnd());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const outputPath = import.meta.dir + "/" + (config.output ?? "../README.md");
|
|
115
|
+
await Bun.write(outputPath, parts.join("\n\n") + "\n");
|
|
116
|
+
console.log(
|
|
117
|
+
`README.md compiled (${config.sections.length} sections${profile ? `, profile: ${profile}` : ""})`
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Add to package.json
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
"scripts": {
|
|
125
|
+
"readme": "bun docs/build-readme.ts",
|
|
126
|
+
"readme:short": "bun docs/build-readme.ts short"
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Available bunshot sections
|
|
131
|
+
|
|
132
|
+
Pull any of these into your project's README via `"file": "@lastshotlabs/bunshot/docs/{section}/{variant}.md"`:
|
|
133
|
+
|
|
134
|
+
| Section | Variants |
|
|
135
|
+
|---------|----------|
|
|
136
|
+
| `quick-start` | `full` |
|
|
137
|
+
| `stack` | `full` |
|
|
138
|
+
| `cli` | `full` |
|
|
139
|
+
| `installation` | `full` |
|
|
140
|
+
| `configuration-example` | `full`, `overview` |
|
|
141
|
+
| `adding-routes` | `full`, `overview` |
|
|
142
|
+
| `mongodb-connections` | `full`, `overview` |
|
|
143
|
+
| `adding-models` | `full`, `overview` |
|
|
144
|
+
| `jobs` | `full`, `overview` |
|
|
145
|
+
| `websocket` | `full`, `overview` |
|
|
146
|
+
| `websocket-rooms` | `full`, `overview` |
|
|
147
|
+
| `adding-middleware` | `full` |
|
|
148
|
+
| `response-caching` | `full`, `overview` |
|
|
149
|
+
| `extending-context` | `full` |
|
|
150
|
+
| `configuration` | `full`, `overview` |
|
|
151
|
+
| `running-without-redis` | `full` |
|
|
152
|
+
| `running-without-redis-or-mongodb` | `full` |
|
|
153
|
+
| `auth-flow` | `full`, `overview` |
|
|
154
|
+
| `roles` | `full`, `overview` |
|
|
155
|
+
| `multi-tenancy` | `full`, `overview` |
|
|
156
|
+
| `oauth` | `full`, `overview` |
|
|
157
|
+
| `peer-dependencies` | `full` |
|
|
158
|
+
| `environment-variables` | `full` |
|
|
159
|
+
| `exports` | `full` |
|
|
160
|
+
|
|
161
|
+
### Writing your own sections
|
|
162
|
+
|
|
163
|
+
Each section file is self-contained markdown starting with a `## Heading`. Create `docs/sections/{topic}/full.md` and optionally `overview.md`:
|
|
164
|
+
|
|
165
|
+
```markdown
|
|
166
|
+
## My Feature
|
|
167
|
+
|
|
168
|
+
Description and code examples here...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The `---` separators between sections are inserted by the build script — don't include them in section files.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
## Environment Variables
|
|
2
|
+
|
|
3
|
+
```env
|
|
4
|
+
NODE_ENV=development
|
|
5
|
+
PORT=...
|
|
6
|
+
|
|
7
|
+
# MongoDB (single connection — used by connectMongo())
|
|
8
|
+
MONGO_USER_DEV=...
|
|
9
|
+
MONGO_PW_DEV=...
|
|
10
|
+
MONGO_HOST_DEV=...
|
|
11
|
+
MONGO_DB_DEV=...
|
|
12
|
+
MONGO_USER_PROD=...
|
|
13
|
+
MONGO_PW_PROD=...
|
|
14
|
+
MONGO_HOST_PROD=...
|
|
15
|
+
MONGO_DB_PROD=...
|
|
16
|
+
|
|
17
|
+
# MongoDB auth connection (separate server — used by connectAuthMongo())
|
|
18
|
+
# Only needed when running auth on a different cluster from app data
|
|
19
|
+
MONGO_AUTH_USER_DEV=...
|
|
20
|
+
MONGO_AUTH_PW_DEV=...
|
|
21
|
+
MONGO_AUTH_HOST_DEV=...
|
|
22
|
+
MONGO_AUTH_DB_DEV=...
|
|
23
|
+
MONGO_AUTH_USER_PROD=...
|
|
24
|
+
MONGO_AUTH_PW_PROD=...
|
|
25
|
+
MONGO_AUTH_HOST_PROD=...
|
|
26
|
+
MONGO_AUTH_DB_PROD=...
|
|
27
|
+
|
|
28
|
+
# Redis
|
|
29
|
+
REDIS_HOST_DEV=host:port
|
|
30
|
+
REDIS_USER_DEV=...
|
|
31
|
+
REDIS_PW_DEV=...
|
|
32
|
+
REDIS_HOST_PROD=host:port
|
|
33
|
+
REDIS_USER_PROD=...
|
|
34
|
+
REDIS_PW_PROD=...
|
|
35
|
+
|
|
36
|
+
# JWT
|
|
37
|
+
JWT_SECRET_DEV=...
|
|
38
|
+
JWT_SECRET_PROD=...
|
|
39
|
+
|
|
40
|
+
# Bearer API key (required on every non-bypassed request)
|
|
41
|
+
BEARER_TOKEN_DEV=...
|
|
42
|
+
BEARER_TOKEN_PROD=...
|
|
43
|
+
|
|
44
|
+
# Logging (optional — defaults to on in dev)
|
|
45
|
+
LOGGING_VERBOSE=true
|
|
46
|
+
|
|
47
|
+
# OAuth (only needed if using oauthProviders)
|
|
48
|
+
GOOGLE_CLIENT_ID=...
|
|
49
|
+
GOOGLE_CLIENT_SECRET=...
|
|
50
|
+
|
|
51
|
+
APPLE_CLIENT_ID=...
|
|
52
|
+
APPLE_TEAM_ID=...
|
|
53
|
+
APPLE_KEY_ID=...
|
|
54
|
+
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
|
|
55
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
## Exports
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
// Server factory
|
|
6
|
+
createServer, createApp,
|
|
7
|
+
|
|
8
|
+
// DB
|
|
9
|
+
connectMongo, connectAuthMongo, connectAppMongo, disconnectMongo,
|
|
10
|
+
authConnection, appConnection, mongoose,
|
|
11
|
+
connectRedis, disconnectRedis, getRedis,
|
|
12
|
+
|
|
13
|
+
// Jobs
|
|
14
|
+
createQueue, createWorker,
|
|
15
|
+
type Job,
|
|
16
|
+
|
|
17
|
+
// WebSocket
|
|
18
|
+
websocket, createWsUpgradeHandler, publish,
|
|
19
|
+
subscribe, unsubscribe, getSubscriptions, handleRoomActions,
|
|
20
|
+
getRooms, getRoomSubscribers,
|
|
21
|
+
|
|
22
|
+
// Auth utilities
|
|
23
|
+
signToken, verifyToken,
|
|
24
|
+
createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount,
|
|
25
|
+
evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions,
|
|
26
|
+
setRefreshToken, getSessionByRefreshToken, rotateRefreshToken, // refresh token management
|
|
27
|
+
createVerificationToken, getVerificationToken, deleteVerificationToken, // email verification tokens
|
|
28
|
+
createResetToken, consumeResetToken, setPasswordResetStore, // password reset tokens
|
|
29
|
+
createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore, // MFA challenge tokens
|
|
30
|
+
storeOAuthCode, consumeOAuthCode, setOAuthCodeStore, // OAuth one-time authorization codes
|
|
31
|
+
bustAuthLimit, trackAttempt, isLimited, clearMemoryRateLimitStore, // auth rate limiting — use in custom routes or admin unlocks
|
|
32
|
+
buildFingerprint, // HTTP fingerprint hash (IP-independent) — use in custom bot detection logic
|
|
33
|
+
sqliteAuthAdapter, setSqliteDb, startSqliteCleanup, // SQLite backend (persisted)
|
|
34
|
+
memoryAuthAdapter, clearMemoryStore, // in-memory backend (ephemeral)
|
|
35
|
+
setUserRoles, addUserRole, removeUserRole, // app-wide role management
|
|
36
|
+
getTenantRoles, setTenantRoles, addTenantRole, removeTenantRole, // tenant-scoped role management
|
|
37
|
+
type AuthAdapter, type OAuthProfile, type OAuthProviderConfig, type MfaChallengeData,
|
|
38
|
+
type AuthRateLimitConfig, type BotProtectionConfig, type BotProtectionOptions,
|
|
39
|
+
type LimitOpts, type RateLimitOptions,
|
|
40
|
+
type SessionMetadata, type SessionInfo, type RefreshResult,
|
|
41
|
+
|
|
42
|
+
// Tenancy
|
|
43
|
+
createTenant, deleteTenant, getTenant, listTenants, // tenant provisioning (MongoDB)
|
|
44
|
+
invalidateTenantCache, // invalidate LRU cache entry
|
|
45
|
+
type TenantInfo, type CreateTenantOptions,
|
|
46
|
+
type TenancyConfig, type TenantConfig,
|
|
47
|
+
|
|
48
|
+
// Middleware
|
|
49
|
+
bearerAuth, identify, userAuth, rateLimit,
|
|
50
|
+
botProtection, // CIDR blocklist + per-route bot protection
|
|
51
|
+
requireRole, // role-based access control (tenant-aware)
|
|
52
|
+
requireVerifiedEmail, // blocks unverified email addresses
|
|
53
|
+
cacheResponse, bustCache, bustCachePattern, setCacheStore, // response caching (tenant-namespaced)
|
|
54
|
+
|
|
55
|
+
// Crypto utilities
|
|
56
|
+
timingSafeEqual, // constant-time string comparison for secrets/hashes
|
|
57
|
+
sha256, // SHA-256 hash helper
|
|
58
|
+
|
|
59
|
+
// IP / proxy utilities
|
|
60
|
+
getClientIp, // centralized IP extraction — respects security.trustProxy setting
|
|
61
|
+
setTrustProxy, // configure trust level (called automatically by createApp)
|
|
62
|
+
|
|
63
|
+
// Utilities
|
|
64
|
+
HttpError, log, validate, createRouter, createRoute,
|
|
65
|
+
registerSchema, registerSchemas, // named OpenAPI schema registration
|
|
66
|
+
zodToMongoose, // Zod → Mongoose schema conversion
|
|
67
|
+
createDtoMapper, // DB document → API DTO mapper factory
|
|
68
|
+
type ZodToMongooseConfig, type ZodToMongooseRefConfig, type DtoMapperConfig,
|
|
69
|
+
getAppRoles, // returns the valid roles list configured at startup
|
|
70
|
+
|
|
71
|
+
// Constants
|
|
72
|
+
COOKIE_TOKEN, HEADER_USER_TOKEN,
|
|
73
|
+
COOKIE_REFRESH_TOKEN, HEADER_REFRESH_TOKEN, // refresh token cookie/header names
|
|
74
|
+
|
|
75
|
+
// Types
|
|
76
|
+
type AppEnv, type AppVariables,
|
|
77
|
+
type CreateServerConfig, type CreateAppConfig, type ModelSchemasConfig,
|
|
78
|
+
type DbConfig, type AppMeta, type AuthConfig, type OAuthConfig, type SecurityConfig,
|
|
79
|
+
type PrimaryField, type EmailVerificationConfig, type PasswordResetConfig,
|
|
80
|
+
type RefreshTokenConfig, type MfaConfig, type MfaEmailOtpConfig, type JobsConfig,
|
|
81
|
+
type AccountDeletionConfig, type PasswordPolicyConfig, type OAuthCodePayload,
|
|
82
|
+
type SocketData, type WsConfig,
|
|
83
|
+
} from "@lastshotlabs/bunshot";
|
|
84
|
+
|
|
85
|
+
// Jobs (separate entrypoint)
|
|
86
|
+
import {
|
|
87
|
+
createQueue, createWorker,
|
|
88
|
+
createCronWorker, cleanupStaleSchedulers, getRegisteredCronNames,
|
|
89
|
+
createDLQHandler,
|
|
90
|
+
type Job,
|
|
91
|
+
} from "@lastshotlabs/bunshot/queue";
|
|
92
|
+
```
|