@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.
Files changed (108) hide show
  1. package/dist/adapters/localStorage.js +20 -5
  2. package/dist/adapters/memoryAuth.d.ts +6 -0
  3. package/dist/adapters/memoryAuth.js +117 -2
  4. package/dist/adapters/mongoAuth.js +97 -1
  5. package/dist/adapters/sqliteAuth.d.ts +23 -0
  6. package/dist/adapters/sqliteAuth.js +153 -2
  7. package/dist/app.d.ts +105 -2
  8. package/dist/app.js +112 -9
  9. package/dist/index.d.ts +23 -4
  10. package/dist/index.js +13 -2
  11. package/dist/lib/HttpError.d.ts +2 -1
  12. package/dist/lib/HttpError.js +3 -1
  13. package/dist/lib/appConfig.d.ts +113 -0
  14. package/dist/lib/appConfig.js +38 -0
  15. package/dist/lib/auditLog.d.ts +6 -0
  16. package/dist/lib/auditLog.js +17 -0
  17. package/dist/lib/authAdapter.d.ts +71 -1
  18. package/dist/lib/authRateLimit.js +36 -0
  19. package/dist/lib/breachedPassword.d.ts +13 -0
  20. package/dist/lib/breachedPassword.js +48 -0
  21. package/dist/lib/captcha.d.ts +25 -0
  22. package/dist/lib/captcha.js +37 -0
  23. package/dist/lib/context.d.ts +5 -0
  24. package/dist/lib/credentialStuffing.d.ts +31 -0
  25. package/dist/lib/credentialStuffing.js +77 -0
  26. package/dist/lib/emailVerification.d.ts +6 -0
  27. package/dist/lib/emailVerification.js +46 -3
  28. package/dist/lib/jwks.d.ts +25 -0
  29. package/dist/lib/jwks.js +51 -0
  30. package/dist/lib/jwt.d.ts +15 -2
  31. package/dist/lib/jwt.js +92 -5
  32. package/dist/lib/logger.d.ts +2 -0
  33. package/dist/lib/logger.js +6 -0
  34. package/dist/lib/m2m.d.ts +29 -0
  35. package/dist/lib/m2m.js +48 -0
  36. package/dist/lib/mfaChallenge.d.ts +14 -1
  37. package/dist/lib/mfaChallenge.js +111 -6
  38. package/dist/lib/mongo.js +1 -1
  39. package/dist/lib/oauthCode.js +23 -18
  40. package/dist/lib/resetPassword.js +3 -1
  41. package/dist/lib/saml.d.ts +25 -0
  42. package/dist/lib/saml.js +64 -0
  43. package/dist/lib/scim.d.ts +44 -0
  44. package/dist/lib/scim.js +54 -0
  45. package/dist/lib/securityEvents.d.ts +28 -0
  46. package/dist/lib/securityEvents.js +26 -0
  47. package/dist/lib/session.d.ts +10 -0
  48. package/dist/lib/session.js +67 -5
  49. package/dist/lib/signing.js +5 -2
  50. package/dist/lib/suspension.d.ts +13 -0
  51. package/dist/lib/suspension.js +23 -0
  52. package/dist/lib/upload.d.ts +4 -0
  53. package/dist/lib/upload.js +26 -1
  54. package/dist/lib/uploadRegistry.d.ts +18 -0
  55. package/dist/lib/uploadRegistry.js +83 -0
  56. package/dist/lib/ws.js +7 -0
  57. package/dist/middleware/bearerAuth.js +1 -1
  58. package/dist/middleware/captcha.d.ts +10 -0
  59. package/dist/middleware/captcha.js +36 -0
  60. package/dist/middleware/csrf.js +8 -4
  61. package/dist/middleware/errorHandler.js +4 -1
  62. package/dist/middleware/identify.js +40 -13
  63. package/dist/middleware/requestSigning.js +6 -5
  64. package/dist/middleware/requireMfaSetup.js +2 -1
  65. package/dist/middleware/requireScope.d.ts +10 -0
  66. package/dist/middleware/requireScope.js +25 -0
  67. package/dist/middleware/requireStepUp.d.ts +18 -0
  68. package/dist/middleware/requireStepUp.js +29 -0
  69. package/dist/middleware/scimAuth.d.ts +8 -0
  70. package/dist/middleware/scimAuth.js +29 -0
  71. package/dist/middleware/webhookAuth.d.ts +1 -1
  72. package/dist/middleware/webhookAuth.js +6 -5
  73. package/dist/models/AuthUser.d.ts +7 -0
  74. package/dist/models/AuthUser.js +7 -0
  75. package/dist/models/M2MClient.d.ts +18 -0
  76. package/dist/models/M2MClient.js +18 -0
  77. package/dist/routes/auth.d.ts +3 -2
  78. package/dist/routes/auth.js +155 -16
  79. package/dist/routes/jobs.js +21 -3
  80. package/dist/routes/m2m.d.ts +2 -0
  81. package/dist/routes/m2m.js +72 -0
  82. package/dist/routes/metrics.d.ts +1 -0
  83. package/dist/routes/metrics.js +3 -0
  84. package/dist/routes/mfa.js +9 -1
  85. package/dist/routes/oauth.js +6 -0
  86. package/dist/routes/oidc.d.ts +2 -0
  87. package/dist/routes/oidc.js +29 -0
  88. package/dist/routes/passkey.d.ts +1 -0
  89. package/dist/routes/passkey.js +157 -0
  90. package/dist/routes/saml.d.ts +2 -0
  91. package/dist/routes/saml.js +86 -0
  92. package/dist/routes/scim.d.ts +2 -0
  93. package/dist/routes/scim.js +255 -0
  94. package/dist/routes/uploads.d.ts +13 -1
  95. package/dist/routes/uploads.js +98 -6
  96. package/dist/services/auth.d.ts +2 -0
  97. package/dist/services/auth.js +101 -22
  98. package/dist/services/mfa.js +2 -2
  99. package/dist/ws/index.js +2 -1
  100. package/docs/sections/auth-flow/full.md +790 -779
  101. package/docs/sections/auth-security-examples/full.md +23 -0
  102. package/docs/sections/metrics/full.md +6 -2
  103. package/docs/sections/passkey-login/full.md +90 -0
  104. package/docs/sections/passkey-login/overview.md +1 -0
  105. package/docs/sections/uploads/full.md +11 -2
  106. package/docs/sections/webhook-auth/full.md +1 -1
  107. package/docs/sections/websocket/full.md +12 -0
  108. 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. **Recommended:** Lock down `/metrics` in production since it exposes operational details (error rates, queue depths, tenant activity patterns).
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: [userAuth, requireRole("admin")], // custom middleware stack
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" \| "sha1"` | `"sha256"` | HMAC algorithm. |
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.25",
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",