@lastshotlabs/bunshot 0.0.16 → 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 +322 -16
- package/dist/adapters/memoryAuth.d.ts +3 -0
- package/dist/adapters/memoryAuth.js +48 -2
- package/dist/adapters/mongoAuth.js +39 -1
- package/dist/adapters/sqliteAuth.d.ts +3 -0
- package/dist/adapters/sqliteAuth.js +53 -0
- package/dist/app.d.ts +45 -2
- package/dist/app.js +79 -4
- package/dist/index.d.ts +14 -7
- package/dist/index.js +8 -4
- package/dist/lib/appConfig.d.ts +35 -0
- package/dist/lib/appConfig.js +10 -0
- package/dist/lib/authAdapter.d.ts +24 -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 +2 -0
- package/dist/lib/constants.js +2 -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.js +17 -4
- package/dist/lib/mfaChallenge.d.ts +23 -1
- package/dist/lib/mfaChallenge.js +151 -42
- 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/resetPassword.js +12 -16
- package/dist/lib/session.js +6 -4
- package/dist/lib/ws.js +5 -1
- 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 +14 -2
- 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.js +2 -3
- package/dist/models/AuthUser.d.ts +9 -0
- package/dist/models/AuthUser.js +9 -0
- package/dist/routes/auth.js +21 -9
- package/dist/routes/mfa.d.ts +5 -1
- package/dist/routes/mfa.js +221 -14
- package/dist/routes/oauth.js +274 -10
- 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 +10 -3
- package/dist/services/auth.d.ts +1 -0
- package/dist/services/auth.js +21 -5
- package/dist/services/mfa.d.ts +47 -0
- package/dist/services/mfa.js +276 -9
- package/dist/ws/index.js +3 -2
- package/docs/sections/auth-flow/full.md +180 -2
- package/docs/sections/configuration/full.md +20 -0
- package/docs/sections/configuration/overview.md +1 -1
- package/docs/sections/configuration-example/full.md +19 -1
- package/docs/sections/exports/full.md +11 -2
- package/docs/sections/multi-tenancy/full.md +5 -1
- package/docs/sections/oauth/full.md +80 -10
- package/docs/sections/oauth/overview.md +2 -2
- package/docs/sections/peer-dependencies/full.md +6 -2
- package/docs/sections/response-caching/full.md +3 -1
- package/docs/sections/websocket/full.md +4 -3
- package/docs/sections/websocket/overview.md +1 -1
- package/package.json +16 -4
|
@@ -157,6 +157,69 @@ This two-step flow ensures the `onSend` callback actually delivers emails before
|
|
|
157
157
|
- If email OTP is the only method: requires the account password in the `password` field
|
|
158
158
|
- Disabling the last MFA method turns off MFA entirely
|
|
159
159
|
|
|
160
|
+
### WebAuthn / Security Keys
|
|
161
|
+
|
|
162
|
+
Hardware security keys (YubiKey, etc.) and platform authenticators (Touch ID, Windows Hello) via the WebAuthn/FIDO2 standard. Users can register multiple keys and use them as an MFA method alongside TOTP and email OTP.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
await createServer({
|
|
166
|
+
auth: {
|
|
167
|
+
mfa: {
|
|
168
|
+
webauthn: {
|
|
169
|
+
rpId: "example.com", // Relying Party ID — your domain
|
|
170
|
+
origin: "https://example.com", // Expected origin(s)
|
|
171
|
+
rpName: "My App", // Display name (default: app name)
|
|
172
|
+
userVerification: "preferred", // "required" | "preferred" | "discouraged"
|
|
173
|
+
timeout: 60000, // Ceremony timeout in ms (default: 60000)
|
|
174
|
+
strictSignCount: false, // Reject when sign count goes backward (default: false — warn only)
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Requires `@simplewebauthn/server` peer dependency:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
bun add @simplewebauthn/server
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
If `mfa.webauthn` is configured but the dependency is missing, the server fails fast at startup with a clear error message.
|
|
188
|
+
|
|
189
|
+
#### Endpoints
|
|
190
|
+
|
|
191
|
+
| Endpoint | Auth | Purpose |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `POST /auth/mfa/webauthn/register-options` | userAuth | Generate registration options for `navigator.credentials.create()` |
|
|
194
|
+
| `POST /auth/mfa/webauthn/register` | userAuth | Verify attestation and store credential |
|
|
195
|
+
| `GET /auth/mfa/webauthn/credentials` | userAuth | List registered security keys |
|
|
196
|
+
| `DELETE /auth/mfa/webauthn/credentials/:credentialId` | userAuth | Remove a single key |
|
|
197
|
+
| `DELETE /auth/mfa/webauthn` | userAuth | Disable WebAuthn entirely |
|
|
198
|
+
|
|
199
|
+
#### Registration flow
|
|
200
|
+
|
|
201
|
+
1. `POST /auth/mfa/webauthn/register-options` → returns `{ options, registrationToken }`
|
|
202
|
+
2. Client passes `options` to `navigator.credentials.create()` — browser prompts user to tap/scan key
|
|
203
|
+
3. `POST /auth/mfa/webauthn/register` with `{ registrationToken, attestationResponse, name? }` → stores credential → returns recovery codes
|
|
204
|
+
|
|
205
|
+
#### Login flow with WebAuthn
|
|
206
|
+
|
|
207
|
+
1. `POST /auth/login` → `{ mfaRequired: true, mfaToken, mfaMethods: ["webauthn"], webauthnOptions: {...} }`
|
|
208
|
+
2. Client passes `webauthnOptions` to `navigator.credentials.get()` — browser prompts for key
|
|
209
|
+
3. `POST /auth/mfa/verify` with `{ mfaToken, webauthnResponse: {...} }` → creates session
|
|
210
|
+
|
|
211
|
+
The `webauthnOptions` object follows the WebAuthn spec — pass it directly to `navigator.credentials.get()`. The `webauthnResponse` is the full result from the browser API.
|
|
212
|
+
|
|
213
|
+
#### Credential removal
|
|
214
|
+
|
|
215
|
+
- Removing a spare key (other keys or MFA methods still active): no extra verification needed
|
|
216
|
+
- Removing the last credential of the last MFA method: requires TOTP code or password
|
|
217
|
+
- `DELETE /auth/mfa/webauthn` (disable all): always requires verification
|
|
218
|
+
|
|
219
|
+
#### Sign count validation
|
|
220
|
+
|
|
221
|
+
WebAuthn authenticators increment a sign count on each use to detect cloned keys. By default, a backward count logs a warning but allows authentication. Set `strictSignCount: true` to reject authentication when the count goes backward.
|
|
222
|
+
|
|
160
223
|
### Account Deletion
|
|
161
224
|
|
|
162
225
|
Enable `DELETE /auth/me` for user-initiated account deletion:
|
|
@@ -193,6 +256,25 @@ await createServer({
|
|
|
193
256
|
|
|
194
257
|
When `queued: true`, deletion is enqueued as a BullMQ job instead of running synchronously. The endpoint returns `202 Accepted` immediately. With `gracePeriod > 0`, the user can cancel via `POST /auth/cancel-deletion`.
|
|
195
258
|
|
|
259
|
+
### Password Policy
|
|
260
|
+
|
|
261
|
+
Configure password complexity requirements via `auth.passwordPolicy`. The policy applies to registration and password reset — login uses `min(1)` intentionally to avoid locking out users registered under older/weaker policies.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
await createServer({
|
|
265
|
+
auth: {
|
|
266
|
+
passwordPolicy: {
|
|
267
|
+
minLength: 10, // default: 8
|
|
268
|
+
requireLetter: true, // default: true — at least one a–z or A–Z
|
|
269
|
+
requireDigit: true, // default: true — at least one 0–9
|
|
270
|
+
requireSpecial: true, // default: false — at least one non-alphanumeric character
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
When not configured, the default policy requires 8+ characters with at least one letter and one digit.
|
|
277
|
+
|
|
196
278
|
### Protecting routes
|
|
197
279
|
|
|
198
280
|
```ts
|
|
@@ -297,6 +379,9 @@ All built-in auth endpoints are rate-limited out of the box with sensible defaul
|
|
|
297
379
|
| `POST /auth/resend-verification` | Identifier (email/username/phone) | Every attempt | 3 / hour |
|
|
298
380
|
| `POST /auth/forgot-password` | IP address | Every attempt | 5 / 15 min |
|
|
299
381
|
| `POST /auth/reset-password` | IP address | Every attempt | 10 / 15 min |
|
|
382
|
+
| `POST /auth/refresh` | IP address | Every attempt | 30 / min |
|
|
383
|
+
| `POST /auth/mfa/verify` | IP address | Every attempt | 10 / 15 min |
|
|
384
|
+
| `POST /auth/mfa/resend` | IP address | Every attempt | 5 / min |
|
|
300
385
|
|
|
301
386
|
Login is keyed by the **identifier being targeted** — an attacker rotating IPs to brute-force `alice@example.com` is blocked regardless of source IP. A successful login resets the counter so legitimate users aren't locked out.
|
|
302
387
|
|
|
@@ -338,14 +423,14 @@ Key formats: `login:{identifier}`, `register:{ip}`, `verify:{ip}`, `resend:{user
|
|
|
338
423
|
`trackAttempt` and `isLimited` are exported so you can apply the same Redis-backed rate limiting to any route in your app. They use the same store configured via `auth.rateLimit.store`.
|
|
339
424
|
|
|
340
425
|
```ts
|
|
341
|
-
import { trackAttempt, isLimited, bustAuthLimit } from "@lastshotlabs/bunshot";
|
|
426
|
+
import { trackAttempt, isLimited, bustAuthLimit, getClientIp } from "@lastshotlabs/bunshot";
|
|
342
427
|
|
|
343
428
|
// trackAttempt — increments the counter and returns true if now over the limit
|
|
344
429
|
// isLimited — checks without incrementing (read-only)
|
|
345
430
|
// bustAuthLimit — resets a key (e.g. on success or admin unlock)
|
|
346
431
|
|
|
347
432
|
router.post("/api/submit", async (c) => {
|
|
348
|
-
const ip = c
|
|
433
|
+
const ip = getClientIp(c);
|
|
349
434
|
const key = `submit:${ip}`;
|
|
350
435
|
|
|
351
436
|
if (await trackAttempt(key, { windowMs: 60 * 1000, max: 5 })) {
|
|
@@ -428,6 +513,49 @@ router.use("/api/submit", botProtection({ blockList: ["198.51.100.0/24"] }));
|
|
|
428
513
|
|
|
429
514
|
---
|
|
430
515
|
|
|
516
|
+
### Trusted Proxy
|
|
517
|
+
|
|
518
|
+
By default, Bunshot uses the socket-level IP address for all rate limiting and session metadata — the `X-Forwarded-For` header is **ignored entirely**. This prevents attackers from spoofing IPs to bypass rate limits.
|
|
519
|
+
|
|
520
|
+
If your app runs behind a reverse proxy (nginx, Cloudflare, AWS ALB), configure `security.trustProxy` so the framework reads the real client IP from the `X-Forwarded-For` chain:
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
await createServer({
|
|
524
|
+
security: {
|
|
525
|
+
trustProxy: 1, // trust 1 proxy hop — use the second-to-last IP in X-Forwarded-For
|
|
526
|
+
// trustProxy: 2, // behind 2 proxies (e.g. Cloudflare → ALB → app)
|
|
527
|
+
// trustProxy: false, // default — use socket IP, ignore XFF entirely
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
The number represents how many trusted proxy hops sit between your app and the internet. With `trustProxy: N`, the framework takes the Nth-from-right entry in the `X-Forwarded-For` chain, skipping the N trusted proxies.
|
|
533
|
+
|
|
534
|
+
All rate limiting (auth, general, bot protection) and session metadata (IP in `GET /auth/sessions`) use the centralized `getClientIp(c)` utility, which respects this setting. It's also exported for use in your own routes:
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
import { getClientIp } from "@lastshotlabs/bunshot";
|
|
538
|
+
|
|
539
|
+
router.post("/api/action", async (c) => {
|
|
540
|
+
const ip = getClientIp(c); // respects trustProxy setting
|
|
541
|
+
// ...
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### JWT Secret Validation
|
|
546
|
+
|
|
547
|
+
JWT secrets are validated on first use. The framework throws a clear error if:
|
|
548
|
+
- The environment variable (`JWT_SECRET_DEV` or `JWT_SECRET_PROD`) is missing
|
|
549
|
+
- The secret is shorter than 32 characters
|
|
550
|
+
|
|
551
|
+
Generate a strong secret:
|
|
552
|
+
|
|
553
|
+
```bash
|
|
554
|
+
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
431
559
|
### Setting a password after social login
|
|
432
560
|
|
|
433
561
|
If a user signed up via Google or Apple and later wants to add a password, send an authenticated request to `POST /auth/set-password`:
|
|
@@ -454,3 +582,53 @@ const myAdapter: AuthAdapter = {
|
|
|
454
582
|
},
|
|
455
583
|
};
|
|
456
584
|
```
|
|
585
|
+
|
|
586
|
+
### CSRF Protection
|
|
587
|
+
|
|
588
|
+
Opt-in via `security.csrf` — protects cookie-authenticated browser clients against cross-site request forgery attacks. Mobile apps and SPAs using header-based auth (`x-user-token`) are not affected and do not need CSRF.
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
await createServer({
|
|
592
|
+
security: {
|
|
593
|
+
csrf: {
|
|
594
|
+
enabled: true,
|
|
595
|
+
// exemptPaths: ["/webhooks/*"], // additional exempt paths
|
|
596
|
+
// checkOrigin: true, // validate Origin header (default: true)
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**How it works:**
|
|
603
|
+
|
|
604
|
+
1. The first GET request sets a `csrf_token` cookie (non-HttpOnly, readable by JS)
|
|
605
|
+
2. The token is HMAC-SHA256 signed with the JWT secret to prevent forgery
|
|
606
|
+
3. For state-changing requests (POST/PUT/PATCH/DELETE), the client must send the cookie value back in the `x-csrf-token` header
|
|
607
|
+
4. The middleware validates the signature and compares the header to the cookie using timing-safe comparison
|
|
608
|
+
5. Requests without an auth cookie (`token`) skip validation — they are not vulnerable to CSRF
|
|
609
|
+
|
|
610
|
+
The CSRF cookie is refreshed on login, register, MFA verify, and OAuth exchange. It is cleared on logout.
|
|
611
|
+
|
|
612
|
+
**Client-side integration:**
|
|
613
|
+
|
|
614
|
+
```js
|
|
615
|
+
function getCsrfToken() {
|
|
616
|
+
return document.cookie
|
|
617
|
+
.split("; ")
|
|
618
|
+
.find(row => row.startsWith("csrf_token="))
|
|
619
|
+
?.split("=")[1];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Include on all state-changing requests
|
|
623
|
+
fetch("/api/resource", {
|
|
624
|
+
method: "POST",
|
|
625
|
+
credentials: "include",
|
|
626
|
+
headers: {
|
|
627
|
+
"Content-Type": "application/json",
|
|
628
|
+
"X-CSRF-Token": getCsrfToken(),
|
|
629
|
+
},
|
|
630
|
+
body: JSON.stringify(data),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// After login, read the NEW csrf_token value (it's refreshed on auth state changes)
|
|
634
|
+
```
|
|
@@ -43,6 +43,8 @@ await createServer({
|
|
|
43
43
|
resendVerification: { windowMs: 60 * 60 * 1000, max: 3 }, // default: 3 attempts / hour (per user)
|
|
44
44
|
forgotPassword: { windowMs: 15 * 60 * 1000, max: 5 }, // default: 5 attempts / 15 min (per IP)
|
|
45
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)
|
|
46
48
|
store: "redis", // default: "redis" when Redis is enabled, else "memory"
|
|
47
49
|
},
|
|
48
50
|
sessionPolicy: { // optional — session concurrency and metadata
|
|
@@ -51,9 +53,16 @@ await createServer({
|
|
|
51
53
|
includeInactiveSessions: false, // default: false — include expired/deleted sessions in GET /auth/sessions
|
|
52
54
|
trackLastActive: false, // default: false — update lastActiveAt on every auth'd request (adds one DB write)
|
|
53
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
|
+
},
|
|
54
62
|
oauth: {
|
|
55
63
|
providers: { google: { ... }, apple: { ... } }, // omit a provider to disable it
|
|
56
64
|
postRedirect: "/dashboard", // default: "/"
|
|
65
|
+
allowedRedirectUrls: ["https://myapp.com"], // optional — validate postRedirect against allowlist at startup
|
|
57
66
|
},
|
|
58
67
|
refreshTokens: { // optional — short-lived access + long-lived refresh tokens
|
|
59
68
|
accessTokenExpiry: 900, // default: 900 (15 min)
|
|
@@ -104,6 +113,16 @@ await createServer({
|
|
|
104
113
|
fingerprintRateLimit: true, // rate-limit by HTTP fingerprint (IP-rotation resistant). default: false
|
|
105
114
|
blockList: ["198.51.100.0/24"], // IPv4 CIDRs or exact IPs to block with 403. default: []
|
|
106
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
|
+
},
|
|
107
126
|
},
|
|
108
127
|
|
|
109
128
|
// Extra middleware injected after identify, before route matching
|
|
@@ -130,6 +149,7 @@ await createServer({
|
|
|
130
149
|
handler: { ... }, // override open/message/close/drain handlers
|
|
131
150
|
upgradeHandler: async (req, server) => { ... }, // replace default cookie-JWT upgrade logic
|
|
132
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
|
|
133
153
|
},
|
|
134
154
|
});
|
|
135
155
|
```
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
| `app` | App name and version (shown in docs) |
|
|
9
9
|
| `auth` | Roles, OAuth, email verification, MFA, refresh tokens, rate limiting, account deletion |
|
|
10
10
|
| `db` | Connection and store routing — mongo, redis, sqlite, sessions, cache, auth adapter |
|
|
11
|
-
| `security` | CORS, bearer auth, rate limiting, bot protection |
|
|
11
|
+
| `security` | CORS, bearer auth, rate limiting, bot protection, CSRF |
|
|
12
12
|
| `tenancy` | Multi-tenant resolution (header/subdomain/path) |
|
|
13
13
|
| `jobs` | Job status REST endpoint config |
|
|
14
14
|
| `ws` | WebSocket handler and upgrade overrides |
|
|
@@ -48,6 +48,24 @@ const auth: AuthConfig = {
|
|
|
48
48
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
49
49
|
redirectUri: `http://localhost:${process.env.PORT ?? 3000}/auth/google/callback`,
|
|
50
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
|
+
},
|
|
51
69
|
},
|
|
52
70
|
},
|
|
53
71
|
};
|
|
@@ -96,4 +114,4 @@ Every field above is optional except `routesDir`. See the [Configuration](#confi
|
|
|
96
114
|
| `GET /health` | Health check |
|
|
97
115
|
| `GET /docs` | Scalar API docs UI |
|
|
98
116
|
| `GET /openapi.json` | OpenAPI spec |
|
|
99
|
-
| `WS /ws` | WebSocket endpoint (cookie-JWT auth) |
|
|
117
|
+
| `WS /ws` | WebSocket endpoint (cookie-JWT auth) |
|
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
createVerificationToken, getVerificationToken, deleteVerificationToken, // email verification tokens
|
|
28
28
|
createResetToken, consumeResetToken, setPasswordResetStore, // password reset tokens
|
|
29
29
|
createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore, // MFA challenge tokens
|
|
30
|
-
|
|
30
|
+
storeOAuthCode, consumeOAuthCode, setOAuthCodeStore, // OAuth one-time authorization codes
|
|
31
|
+
bustAuthLimit, trackAttempt, isLimited, clearMemoryRateLimitStore, // auth rate limiting — use in custom routes or admin unlocks
|
|
31
32
|
buildFingerprint, // HTTP fingerprint hash (IP-independent) — use in custom bot detection logic
|
|
32
33
|
sqliteAuthAdapter, setSqliteDb, startSqliteCleanup, // SQLite backend (persisted)
|
|
33
34
|
memoryAuthAdapter, clearMemoryStore, // in-memory backend (ephemeral)
|
|
@@ -51,6 +52,14 @@ import {
|
|
|
51
52
|
requireVerifiedEmail, // blocks unverified email addresses
|
|
52
53
|
cacheResponse, bustCache, bustCachePattern, setCacheStore, // response caching (tenant-namespaced)
|
|
53
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
|
+
|
|
54
63
|
// Utilities
|
|
55
64
|
HttpError, log, validate, createRouter, createRoute,
|
|
56
65
|
registerSchema, registerSchemas, // named OpenAPI schema registration
|
|
@@ -69,7 +78,7 @@ import {
|
|
|
69
78
|
type DbConfig, type AppMeta, type AuthConfig, type OAuthConfig, type SecurityConfig,
|
|
70
79
|
type PrimaryField, type EmailVerificationConfig, type PasswordResetConfig,
|
|
71
80
|
type RefreshTokenConfig, type MfaConfig, type MfaEmailOtpConfig, type JobsConfig,
|
|
72
|
-
type AccountDeletionConfig,
|
|
81
|
+
type AccountDeletionConfig, type PasswordPolicyConfig, type OAuthCodePayload,
|
|
73
82
|
type SocketData, type WsConfig,
|
|
74
83
|
} from "@lastshotlabs/bunshot";
|
|
75
84
|
|
|
@@ -31,6 +31,10 @@ await createServer({
|
|
|
31
31
|
|
|
32
32
|
These paths skip tenant resolution by default: `/health`, `/docs`, `/openapi.json`, `/auth/` (auth is global — all tenants share a user pool). Add more via `exemptPaths`.
|
|
33
33
|
|
|
34
|
+
### `onResolve` is required in production
|
|
35
|
+
|
|
36
|
+
When `tenancy` is configured without an `onResolve` callback, tenant IDs from headers/subdomains/paths are trusted without validation — a cross-tenant access risk. **In production (`NODE_ENV=production`), the server will refuse to start** if `onResolve` is missing. In development, a warning is logged instead.
|
|
37
|
+
|
|
34
38
|
### Accessing tenant in routes
|
|
35
39
|
|
|
36
40
|
```ts
|
|
@@ -59,4 +63,4 @@ await deleteTenant("acme"); // soft-delete + invalidates resolu
|
|
|
59
63
|
When tenant context is present, rate limits and cache keys are automatically namespaced per-tenant — no code changes needed. Each tenant gets independent rate limit buckets and cache entries.
|
|
60
64
|
|
|
61
65
|
- Rate limit keys: `t:${tenantId}:ip:${ip}` (instead of `ip:${ip}`)
|
|
62
|
-
- Cache keys: `cache:${appName}:${tenantId}:${key}` (instead of `cache:${appName}:${key}`)
|
|
66
|
+
- Cache keys: `cache:${appName}:${tenantId}:${key}` (instead of `cache:${appName}:${key}`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Social Login (OAuth)
|
|
2
2
|
|
|
3
|
-
Pass `auth.oauth.providers` to `createServer` to enable Google and/or
|
|
3
|
+
Pass `auth.oauth.providers` to `createServer` to enable Google, Apple, Microsoft, and/or GitHub sign-in. Routes are mounted automatically for each configured provider.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
await createServer({
|
|
@@ -22,6 +22,17 @@ await createServer({
|
|
|
22
22
|
privateKey: process.env.APPLE_PRIVATE_KEY!, // PEM string
|
|
23
23
|
redirectUri: "https://myapp.com/auth/apple/callback",
|
|
24
24
|
},
|
|
25
|
+
microsoft: {
|
|
26
|
+
tenantId: process.env.MICROSOFT_TENANT_ID!, // "common", "organizations", "consumers", or tenant GUID
|
|
27
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
28
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
29
|
+
redirectUri: "https://myapp.com/auth/microsoft/callback",
|
|
30
|
+
},
|
|
31
|
+
github: {
|
|
32
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
33
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
34
|
+
redirectUri: "https://myapp.com/auth/github/callback",
|
|
35
|
+
},
|
|
25
36
|
},
|
|
26
37
|
},
|
|
27
38
|
},
|
|
@@ -34,22 +45,77 @@ await createServer({
|
|
|
34
45
|
|---|---|---|---|---|
|
|
35
46
|
| Google | `GET /auth/google` | `GET /auth/google/callback` | `GET /auth/google/link` | `DELETE /auth/google/link` |
|
|
36
47
|
| Apple | `GET /auth/apple` | `POST /auth/apple/callback` | `GET /auth/apple/link` | — |
|
|
48
|
+
| Microsoft | `GET /auth/microsoft` | `GET /auth/microsoft/callback` | `GET /auth/microsoft/link` | `DELETE /auth/microsoft/link` |
|
|
49
|
+
| GitHub | `GET /auth/github` | `GET /auth/github/callback` | `GET /auth/github/link` | `DELETE /auth/github/link` |
|
|
37
50
|
|
|
38
51
|
> Apple sends its callback as a **POST** with form data. Your server must be publicly reachable and the redirect URI must be registered in the Apple developer console.
|
|
39
52
|
|
|
53
|
+
> **Microsoft `tenantId` options:** `"common"` accepts any Microsoft account (personal + work/school), `"organizations"` accepts work/school accounts only, `"consumers"` accepts personal accounts only, or pass a specific tenant GUID to restrict to a single Azure AD tenant (recommended for company SSO).
|
|
54
|
+
|
|
55
|
+
> **GitHub:** Create an OAuth App (not a GitHub App) at [github.com/settings/developers](https://github.com/settings/developers). The `user:email` scope is requested to retrieve the user's verified email address, since the primary `/user` endpoint may not return it for users with private email settings.
|
|
56
|
+
|
|
57
|
+
Additionally, a shared code exchange endpoint is always mounted:
|
|
58
|
+
|
|
59
|
+
| Endpoint | Purpose |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `POST /auth/oauth/exchange` | Exchange one-time authorization code for session token |
|
|
62
|
+
|
|
40
63
|
### Flow
|
|
41
64
|
|
|
42
|
-
1. Client navigates to `GET /auth/google` (or `/auth/apple`)
|
|
65
|
+
1. Client navigates to `GET /auth/google` (or `/auth/apple`, `/auth/microsoft`, `/auth/github`)
|
|
43
66
|
2. Package redirects to the provider's OAuth page
|
|
44
67
|
3. Provider redirects (or POSTs) back to the callback URL
|
|
45
68
|
4. Package exchanges the code, fetches the user profile, and calls `authAdapter.findOrCreateByProvider`
|
|
46
|
-
5. A session is created
|
|
69
|
+
5. A session is created and a **one-time authorization code** is generated
|
|
70
|
+
6. User is redirected to `auth.oauth.postRedirect?code=<one-time-code>`
|
|
71
|
+
7. Client exchanges the code for a session token via `POST /auth/oauth/exchange`
|
|
72
|
+
|
|
73
|
+
> **Security:** The JWT is never exposed in the redirect URL. The one-time code expires after 60 seconds and can only be used once, preventing token leakage via browser history, server logs, or referrer headers.
|
|
74
|
+
|
|
75
|
+
#### Code exchange
|
|
76
|
+
|
|
77
|
+
After the OAuth redirect, the client must exchange the one-time code for a session token:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// Client-side
|
|
81
|
+
const res = await fetch("/auth/oauth/exchange", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({ code: new URLSearchParams(location.search).get("code") }),
|
|
85
|
+
});
|
|
86
|
+
const { token, userId, email, refreshToken } = await res.json();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The exchange endpoint sets session cookies automatically for browser clients. Mobile/SPA clients can use the JSON response directly. Rate limited to 20 requests per minute per IP.
|
|
90
|
+
|
|
91
|
+
| Field | Description |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `token` | Session JWT |
|
|
94
|
+
| `userId` | Authenticated user ID |
|
|
95
|
+
| `email` | User email (if available) |
|
|
96
|
+
| `refreshToken` | Refresh token (only when `auth.refreshTokens` is configured) |
|
|
97
|
+
|
|
98
|
+
### Redirect URL validation
|
|
99
|
+
|
|
100
|
+
Pass `auth.oauth.allowedRedirectUrls` to restrict where OAuth callbacks can redirect:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
auth: {
|
|
104
|
+
oauth: {
|
|
105
|
+
postRedirect: "/dashboard",
|
|
106
|
+
allowedRedirectUrls: ["https://myapp.com", "https://staging.myapp.com"],
|
|
107
|
+
providers: { ... },
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
When configured, the `postRedirect` value is validated against the allowlist at startup. If omitted, any redirect URL is accepted (not recommended for production).
|
|
47
113
|
|
|
48
114
|
### User storage
|
|
49
115
|
|
|
50
116
|
The default `mongoAuthAdapter` stores social users in `AuthUser` with a `providerIds` field (e.g. `["google:1234567890"]`). If no existing provider key is found, a new account is created — emails are never auto-linked. To connect a social identity to an existing credential account the user must explicitly use the link flow below.
|
|
51
117
|
|
|
52
|
-
**Email conflict handling:** If a user attempts to sign in via Google (or Apple) and the email returned by the provider already belongs to a credential-based account, `findOrCreateByProvider` throws `HttpError(409, ...)`. The OAuth callback catches this and redirects to `auth.oauth.postRedirect?error=<message>` so the client can display a helpful prompt (e.g. "An account with this email already exists — sign in with your password, then link Google from your account settings.").
|
|
118
|
+
**Email conflict handling:** If a user attempts to sign in via Google (or Apple/Microsoft/GitHub) and the email returned by the provider already belongs to a credential-based account, `findOrCreateByProvider` throws `HttpError(409, ...)`. The OAuth callback catches this and redirects to `auth.oauth.postRedirect?error=<message>` so the client can display a helpful prompt (e.g. "An account with this email already exists — sign in with your password, then link Google from your account settings.").
|
|
53
119
|
|
|
54
120
|
To support social login with a custom adapter, implement `findOrCreateByProvider`:
|
|
55
121
|
|
|
@@ -66,11 +132,13 @@ const myAdapter: AuthAdapter = {
|
|
|
66
132
|
|
|
67
133
|
### Linking a provider to an existing account
|
|
68
134
|
|
|
69
|
-
A logged-in user can link their account to a Google or
|
|
135
|
+
A logged-in user can link their account to a Google, Apple, Microsoft, or GitHub identity by navigating to the link route. This is the only way to associate a social login with an existing credential account — email matching is intentionally not done automatically.
|
|
70
136
|
|
|
71
137
|
```
|
|
72
|
-
GET /auth/google/link
|
|
73
|
-
GET /auth/apple/link
|
|
138
|
+
GET /auth/google/link (requires active session via cookie)
|
|
139
|
+
GET /auth/apple/link (requires active session via cookie)
|
|
140
|
+
GET /auth/microsoft/link (requires active session via cookie)
|
|
141
|
+
GET /auth/github/link (requires active session via cookie)
|
|
74
142
|
```
|
|
75
143
|
|
|
76
144
|
The link flow:
|
|
@@ -96,10 +164,12 @@ const myAdapter: AuthAdapter = {
|
|
|
96
164
|
|
|
97
165
|
### Unlinking a provider
|
|
98
166
|
|
|
99
|
-
A logged-in user can remove a linked Google identity via:
|
|
167
|
+
A logged-in user can remove a linked Google, Microsoft, or GitHub identity via:
|
|
100
168
|
|
|
101
169
|
```
|
|
102
|
-
DELETE /auth/google/link
|
|
170
|
+
DELETE /auth/google/link (requires active session via cookie)
|
|
171
|
+
DELETE /auth/microsoft/link (requires active session via cookie)
|
|
172
|
+
DELETE /auth/github/link (requires active session via cookie)
|
|
103
173
|
```
|
|
104
174
|
|
|
105
175
|
Returns `204 No Content` on success. All `google:*` entries are removed from the user's `providerIds`.
|
|
@@ -116,4 +186,4 @@ const myAdapter: AuthAdapter = {
|
|
|
116
186
|
await db.update(users).set({ providerIds: filtered }).where(eq(users.id, userId));
|
|
117
187
|
},
|
|
118
188
|
};
|
|
119
|
-
```
|
|
189
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Social Login (OAuth)
|
|
2
2
|
|
|
3
|
-
Pass `auth.oauth.providers` to enable Google and/or
|
|
3
|
+
Pass `auth.oauth.providers` to enable Google, Apple, Microsoft, and/or GitHub sign-in. Routes are mounted automatically for each configured provider.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
auth: {
|
|
@@ -13,4 +13,4 @@ auth: {
|
|
|
13
13
|
}
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Auto-mounted routes per provider: initiate (`GET /auth/{provider}`), callback, link to existing account (`GET /auth/{provider}/link`), and unlink (`DELETE /auth/{provider}/link`). Supports custom adapters via `findOrCreateByProvider`, `linkProvider`, and `unlinkProvider`.
|
|
16
|
+
Auto-mounted routes per provider: initiate (`GET /auth/{provider}`), callback, link to existing account (`GET /auth/{provider}/link`), and unlink (`DELETE /auth/{provider}/link`). After OAuth redirect, the client exchanges a one-time authorization code via `POST /auth/oauth/exchange` to receive the session token (the JWT is never exposed in the redirect URL). Supports custom adapters via `findOrCreateByProvider`, `linkProvider`, and `unlinkProvider`. Optionally restrict redirect URLs with `allowedRedirectUrls`.
|
|
@@ -31,6 +31,9 @@ bun add bullmq
|
|
|
31
31
|
|
|
32
32
|
# MFA / TOTP
|
|
33
33
|
bun add otpauth
|
|
34
|
+
|
|
35
|
+
# MFA / WebAuthn (security keys, Touch ID, Windows Hello)
|
|
36
|
+
bun add @simplewebauthn/server
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
| Package | Required version | When you need it |
|
|
@@ -38,6 +41,7 @@ bun add otpauth
|
|
|
38
41
|
| `mongoose` | `>=9.0 <10` | `db.auth: "mongo"`, `db.sessions: "mongo"`, or `db.cache: "mongo"` |
|
|
39
42
|
| `ioredis` | `>=5.0 <6` | `db.redis: true` (the default), or any store set to `"redis"` |
|
|
40
43
|
| `bullmq` | `>=5.0 <6` | Workers / queues |
|
|
41
|
-
| `otpauth` | `>=9.0 <10` | `auth.mfa` configuration |
|
|
44
|
+
| `otpauth` | `>=9.0 <10` | `auth.mfa` configuration (TOTP) |
|
|
45
|
+
| `@simplewebauthn/server` | `>=10.0.0` | `auth.mfa.webauthn` configuration |
|
|
42
46
|
|
|
43
|
-
If you're running fully on SQLite or memory (no Redis, no MongoDB), none of the optional peers are needed.
|
|
47
|
+
If you're running fully on SQLite or memory (no Redis, no MongoDB), none of the optional peers are needed.
|
|
@@ -100,6 +100,8 @@ router.put("/products/:id", userAuth, async (c) => {
|
|
|
100
100
|
|
|
101
101
|
Only 2xx responses are cached. Non-2xx responses pass through uncached. Omit `ttl` to cache indefinitely — the entry will persist until explicitly busted with `bustCache`.
|
|
102
102
|
|
|
103
|
+
**Header sanitization:** Security-sensitive response headers (`set-cookie`, `www-authenticate`, `authorization`, `x-csrf-token`, `proxy-authenticate`) are automatically stripped before caching to prevent session fixation or auth bypass via cached responses.
|
|
104
|
+
|
|
103
105
|
### Busting by pattern
|
|
104
106
|
|
|
105
107
|
When cache keys include variable parts (e.g. query params), use `bustCachePattern` to invalidate an entire logical group at once. It runs against all four stores — Redis (via SCAN), Mongo (via regex), SQLite (via LIKE), and Memory (via regex) — in parallel:
|
|
@@ -112,4 +114,4 @@ import { bustCachePattern } from "@lastshotlabs/bunshot";
|
|
|
112
114
|
await bustCachePattern(`balance:${userId}:*`);
|
|
113
115
|
```
|
|
114
116
|
|
|
115
|
-
The `*` wildcard is translated to a Redis glob, a Mongo/Memory regex, and a SQLite LIKE pattern automatically. Like `bustCache`, it silently skips any store that isn't connected, so it's safe to call in apps that only use one store.
|
|
117
|
+
The `*` wildcard is translated to a Redis glob, a Mongo/Memory regex, and a SQLite LIKE pattern automatically. Like `bustCache`, it silently skips any store that isn't connected, so it's safe to call in apps that only use one store.
|
|
@@ -8,8 +8,9 @@ The `/ws` endpoint is mounted automatically by `createServer`. No extra setup ne
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| Upgrade / auth | Reads `auth-token` cookie → verifies JWT → checks session → sets `ws.data.userId` |
|
|
10
10
|
| `open` | Logs connection, sends `{ event: "connected", id }` |
|
|
11
|
-
| `message` |
|
|
11
|
+
| `message` | Checks message size (closes with 1009 if exceeds `maxMessageSize`), handles room actions (see below), drops non-room messages unless custom handler provided |
|
|
12
12
|
| `close` | Clears `ws.data.rooms`, logs disconnection |
|
|
13
|
+
| `maxMessageSize` | 65 536 bytes (64 KB) — configurable via `ws.maxMessageSize` |
|
|
13
14
|
|
|
14
15
|
### Socket data (`SocketData`)
|
|
15
16
|
|
|
@@ -56,7 +57,7 @@ With no type parameter, `SocketData` defaults to `{ id, userId, rooms }` — the
|
|
|
56
57
|
|
|
57
58
|
### Overriding the message handler
|
|
58
59
|
|
|
59
|
-
Pass `ws.handler` to `createServer` to
|
|
60
|
+
Pass `ws.handler` to `createServer` to add custom message handling. Room action handling always runs first — your handler only receives non-room messages:
|
|
60
61
|
|
|
61
62
|
```ts
|
|
62
63
|
await createServer({
|
|
@@ -97,4 +98,4 @@ await createServer({
|
|
|
97
98
|
},
|
|
98
99
|
},
|
|
99
100
|
});
|
|
100
|
-
```
|
|
101
|
+
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## WebSocket
|
|
2
2
|
|
|
3
|
-
The `/ws` endpoint is mounted automatically by `createServer`. Default behavior: cookie-JWT auth on upgrade, room action handling,
|
|
3
|
+
The `/ws` endpoint is mounted automatically by `createServer`. Default behavior: cookie-JWT auth on upgrade, room action handling, message size enforcement (64 KB default). Non-room messages are dropped unless a custom handler is provided.
|
|
4
4
|
|
|
5
5
|
`SocketData` carries `id`, `userId`, and `rooms` per connection. Pass a type parameter to `createServer<T>` to extend with custom fields. Override `ws.handler` (open/message/close) and `ws.upgradeHandler` for custom behavior.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lastshotlabs/bunshot",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
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",
|
|
@@ -51,7 +51,14 @@
|
|
|
51
51
|
"dev": "bun --watch src/index.ts",
|
|
52
52
|
"start": "bun src/index.ts",
|
|
53
53
|
"readme": "bun docs/build-readme.ts",
|
|
54
|
-
"readme:npm": "bun docs/build-readme.ts npm"
|
|
54
|
+
"readme:npm": "bun docs/build-readme.ts npm",
|
|
55
|
+
"test": "bun test tests/unit tests/integration",
|
|
56
|
+
"test:coverage": "bun test --coverage tests/unit tests/integration",
|
|
57
|
+
"test:docker:up": "docker compose -f docker-compose.test.yml up -d --wait",
|
|
58
|
+
"test:docker:down": "docker compose -f docker-compose.test.yml down",
|
|
59
|
+
"test:docker": "bun test --config bunfig.docker.toml tests/docker/",
|
|
60
|
+
"test:all": "bun test tests/unit tests/integration && bun run test:docker",
|
|
61
|
+
"test:coverage:full": "bun run test:docker:up && bun test --coverage --config bunfig.ci.toml tests/unit tests/integration tests/docker; bun run test:docker:down"
|
|
55
62
|
},
|
|
56
63
|
"dependencies": {
|
|
57
64
|
"@hono/zod-openapi": "1.2.2",
|
|
@@ -65,7 +72,8 @@
|
|
|
65
72
|
"mongoose": ">=9.0 <10",
|
|
66
73
|
"ioredis": ">=5.0 <6",
|
|
67
74
|
"bullmq": ">=5.0 <6",
|
|
68
|
-
"otpauth": ">=9.0 <10"
|
|
75
|
+
"otpauth": ">=9.0 <10",
|
|
76
|
+
"@simplewebauthn/server": ">=10.0.0"
|
|
69
77
|
},
|
|
70
78
|
"peerDependenciesMeta": {
|
|
71
79
|
"mongoose": {
|
|
@@ -79,6 +87,9 @@
|
|
|
79
87
|
},
|
|
80
88
|
"otpauth": {
|
|
81
89
|
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"@simplewebauthn/server": {
|
|
92
|
+
"optional": true
|
|
82
93
|
}
|
|
83
94
|
},
|
|
84
95
|
"devDependencies": {
|
|
@@ -90,7 +101,8 @@
|
|
|
90
101
|
"otpauth": "^9.5.0",
|
|
91
102
|
"tsc-alias": "^1.8.16",
|
|
92
103
|
"typescript": "^5.9.3",
|
|
93
|
-
"zod": ">=4.0"
|
|
104
|
+
"zod": ">=4.0",
|
|
105
|
+
"@simplewebauthn/server": "^13.1.1"
|
|
94
106
|
},
|
|
95
107
|
"publishConfig": {
|
|
96
108
|
"access": "public"
|