@lastshotlabs/bunshot 0.0.25 → 0.0.27
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/dist/adapters/localStorage.js +20 -5
- package/dist/adapters/memoryAuth.d.ts +6 -0
- package/dist/adapters/memoryAuth.js +117 -2
- package/dist/adapters/mongoAuth.js +97 -1
- package/dist/adapters/sqliteAuth.d.ts +23 -0
- package/dist/adapters/sqliteAuth.js +153 -2
- package/dist/app.d.ts +105 -2
- package/dist/app.js +112 -9
- package/dist/index.d.ts +23 -4
- package/dist/index.js +13 -2
- package/dist/lib/HttpError.d.ts +2 -1
- package/dist/lib/HttpError.js +3 -1
- package/dist/lib/appConfig.d.ts +113 -0
- package/dist/lib/appConfig.js +38 -0
- package/dist/lib/auditLog.d.ts +6 -0
- package/dist/lib/auditLog.js +17 -0
- package/dist/lib/authAdapter.d.ts +71 -1
- package/dist/lib/authRateLimit.js +36 -0
- package/dist/lib/breachedPassword.d.ts +13 -0
- package/dist/lib/breachedPassword.js +48 -0
- package/dist/lib/captcha.d.ts +25 -0
- package/dist/lib/captcha.js +37 -0
- package/dist/lib/context.d.ts +5 -0
- package/dist/lib/credentialStuffing.d.ts +31 -0
- package/dist/lib/credentialStuffing.js +77 -0
- package/dist/lib/emailVerification.d.ts +6 -0
- package/dist/lib/emailVerification.js +46 -3
- package/dist/lib/jwks.d.ts +25 -0
- package/dist/lib/jwks.js +51 -0
- package/dist/lib/jwt.d.ts +15 -2
- package/dist/lib/jwt.js +92 -5
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +6 -0
- package/dist/lib/m2m.d.ts +29 -0
- package/dist/lib/m2m.js +48 -0
- package/dist/lib/mfaChallenge.d.ts +14 -1
- package/dist/lib/mfaChallenge.js +111 -6
- package/dist/lib/mongo.js +1 -1
- package/dist/lib/oauthCode.js +23 -18
- package/dist/lib/resetPassword.js +3 -1
- package/dist/lib/saml.d.ts +25 -0
- package/dist/lib/saml.js +64 -0
- package/dist/lib/scim.d.ts +44 -0
- package/dist/lib/scim.js +54 -0
- package/dist/lib/securityEvents.d.ts +28 -0
- package/dist/lib/securityEvents.js +26 -0
- package/dist/lib/session.d.ts +10 -0
- package/dist/lib/session.js +67 -5
- package/dist/lib/signing.js +5 -2
- package/dist/lib/suspension.d.ts +13 -0
- package/dist/lib/suspension.js +23 -0
- package/dist/lib/upload.d.ts +4 -0
- package/dist/lib/upload.js +26 -1
- package/dist/lib/uploadRegistry.d.ts +18 -0
- package/dist/lib/uploadRegistry.js +83 -0
- package/dist/lib/ws.js +7 -0
- package/dist/middleware/bearerAuth.js +1 -1
- package/dist/middleware/captcha.d.ts +10 -0
- package/dist/middleware/captcha.js +36 -0
- package/dist/middleware/csrf.js +8 -4
- package/dist/middleware/errorHandler.js +4 -1
- package/dist/middleware/identify.js +40 -13
- package/dist/middleware/requestSigning.js +6 -5
- package/dist/middleware/requireMfaSetup.js +2 -1
- package/dist/middleware/requireScope.d.ts +10 -0
- package/dist/middleware/requireScope.js +25 -0
- package/dist/middleware/requireStepUp.d.ts +18 -0
- package/dist/middleware/requireStepUp.js +29 -0
- package/dist/middleware/scimAuth.d.ts +8 -0
- package/dist/middleware/scimAuth.js +29 -0
- package/dist/middleware/webhookAuth.d.ts +1 -1
- package/dist/middleware/webhookAuth.js +6 -5
- package/dist/models/AuthUser.d.ts +7 -0
- package/dist/models/AuthUser.js +7 -0
- package/dist/models/M2MClient.d.ts +18 -0
- package/dist/models/M2MClient.js +18 -0
- package/dist/routes/auth.d.ts +3 -2
- package/dist/routes/auth.js +155 -16
- package/dist/routes/jobs.js +21 -3
- package/dist/routes/m2m.d.ts +2 -0
- package/dist/routes/m2m.js +72 -0
- package/dist/routes/metrics.d.ts +1 -0
- package/dist/routes/metrics.js +3 -0
- package/dist/routes/mfa.js +9 -1
- package/dist/routes/oauth.js +6 -0
- package/dist/routes/oidc.d.ts +2 -0
- package/dist/routes/oidc.js +29 -0
- package/dist/routes/passkey.d.ts +1 -0
- package/dist/routes/passkey.js +157 -0
- package/dist/routes/saml.d.ts +2 -0
- package/dist/routes/saml.js +86 -0
- package/dist/routes/scim.d.ts +2 -0
- package/dist/routes/scim.js +255 -0
- package/dist/routes/uploads.d.ts +13 -1
- package/dist/routes/uploads.js +98 -6
- package/dist/services/auth.d.ts +2 -0
- package/dist/services/auth.js +101 -22
- package/dist/services/mfa.js +2 -2
- package/dist/ws/index.js +2 -1
- package/docs/sections/auth-flow/full.md +790 -779
- package/docs/sections/auth-security-examples/full.md +23 -0
- package/docs/sections/metrics/full.md +6 -2
- package/docs/sections/passkey-login/full.md +90 -0
- package/docs/sections/passkey-login/overview.md +1 -0
- package/docs/sections/uploads/full.md +11 -2
- package/docs/sections/webhook-auth/full.md +1 -1
- package/docs/sections/websocket/full.md +12 -0
- package/package.json +3 -2
|
@@ -363,3 +363,26 @@ await createServer({
|
|
|
363
363
|
- `auth: "memory"` and `sessions: "memory"` use in-memory Maps. Import `clearMemoryStore()` from the package in your tests to reset state between test runs.
|
|
364
364
|
- Rate limits are set to 10 000 per minute rather than disabled entirely so the rate-limit code path is exercised (avoiding "works in dev, breaks in prod" surprises). The limit is just high enough that it never triggers during normal use.
|
|
365
365
|
- If you want to test email flows locally without a real provider, the commented-out block shows how to log the token to the console — paste it into the browser or a `curl` call directly.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## JWT Claims (iss, aud, iat)
|
|
370
|
+
|
|
371
|
+
By default Bunshot tokens include `sub`, `sid`, and `exp`. Enable standard JWT claims for multi-service deployments or compliance requirements:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
createApp({
|
|
375
|
+
auth: {
|
|
376
|
+
jwt: {
|
|
377
|
+
issuer: "https://auth.yourapp.com", // iss claim — who issued the token
|
|
378
|
+
audience: "your-api", // aud claim — who the token is for
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
When configured:
|
|
385
|
+
- **`iss`** and **`aud`** are included in every token and validated on every verification. Tokens from a different issuer or intended for a different audience are rejected.
|
|
386
|
+
- **`iat`** (issued-at) is always included once JWT config is set. Use it to detect token reuse or implement absolute expiry windows.
|
|
387
|
+
|
|
388
|
+
This is recommended for fintech and multi-tenant deployments where tokens from one service should not be accepted by another.
|
|
@@ -94,12 +94,16 @@ This counter is always present when metrics are enabled so you can alert on scra
|
|
|
94
94
|
|
|
95
95
|
### Security
|
|
96
96
|
|
|
97
|
-
Auth defaults to `"none"` for easy adoption. A production warning is logged when metrics are enabled without auth.
|
|
97
|
+
Auth defaults to `"none"` for easy adoption. A production warning is logged when metrics are enabled without auth.
|
|
98
|
+
|
|
99
|
+
**In production, `/metrics` returns `403` when `auth` is `"none"`** — the endpoint is hard-blocked to prevent accidental exposure of operational details (error rates, queue depths, tenant activity patterns). Configure `metrics.auth` before deploying:
|
|
98
100
|
|
|
99
101
|
```ts
|
|
100
102
|
metrics: {
|
|
101
103
|
enabled: true,
|
|
102
|
-
auth:
|
|
104
|
+
auth: "userAuth", // require a logged-in user
|
|
105
|
+
// or:
|
|
106
|
+
auth: [userAuth, requireRole("admin")], // custom middleware stack
|
|
103
107
|
}
|
|
104
108
|
```
|
|
105
109
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#### Passkey Login (Passwordless)
|
|
2
|
+
|
|
3
|
+
Passkeys (Windows Hello, Face ID, Touch ID) can be used as a **first-factor** passwordless login — no password required. The user authenticates directly with their biometric or device PIN.
|
|
4
|
+
|
|
5
|
+
This is separate from [WebAuthn as an MFA method](#webauthn--security-keys) (which requires password first). When both are configured, passkey login and WebAuthn MFA coexist independently.
|
|
6
|
+
|
|
7
|
+
> **Prerequisites:** Credentials must be registered with `residentKey: "required"` and `userVerification: "required"` to work as passkeys. bunshot sets both automatically on all WebAuthn registrations — credentials registered with an older version of the library will continue to work as MFA-only second factors but won't be usable for passwordless login.
|
|
8
|
+
|
|
9
|
+
##### Enable passkey login
|
|
10
|
+
|
|
11
|
+
Add `allowPasswordlessLogin: true` to the `mfa.webauthn` config:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
await createServer({
|
|
15
|
+
auth: {
|
|
16
|
+
mfa: {
|
|
17
|
+
webauthn: {
|
|
18
|
+
rpId: "example.com",
|
|
19
|
+
origin: "https://example.com",
|
|
20
|
+
allowPasswordlessLogin: true, // mounts /auth/passkey/* routes
|
|
21
|
+
passkeyMfaBypass: true, // default — passkey satisfies both factors
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
When `allowPasswordlessLogin` is `false` (the default), the `/auth/passkey/*` routes are not mounted at all — callers receive a `404`.
|
|
29
|
+
|
|
30
|
+
##### Endpoints
|
|
31
|
+
|
|
32
|
+
| Endpoint | Auth | Rate limit | Purpose |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `POST /auth/passkey/login-options` | None | 5 / min per IP | Get WebAuthn challenge options |
|
|
35
|
+
| `POST /auth/passkey/login` | None | 10 / 15 min per IP | Verify assertion, issue session |
|
|
36
|
+
|
|
37
|
+
##### Login flow
|
|
38
|
+
|
|
39
|
+
1. `POST /auth/passkey/login-options` with optional `{ email? }` → `{ options, passkeyToken }`
|
|
40
|
+
2. Client passes `options` to `startAuthentication(options)` from `@simplewebauthn/browser` — OS shows biometric / PIN prompt
|
|
41
|
+
3. `POST /auth/passkey/login` with `{ passkeyToken, assertionResponse }` → `{ token, userId, ... }`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { startAuthentication } from '@simplewebauthn/browser'
|
|
45
|
+
|
|
46
|
+
// Step 1 — get challenge
|
|
47
|
+
const { options, passkeyToken } = await fetch('/auth/passkey/login-options', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ email }), // optional hint
|
|
51
|
+
}).then(r => r.json())
|
|
52
|
+
|
|
53
|
+
// Step 2 — browser OS prompt
|
|
54
|
+
const assertionResponse = await startAuthentication(options)
|
|
55
|
+
|
|
56
|
+
// Step 3 — verify & get session
|
|
57
|
+
const result = await fetch('/auth/passkey/login', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ passkeyToken, assertionResponse }),
|
|
61
|
+
}).then(r => r.json())
|
|
62
|
+
// result: { token, userId, email? } or { mfaRequired, mfaToken, mfaMethods } when passkeyMfaBypass: false
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
##### MFA bypass
|
|
66
|
+
|
|
67
|
+
By default (`passkeyMfaBypass: true`), a passkey login that passes `userVerification: "required"` satisfies **both factors** — no subsequent TOTP or email OTP prompt, even if the user has MFA enabled. Biometric + device possession is equivalent to password + TOTP.
|
|
68
|
+
|
|
69
|
+
Set `passkeyMfaBypass: false` to require MFA after a passkey login — for apps with strict compliance requirements:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
mfa: {
|
|
73
|
+
webauthn: {
|
|
74
|
+
// ...
|
|
75
|
+
allowPasswordlessLogin: true,
|
|
76
|
+
passkeyMfaBypass: false, // require TOTP/OTP after passkey
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
##### Enumeration prevention
|
|
82
|
+
|
|
83
|
+
`POST /auth/passkey/login-options` always returns a valid-looking challenge regardless of whether the email exists or has registered credentials. It never returns a `404` or a distinguishable error — the shape and timing are identical for known and unknown emails.
|
|
84
|
+
|
|
85
|
+
##### Security
|
|
86
|
+
|
|
87
|
+
- `userVerification` defaults to `"required"` for passkey login — a bare hardware key tap without biometric or PIN is rejected. Set `mfa.webauthn.userVerification: "preferred"` to allow touch-only hardware keys (same tradeoff as "remember this device": proves possession, not identity)
|
|
88
|
+
- The `passkeyToken` is a 120-second single-use challenge token — it is consumed on the first verification attempt, so replay is not possible even within the TTL window
|
|
89
|
+
- Sign count is validated and updated on every successful assertion. A backward sign count logs a warning; set `strictSignCount: true` to reject it (possible cloned authenticator signal)
|
|
90
|
+
- Sessions created via passkey login are independent of the credential — revoking or deleting a credential does not invalidate existing sessions. Use explicit session revocation for that
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Passkeys (Windows Hello, Face ID, Touch ID) as a **passwordless first-factor** — no password required. Enable with `mfa.webauthn.allowPasswordlessLogin: true`. Mounts `POST /auth/passkey/login-options` and `POST /auth/passkey/login`. By default a verified passkey satisfies both factors (`passkeyMfaBypass: true`). Enumeration-safe: login-options always returns valid-looking challenge data.
|
|
@@ -123,12 +123,21 @@ When `presignedUrls` is set, two routes are mounted (default base path `/uploads
|
|
|
123
123
|
- `POST /uploads/presign` — returns a time-limited PUT URL
|
|
124
124
|
- `DELETE /uploads/:key` — deletes a stored file
|
|
125
125
|
|
|
126
|
+
**`POST /uploads/presign` request fields:**
|
|
127
|
+
|
|
128
|
+
| Field | Type | Description |
|
|
129
|
+
|-------|------|-------------|
|
|
130
|
+
| `key` | `string` | Storage key for the upload |
|
|
131
|
+
| `mimeType` | `string` (optional) | MIME type of the file. Certain dangerous types are rejected with `400 { error: "File type not allowed." }`: `application/x-executable`, `application/x-sh`, `application/x-msdownload`, `text/html`, `application/x-httpd-php`, `application/javascript`, `text/javascript` |
|
|
132
|
+
| `expirySeconds` | `number` (optional) | URL expiry in seconds |
|
|
133
|
+
| `maxBytes` | `number` (optional) | Maximum allowed file size in bytes (client-enforced via Content-Length). Defaults to 10MB. Maximum: 100MB. Returned in the response so clients can enforce the limit before uploading. |
|
|
134
|
+
|
|
126
135
|
```typescript
|
|
127
136
|
// 1. Client requests a presigned URL from your API
|
|
128
|
-
const { url, key } = await fetch("/uploads/presign", {
|
|
137
|
+
const { url, key, maxBytes } = await fetch("/uploads/presign", {
|
|
129
138
|
method: "POST",
|
|
130
139
|
headers: { "Content-Type": "application/json" },
|
|
131
|
-
body: JSON.stringify({ key: "photos/my-image.jpg", mimeType: "image/jpeg", expirySeconds: 300 }),
|
|
140
|
+
body: JSON.stringify({ key: "photos/my-image.jpg", mimeType: "image/jpeg", expirySeconds: 300, maxBytes: 5 * 1024 * 1024 }),
|
|
132
141
|
}).then(r => r.json());
|
|
133
142
|
|
|
134
143
|
// 2. Client uploads directly to storage (no server bandwidth used)
|
|
@@ -51,7 +51,7 @@ If the header value does **not** start with the configured prefix, the full valu
|
|
|
51
51
|
|--------|------|---------|-------------|
|
|
52
52
|
| `secret` | `string \| (c) => string \| Promise<string>` | — | Shared HMAC secret. Pass a function for dynamic resolution (e.g. per-tenant lookup). |
|
|
53
53
|
| `header` | `string` | `"x-webhook-signature"` | Header that carries the signature. |
|
|
54
|
-
| `algorithm` | `"sha256" \| "sha512"
|
|
54
|
+
| `algorithm` | `"sha256" \| "sha512"` | `"sha256"` | HMAC algorithm. |
|
|
55
55
|
| `prefix` | `string` | — | Strip this prefix before comparing (e.g. `"sha256="` for GitHub). |
|
|
56
56
|
| `timestamp` | `{ header, tolerance }` | — | Opt-in replay protection (see below). |
|
|
57
57
|
|
|
@@ -164,6 +164,18 @@ const older = await getMessageHistory("chat:general", { limit: 50, before: histo
|
|
|
164
164
|
|
|
165
165
|
Messages for non-configured rooms are silently ignored. Store errors are caught and logged (non-blocking) — message delivery is never interrupted by a persistence failure.
|
|
166
166
|
|
|
167
|
+
### Room Name Validation
|
|
168
|
+
|
|
169
|
+
Room names sent by clients via `subscribe` and `unsubscribe` actions are validated before any action is taken. Invalid room names are silently dropped — no error is sent to the client.
|
|
170
|
+
|
|
171
|
+
**Valid room name rules:**
|
|
172
|
+
- Characters: `a–z`, `A–Z`, `0–9`, `_`, `:`, `.`, `/`, `-`
|
|
173
|
+
- Length: 1–128 characters
|
|
174
|
+
|
|
175
|
+
Examples of valid names: `chat:general`, `user:123`, `tenant/room-1`, `notifications`
|
|
176
|
+
|
|
177
|
+
Room names containing characters outside this set (e.g. spaces, `@`, `#`, or Unicode) are silently ignored. This prevents injection of arbitrary pub/sub topic names from untrusted client input.
|
|
178
|
+
|
|
167
179
|
### Overriding the upgrade / auth handler
|
|
168
180
|
|
|
169
181
|
Replace the default cookie-JWT handshake entirely via `ws.upgradeHandler`. You must call `server.upgrade()` yourself and include `rooms: new Set()` in data:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lastshotlabs/bunshot",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
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",
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"@hono/zod-openapi": "1.2.2",
|
|
67
67
|
"@scalar/hono-api-reference": "0.10.0",
|
|
68
68
|
"arctic": "^3.7.0",
|
|
69
|
-
"jose": "6.2.0"
|
|
69
|
+
"jose": "6.2.0",
|
|
70
|
+
"samlify": "^2.8"
|
|
70
71
|
},
|
|
71
72
|
"peerDependencies": {
|
|
72
73
|
"hono": ">=4.12 <5",
|