@lastshotlabs/bunshot 0.0.9 → 0.0.10
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 +70 -3
- package/dist/adapters/memoryAuth.d.ts +5 -0
- package/dist/adapters/memoryAuth.js +23 -0
- package/dist/adapters/sqliteAuth.d.ts +5 -0
- package/dist/adapters/sqliteAuth.js +18 -0
- package/dist/app.d.ts +18 -2
- package/dist/app.js +18 -5
- package/dist/entrypoints/mongo.d.ts +3 -0
- package/dist/entrypoints/mongo.js +3 -0
- package/dist/entrypoints/queue.d.ts +2 -0
- package/dist/entrypoints/queue.js +1 -0
- package/dist/entrypoints/redis.d.ts +1 -0
- package/dist/entrypoints/redis.js +1 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +4 -5
- package/dist/lib/appConfig.d.ts +9 -0
- package/dist/lib/appConfig.js +5 -0
- package/dist/lib/emailVerification.js +11 -10
- package/dist/lib/mongo.d.ts +9 -4
- package/dist/lib/mongo.js +61 -10
- package/dist/lib/oauth.js +11 -10
- package/dist/lib/queue.d.ts +3 -4
- package/dist/lib/queue.js +18 -3
- package/dist/lib/redis.d.ts +3 -8
- package/dist/lib/redis.js +19 -8
- package/dist/lib/resetPassword.d.ts +12 -0
- package/dist/lib/resetPassword.js +95 -0
- package/dist/lib/session.js +12 -12
- package/dist/middleware/cacheResponse.js +10 -9
- package/dist/models/AuthUser.d.ts +14 -106
- package/dist/models/AuthUser.js +31 -14
- package/dist/routes/auth.d.ts +3 -2
- package/dist/routes/auth.js +76 -1
- package/package.json +38 -8
package/README.md
CHANGED
|
@@ -60,6 +60,48 @@ bun add @lastshotlabs/bunshot
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
## Peer Dependencies
|
|
64
|
+
|
|
65
|
+
Bunshot declares the following as peer dependencies so you control their versions and avoid duplicate installs in your app.
|
|
66
|
+
|
|
67
|
+
### Required
|
|
68
|
+
|
|
69
|
+
These must be installed in every consuming app:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bun add hono zod
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Package | Required version |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `hono` | `>=4.12 <5` |
|
|
78
|
+
| `zod` | `>=4.0 <5` |
|
|
79
|
+
|
|
80
|
+
### Optional
|
|
81
|
+
|
|
82
|
+
Install only what your app actually uses:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# MongoDB auth / sessions / cache
|
|
86
|
+
bun add mongoose
|
|
87
|
+
|
|
88
|
+
# Redis sessions, cache, rate limiting, or BullMQ
|
|
89
|
+
bun add ioredis
|
|
90
|
+
|
|
91
|
+
# Background job queues
|
|
92
|
+
bun add bullmq
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Package | Required version | When you need it |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `mongoose` | `>=9.0 <10` | `db.auth: "mongo"`, `db.sessions: "mongo"`, or `db.cache: "mongo"` |
|
|
98
|
+
| `ioredis` | `>=5.0 <6` | `db.redis: true` (the default), or any store set to `"redis"` |
|
|
99
|
+
| `bullmq` | `>=5.0 <6` | Workers / queues |
|
|
100
|
+
|
|
101
|
+
If you're running fully on SQLite or memory (no Redis, no MongoDB), none of the optional peers are needed.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
63
105
|
## Quick Start
|
|
64
106
|
|
|
65
107
|
```ts
|
|
@@ -85,6 +127,8 @@ That's it. Your app gets:
|
|
|
85
127
|
| `DELETE /auth/sessions/:sessionId` | Revoke a specific session by ID (requires login) |
|
|
86
128
|
| `POST /auth/verify-email` | Verify email with token (when `emailVerification` is configured) |
|
|
87
129
|
| `POST /auth/resend-verification` | Resend verification email (requires login, when `emailVerification` is configured) |
|
|
130
|
+
| `POST /auth/forgot-password` | Request a password reset email (when `passwordReset` is configured) |
|
|
131
|
+
| `POST /auth/reset-password` | Reset password using a token from the reset email (when `passwordReset` is configured) |
|
|
88
132
|
| `GET /health` | Health check |
|
|
89
133
|
| `GET /docs` | Scalar API docs UI |
|
|
90
134
|
| `GET /openapi.json` | OpenAPI spec |
|
|
@@ -197,18 +241,31 @@ await createServer({
|
|
|
197
241
|
|
|
198
242
|
Import `appConnection` and register models on it. This ensures your models use the correct connection whether you're on a single DB or a separate tenant DB.
|
|
199
243
|
|
|
244
|
+
`appConnection` is a lazy proxy — calling `.model()` at the top level works fine even before `connectMongo()` has been called. Mongoose buffers any queries until the connection is established.
|
|
245
|
+
|
|
200
246
|
```ts
|
|
201
247
|
// src/models/Product.ts
|
|
202
|
-
import { appConnection
|
|
248
|
+
import { appConnection } from "@lastshotlabs/bunshot";
|
|
249
|
+
import { Schema } from "mongoose";
|
|
250
|
+
import type { HydratedDocument } from "mongoose";
|
|
203
251
|
|
|
204
|
-
|
|
252
|
+
interface IProduct {
|
|
253
|
+
name: string;
|
|
254
|
+
price: number;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type ProductDocument = HydratedDocument<IProduct>;
|
|
258
|
+
|
|
259
|
+
const ProductSchema = new Schema<IProduct>({
|
|
205
260
|
name: { type: String, required: true },
|
|
206
261
|
price: { type: Number, required: true },
|
|
207
262
|
}, { timestamps: true });
|
|
208
263
|
|
|
209
|
-
export const Product = appConnection.model("Product", ProductSchema);
|
|
264
|
+
export const Product = appConnection.model<IProduct>("Product", ProductSchema);
|
|
210
265
|
```
|
|
211
266
|
|
|
267
|
+
> **Note:** Import types (`HydratedDocument`, `Schema`, etc.) directly from `"mongoose"` — the `appConnection` and `mongoose` exports from bunshot are runtime proxies and cannot be used as TypeScript namespaces.
|
|
268
|
+
|
|
212
269
|
---
|
|
213
270
|
|
|
214
271
|
## Jobs (BullMQ)
|
|
@@ -724,11 +781,19 @@ await createServer({
|
|
|
724
781
|
await resend.emails.send({ to: email, subject: "Verify your email", text: `Token: ${token}` });
|
|
725
782
|
},
|
|
726
783
|
},
|
|
784
|
+
passwordReset: { // optional — only active when primaryField is "email"
|
|
785
|
+
tokenExpiry: 60 * 60, // default: 3600 (1 hour) — token TTL in seconds
|
|
786
|
+
onSend: async (email, token) => { // called by POST /auth/forgot-password — use any email provider
|
|
787
|
+
await resend.emails.send({ to: email, subject: "Reset your password", text: `Token: ${token}` });
|
|
788
|
+
},
|
|
789
|
+
},
|
|
727
790
|
rateLimit: { // optional — built-in auth endpoint rate limiting
|
|
728
791
|
login: { windowMs: 15 * 60 * 1000, max: 10 }, // default: 10 failures / 15 min
|
|
729
792
|
register: { windowMs: 60 * 60 * 1000, max: 5 }, // default: 5 attempts / hour (per IP)
|
|
730
793
|
verifyEmail: { windowMs: 15 * 60 * 1000, max: 10 }, // default: 10 attempts / 15 min (per IP)
|
|
731
794
|
resendVerification: { windowMs: 60 * 60 * 1000, max: 3 }, // default: 3 attempts / hour (per user)
|
|
795
|
+
forgotPassword: { windowMs: 15 * 60 * 1000, max: 5 }, // default: 5 attempts / 15 min (per IP)
|
|
796
|
+
resetPassword: { windowMs: 15 * 60 * 1000, max: 10 }, // default: 10 attempts / 15 min (per IP)
|
|
732
797
|
store: "redis", // default: "redis" when Redis is enabled, else "memory"
|
|
733
798
|
},
|
|
734
799
|
sessionPolicy: { // optional — session concurrency and metadata
|
|
@@ -994,6 +1059,8 @@ All built-in auth endpoints are rate-limited out of the box with sensible defaul
|
|
|
994
1059
|
| `POST /auth/register` | IP address | Every attempt | 5 / hour |
|
|
995
1060
|
| `POST /auth/verify-email` | IP address | Every attempt | 10 / 15 min |
|
|
996
1061
|
| `POST /auth/resend-verification` | User ID (authenticated) | Every attempt | 3 / hour |
|
|
1062
|
+
| `POST /auth/forgot-password` | IP address | Every attempt | 5 / 15 min |
|
|
1063
|
+
| `POST /auth/reset-password` | IP address | Every attempt | 10 / 15 min |
|
|
997
1064
|
|
|
998
1065
|
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.
|
|
999
1066
|
|
|
@@ -25,3 +25,8 @@ export declare const memoryGetVerificationToken: (token: string) => {
|
|
|
25
25
|
email: string;
|
|
26
26
|
} | null;
|
|
27
27
|
export declare const memoryDeleteVerificationToken: (token: string) => void;
|
|
28
|
+
export declare const memoryCreateResetToken: (token: string, userId: string, email: string, ttlSeconds: number) => void;
|
|
29
|
+
export declare const memoryConsumeResetToken: (hash: string) => {
|
|
30
|
+
userId: string;
|
|
31
|
+
email: string;
|
|
32
|
+
} | null;
|
|
@@ -7,6 +7,7 @@ const _userSessionIds = new Map(); // userId → Set<sessionId>
|
|
|
7
7
|
const _oauthStates = new Map();
|
|
8
8
|
const _cache = new Map();
|
|
9
9
|
const _verificationTokens = new Map();
|
|
10
|
+
const _resetTokens = new Map();
|
|
10
11
|
/** Reset all in-memory state. Useful for test isolation. */
|
|
11
12
|
export const clearMemoryStore = () => {
|
|
12
13
|
_users.clear();
|
|
@@ -16,6 +17,7 @@ export const clearMemoryStore = () => {
|
|
|
16
17
|
_oauthStates.clear();
|
|
17
18
|
_cache.clear();
|
|
18
19
|
_verificationTokens.clear();
|
|
20
|
+
_resetTokens.clear();
|
|
19
21
|
};
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
21
23
|
// Auth adapter
|
|
@@ -290,3 +292,24 @@ export const memoryGetVerificationToken = (token) => {
|
|
|
290
292
|
export const memoryDeleteVerificationToken = (token) => {
|
|
291
293
|
_verificationTokens.delete(token);
|
|
292
294
|
};
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Password reset token helpers (used by src/lib/resetPassword.ts)
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
export const memoryCreateResetToken = (token, userId, email, ttlSeconds) => {
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
// Opportunistically purge expired entries to prevent unbounded memory growth
|
|
301
|
+
for (const [k, v] of _resetTokens) {
|
|
302
|
+
if (v.expiresAt <= now)
|
|
303
|
+
_resetTokens.delete(k);
|
|
304
|
+
}
|
|
305
|
+
_resetTokens.set(token, { userId, email, expiresAt: now + ttlSeconds * 1000 });
|
|
306
|
+
};
|
|
307
|
+
export const memoryConsumeResetToken = (hash) => {
|
|
308
|
+
const entry = _resetTokens.get(hash);
|
|
309
|
+
if (!entry || entry.expiresAt <= Date.now()) {
|
|
310
|
+
_resetTokens.delete(hash);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
_resetTokens.delete(hash);
|
|
314
|
+
return { userId: entry.userId, email: entry.email };
|
|
315
|
+
};
|
|
@@ -25,4 +25,9 @@ export declare const sqliteGetVerificationToken: (token: string) => {
|
|
|
25
25
|
email: string;
|
|
26
26
|
} | null;
|
|
27
27
|
export declare const sqliteDeleteVerificationToken: (token: string) => void;
|
|
28
|
+
export declare const sqliteCreateResetToken: (token: string, userId: string, email: string, ttlSeconds: number) => void;
|
|
29
|
+
export declare const sqliteConsumeResetToken: (hash: string) => {
|
|
30
|
+
userId: string;
|
|
31
|
+
email: string;
|
|
32
|
+
} | null;
|
|
28
33
|
export declare const startSqliteCleanup: (intervalMs?: number) => ReturnType<typeof setInterval>;
|
|
@@ -65,6 +65,12 @@ function initSchema(db) {
|
|
|
65
65
|
email TEXT NOT NULL,
|
|
66
66
|
expiresAt INTEGER NOT NULL
|
|
67
67
|
)`);
|
|
68
|
+
db.run(`CREATE TABLE IF NOT EXISTS password_resets (
|
|
69
|
+
token TEXT PRIMARY KEY,
|
|
70
|
+
userId TEXT NOT NULL,
|
|
71
|
+
email TEXT NOT NULL,
|
|
72
|
+
expiresAt INTEGER NOT NULL
|
|
73
|
+
)`);
|
|
68
74
|
}
|
|
69
75
|
// ---------------------------------------------------------------------------
|
|
70
76
|
// Auth adapter
|
|
@@ -282,6 +288,17 @@ export const sqliteDeleteVerificationToken = (token) => {
|
|
|
282
288
|
getDb().run("DELETE FROM email_verifications WHERE token = ?", [token]);
|
|
283
289
|
};
|
|
284
290
|
// ---------------------------------------------------------------------------
|
|
291
|
+
// Password reset token helpers (used by src/lib/resetPassword.ts)
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
export const sqliteCreateResetToken = (token, userId, email, ttlSeconds) => {
|
|
294
|
+
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
295
|
+
getDb().run("INSERT INTO password_resets (token, userId, email, expiresAt) VALUES (?, ?, ?, ?)", [token, userId, email, expiresAt]);
|
|
296
|
+
};
|
|
297
|
+
export const sqliteConsumeResetToken = (hash) => {
|
|
298
|
+
const row = getDb().query("DELETE FROM password_resets WHERE token = ? AND expiresAt > ? RETURNING userId, email").get(hash, Date.now());
|
|
299
|
+
return row ?? null;
|
|
300
|
+
};
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
285
302
|
// Optional periodic cleanup of expired rows
|
|
286
303
|
// ---------------------------------------------------------------------------
|
|
287
304
|
export const startSqliteCleanup = (intervalMs = 3_600_000) => {
|
|
@@ -298,5 +315,6 @@ export const startSqliteCleanup = (intervalMs = 3_600_000) => {
|
|
|
298
315
|
db.run("DELETE FROM oauth_states WHERE expiresAt <= ?", [now]);
|
|
299
316
|
db.run("DELETE FROM cache_entries WHERE expiresAt IS NOT NULL AND expiresAt <= ?", [now]);
|
|
300
317
|
db.run("DELETE FROM email_verifications WHERE expiresAt <= ?", [now]);
|
|
318
|
+
db.run("DELETE FROM password_resets WHERE expiresAt <= ?", [now]);
|
|
301
319
|
}, intervalMs);
|
|
302
320
|
};
|
package/dist/app.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import type { MiddlewareHandler } from "hono";
|
|
3
3
|
import type { AppEnv } from "./lib/context";
|
|
4
|
-
import type { PrimaryField, EmailVerificationConfig } from "./lib/appConfig";
|
|
4
|
+
import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "./lib/appConfig";
|
|
5
5
|
import type { AuthAdapter } from "./lib/authAdapter";
|
|
6
6
|
import type { OAuthProviderConfig } from "./lib/oauth";
|
|
7
7
|
type StoreType = "redis" | "mongo" | "sqlite" | "memory";
|
|
@@ -82,6 +82,16 @@ export interface AuthRateLimitConfig {
|
|
|
82
82
|
windowMs?: number;
|
|
83
83
|
max?: number;
|
|
84
84
|
};
|
|
85
|
+
/** Max forgot-password requests per IP per window. Default: 5 per 15 min. */
|
|
86
|
+
forgotPassword?: {
|
|
87
|
+
windowMs?: number;
|
|
88
|
+
max?: number;
|
|
89
|
+
};
|
|
90
|
+
/** Max reset-password attempts per IP per window. Default: 10 per 15 min. */
|
|
91
|
+
resetPassword?: {
|
|
92
|
+
windowMs?: number;
|
|
93
|
+
max?: number;
|
|
94
|
+
};
|
|
85
95
|
/**
|
|
86
96
|
* Store backend for auth rate limit counters.
|
|
87
97
|
* Defaults to "redis" when Redis is enabled, otherwise "memory".
|
|
@@ -116,6 +126,12 @@ export interface AuthConfig {
|
|
|
116
126
|
* Provide an onSend callback to send the verification email via any provider (Resend, SendGrid, etc.).
|
|
117
127
|
*/
|
|
118
128
|
emailVerification?: EmailVerificationConfig;
|
|
129
|
+
/**
|
|
130
|
+
* Password reset configuration. Only active when primaryField is "email".
|
|
131
|
+
* Provide an onSend callback to send the reset email via any provider (Resend, SendGrid, etc.).
|
|
132
|
+
* Mounts POST /auth/forgot-password and POST /auth/reset-password.
|
|
133
|
+
*/
|
|
134
|
+
passwordReset?: PasswordResetConfig;
|
|
119
135
|
/** Rate limit configuration for built-in auth endpoints. */
|
|
120
136
|
rateLimit?: AuthRateLimitConfig;
|
|
121
137
|
/** Session concurrency and metadata persistence policy. */
|
|
@@ -140,7 +156,7 @@ export interface AuthSessionPolicyConfig {
|
|
|
140
156
|
*/
|
|
141
157
|
trackLastActive?: boolean;
|
|
142
158
|
}
|
|
143
|
-
export type { PrimaryField, EmailVerificationConfig };
|
|
159
|
+
export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig };
|
|
144
160
|
export interface BotProtectionConfig {
|
|
145
161
|
/**
|
|
146
162
|
* List of IPv4 CIDRs (e.g. "198.51.100.0/24"), IPv4 addresses, or IPv6 addresses to block outright.
|
package/dist/app.js
CHANGED
|
@@ -8,8 +8,9 @@ import { rateLimit } from "./middleware/rateLimit";
|
|
|
8
8
|
import { bearerAuth } from "./middleware/bearerAuth";
|
|
9
9
|
import { identify } from "./middleware/identify";
|
|
10
10
|
import { HEADER_USER_TOKEN } from "./lib/constants";
|
|
11
|
-
import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive } from "./lib/appConfig";
|
|
11
|
+
import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive } from "./lib/appConfig";
|
|
12
12
|
import { setEmailVerificationStore } from "./lib/emailVerification";
|
|
13
|
+
import { setPasswordResetStore } from "./lib/resetPassword";
|
|
13
14
|
import { setAuthRateLimitStore } from "./lib/authRateLimit";
|
|
14
15
|
import { setAuthAdapter } from "./lib/authAdapter";
|
|
15
16
|
import { mongoAuthAdapter } from "./adapters/mongoAuth";
|
|
@@ -39,6 +40,7 @@ export const createApp = async (config) => {
|
|
|
39
40
|
const defaultRole = authConfig.defaultRole;
|
|
40
41
|
const primaryField = authConfig.primaryField ?? "email";
|
|
41
42
|
const emailVerification = authConfig.emailVerification;
|
|
43
|
+
const passwordReset = authConfig.passwordReset;
|
|
42
44
|
const authRateLimit = authConfig.rateLimit;
|
|
43
45
|
const sessionPolicy = authConfig.sessionPolicy ?? {};
|
|
44
46
|
const { sqlite, mongo = "single", redis: enableRedis = true } = db;
|
|
@@ -82,20 +84,31 @@ export const createApp = async (config) => {
|
|
|
82
84
|
else {
|
|
83
85
|
authAdapter = mongoAuthAdapter;
|
|
84
86
|
}
|
|
87
|
+
if (defaultRole && !authAdapter.setRoles) {
|
|
88
|
+
throw new Error(`createApp: "defaultRole" is set to "${defaultRole}" but the auth adapter does not implement setRoles. Add setRoles to your adapter or remove defaultRole.`);
|
|
89
|
+
}
|
|
90
|
+
if (emailVerification && primaryField !== "email") {
|
|
91
|
+
throw new Error(`createApp: "emailVerification" is only supported when primaryField is "email". Either set primaryField to "email" or remove emailVerification.`);
|
|
92
|
+
}
|
|
93
|
+
if (passwordReset && primaryField !== "email") {
|
|
94
|
+
throw new Error(`createApp: "passwordReset" is only supported when primaryField is "email". Either set primaryField to "email" or remove passwordReset.`);
|
|
95
|
+
}
|
|
96
|
+
if (passwordReset && !authAdapter.setPassword) {
|
|
97
|
+
throw new Error(`createApp: "passwordReset" is configured but the auth adapter does not implement setPassword. Add setPassword to your adapter or remove passwordReset.`);
|
|
98
|
+
}
|
|
85
99
|
setAuthAdapter(authAdapter);
|
|
86
100
|
setAppRoles(roles);
|
|
87
101
|
setDefaultRole(defaultRole ?? null);
|
|
88
102
|
setPrimaryField(primaryField);
|
|
89
103
|
setEmailVerificationConfig(emailVerification ?? null);
|
|
90
104
|
setEmailVerificationStore(sessions);
|
|
105
|
+
setPasswordResetConfig(passwordReset ?? null);
|
|
106
|
+
setPasswordResetStore(sessions);
|
|
91
107
|
setAuthRateLimitStore(authRateLimit?.store ?? (enableRedis ? "redis" : "memory"));
|
|
92
108
|
setMaxSessions(sessionPolicy.maxSessions ?? 6);
|
|
93
109
|
setPersistSessionMetadata(sessionPolicy.persistSessionMetadata ?? true);
|
|
94
110
|
setIncludeInactiveSessions(sessionPolicy.includeInactiveSessions ?? false);
|
|
95
111
|
setTrackLastActive(sessionPolicy.trackLastActive ?? false);
|
|
96
|
-
if (defaultRole && !authAdapter.setRoles) {
|
|
97
|
-
throw new Error(`createApp: "defaultRole" is set to "${defaultRole}" but the auth adapter does not implement setRoles. Add setRoles to your adapter or remove defaultRole.`);
|
|
98
|
-
}
|
|
99
112
|
if (oauthProviders)
|
|
100
113
|
initOAuthProviders(oauthProviders);
|
|
101
114
|
const configuredOAuth = getConfiguredOAuthProviders();
|
|
@@ -144,7 +157,7 @@ export const createApp = async (config) => {
|
|
|
144
157
|
}
|
|
145
158
|
if (enableAuthRoutes) {
|
|
146
159
|
const { createAuthRouter } = await import(`${coreRoutesDir}/auth`);
|
|
147
|
-
app.route("/", createAuthRouter({ primaryField, emailVerification, rateLimit: authRateLimit }));
|
|
160
|
+
app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit }));
|
|
148
161
|
}
|
|
149
162
|
if (configuredOAuth.length > 0) {
|
|
150
163
|
app.route("/", createOAuthRouter(configuredOAuth, postOAuthRedirect));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createQueue, createWorker } from "../lib/queue";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { connectRedis, disconnectRedis, getRedis } from "../lib/redis";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { connectRedis, disconnectRedis, getRedis } from "../lib/redis";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { createApp } from "./app";
|
|
2
2
|
export { createServer } from "./server";
|
|
3
|
-
export type { CreateAppConfig, DbConfig, AppMeta, AuthConfig, AuthRateLimitConfig, OAuthConfig, SecurityConfig, BotProtectionConfig, PrimaryField, EmailVerificationConfig } from "./app";
|
|
3
|
+
export type { CreateAppConfig, DbConfig, AppMeta, AuthConfig, AuthRateLimitConfig, OAuthConfig, SecurityConfig, BotProtectionConfig, PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "./app";
|
|
4
4
|
export type { CreateServerConfig, WsConfig } from "./server";
|
|
5
|
+
export { appConnection, authConnection, mongoose, connectMongo, connectAuthMongo, connectAppMongo, disconnectMongo } from "./lib/mongo";
|
|
6
|
+
export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
|
|
5
7
|
export { getAppRoles } from "./lib/appConfig";
|
|
6
8
|
export { HttpError } from "./lib/HttpError";
|
|
7
9
|
export { COOKIE_TOKEN, HEADER_USER_TOKEN } from "./lib/constants";
|
|
@@ -9,10 +11,7 @@ export { createRouter } from "./lib/context";
|
|
|
9
11
|
export type { AppEnv, AppVariables } from "./lib/context";
|
|
10
12
|
export { signToken, verifyToken } from "./lib/jwt";
|
|
11
13
|
export { log } from "./lib/logger";
|
|
12
|
-
export {
|
|
13
|
-
export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
|
|
14
|
-
export { createQueue, createWorker } from "./lib/queue";
|
|
15
|
-
export type { Job } from "./lib/queue";
|
|
14
|
+
export { createResetToken, consumeResetToken, setPasswordResetStore } from "./lib/resetPassword";
|
|
16
15
|
export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore } from "./lib/session";
|
|
17
16
|
export type { SessionMetadata, SessionInfo } from "./lib/session";
|
|
18
17
|
export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
|
|
@@ -30,8 +29,6 @@ export { requireRole } from "./middleware/requireRole";
|
|
|
30
29
|
export { requireVerifiedEmail } from "./middleware/requireVerifiedEmail";
|
|
31
30
|
export { cacheResponse, bustCache, bustCachePattern, setCacheStore } from "./middleware/cacheResponse";
|
|
32
31
|
export { buildFingerprint } from "./lib/fingerprint";
|
|
33
|
-
export { AuthUser } from "./models/AuthUser";
|
|
34
|
-
export { mongoAuthAdapter } from "./adapters/mongoAuth";
|
|
35
32
|
export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
|
|
36
33
|
export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
|
|
37
34
|
export { setUserRoles, addUserRole, removeUserRole } from "./lib/roles";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// App factory
|
|
2
2
|
export { createApp } from "./app";
|
|
3
3
|
export { createServer } from "./server";
|
|
4
|
+
// Database
|
|
5
|
+
export { appConnection, authConnection, mongoose, connectMongo, connectAuthMongo, connectAppMongo, disconnectMongo } from "./lib/mongo";
|
|
6
|
+
export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
|
|
4
7
|
// Lib utilities
|
|
5
8
|
export { getAppRoles } from "./lib/appConfig";
|
|
6
9
|
export { HttpError } from "./lib/HttpError";
|
|
@@ -8,9 +11,7 @@ export { COOKIE_TOKEN, HEADER_USER_TOKEN } from "./lib/constants";
|
|
|
8
11
|
export { createRouter } from "./lib/context";
|
|
9
12
|
export { signToken, verifyToken } from "./lib/jwt";
|
|
10
13
|
export { log } from "./lib/logger";
|
|
11
|
-
export {
|
|
12
|
-
export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
|
|
13
|
-
export { createQueue, createWorker } from "./lib/queue";
|
|
14
|
+
export { createResetToken, consumeResetToken, setPasswordResetStore } from "./lib/resetPassword";
|
|
14
15
|
export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore } from "./lib/session";
|
|
15
16
|
export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
|
|
16
17
|
export { bustAuthLimit, trackAttempt, isLimited } from "./lib/authRateLimit";
|
|
@@ -27,8 +28,6 @@ export { cacheResponse, bustCache, bustCachePattern, setCacheStore } from "./mid
|
|
|
27
28
|
// Lib utilities (bot protection)
|
|
28
29
|
export { buildFingerprint } from "./lib/fingerprint";
|
|
29
30
|
// Models
|
|
30
|
-
export { AuthUser } from "./models/AuthUser";
|
|
31
|
-
export { mongoAuthAdapter } from "./adapters/mongoAuth";
|
|
32
31
|
export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
|
|
33
32
|
export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
|
|
34
33
|
export { setUserRoles, addUserRole, removeUserRole } from "./lib/roles";
|
package/dist/lib/appConfig.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface EmailVerificationConfig {
|
|
|
7
7
|
/** Called after registration with the identifier and verification token. Use to send the email. */
|
|
8
8
|
onSend: (email: string, token: string) => Promise<void>;
|
|
9
9
|
}
|
|
10
|
+
export interface PasswordResetConfig {
|
|
11
|
+
/** Token time-to-live in seconds. Defaults to 3 600 (1 hour). */
|
|
12
|
+
tokenExpiry?: number;
|
|
13
|
+
/** Called with the user's email and the reset token. Use to send the reset email. */
|
|
14
|
+
onSend: (email: string, token: string) => Promise<void>;
|
|
15
|
+
}
|
|
10
16
|
export declare const setAppName: (name: string) => void;
|
|
11
17
|
export declare const getAppName: () => string;
|
|
12
18
|
export declare const setAppRoles: (roles: string[]) => void;
|
|
@@ -18,6 +24,9 @@ export declare const getPrimaryField: () => PrimaryField;
|
|
|
18
24
|
export declare const setEmailVerificationConfig: (config: EmailVerificationConfig | null) => void;
|
|
19
25
|
export declare const getEmailVerificationConfig: () => EmailVerificationConfig | null;
|
|
20
26
|
export declare const getTokenExpiry: () => number;
|
|
27
|
+
export declare const setPasswordResetConfig: (config: PasswordResetConfig | null) => void;
|
|
28
|
+
export declare const getPasswordResetConfig: () => PasswordResetConfig | null;
|
|
29
|
+
export declare const getResetTokenExpiry: () => number;
|
|
21
30
|
export declare const setMaxSessions: (n: number) => void;
|
|
22
31
|
export declare const getMaxSessions: () => number;
|
|
23
32
|
export declare const setPersistSessionMetadata: (v: boolean) => void;
|
package/dist/lib/appConfig.js
CHANGED
|
@@ -3,6 +3,7 @@ let appRoles = [];
|
|
|
3
3
|
let defaultRole = null;
|
|
4
4
|
let _primaryField = "email";
|
|
5
5
|
let _emailVerificationConfig = null;
|
|
6
|
+
let _passwordResetConfig = null;
|
|
6
7
|
export const setAppName = (name) => { appName = name; };
|
|
7
8
|
export const getAppName = () => appName;
|
|
8
9
|
export const setAppRoles = (roles) => { appRoles = roles; };
|
|
@@ -15,6 +16,10 @@ export const setEmailVerificationConfig = (config) => { _emailVerificationConfig
|
|
|
15
16
|
export const getEmailVerificationConfig = () => _emailVerificationConfig;
|
|
16
17
|
const DEFAULT_TOKEN_EXPIRY = 60 * 60 * 24; // 24 hours
|
|
17
18
|
export const getTokenExpiry = () => _emailVerificationConfig?.tokenExpiry ?? DEFAULT_TOKEN_EXPIRY;
|
|
19
|
+
export const setPasswordResetConfig = (config) => { _passwordResetConfig = config; };
|
|
20
|
+
export const getPasswordResetConfig = () => _passwordResetConfig;
|
|
21
|
+
const DEFAULT_RESET_TOKEN_EXPIRY = 60 * 60; // 1 hour
|
|
22
|
+
export const getResetTokenExpiry = () => _passwordResetConfig?.tokenExpiry ?? DEFAULT_RESET_TOKEN_EXPIRY;
|
|
18
23
|
// ---------------------------------------------------------------------------
|
|
19
24
|
// Session policy
|
|
20
25
|
// ---------------------------------------------------------------------------
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { getRedis } from "./redis";
|
|
2
|
-
import { appConnection } from "./mongo";
|
|
2
|
+
import { appConnection, mongoose } from "./mongo";
|
|
3
3
|
import { getAppName, getTokenExpiry } from "./appConfig";
|
|
4
|
-
import { Schema } from "mongoose";
|
|
5
4
|
import { sqliteCreateVerificationToken, sqliteGetVerificationToken, sqliteDeleteVerificationToken, } from "../adapters/sqliteAuth";
|
|
6
5
|
import { memoryCreateVerificationToken, memoryGetVerificationToken, memoryDeleteVerificationToken, } from "../adapters/memoryAuth";
|
|
7
|
-
const verificationSchema = new Schema({
|
|
8
|
-
token: { type: String, required: true, unique: true },
|
|
9
|
-
userId: { type: String, required: true },
|
|
10
|
-
email: { type: String, required: true },
|
|
11
|
-
expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
|
|
12
|
-
}, { collection: "email_verifications" });
|
|
13
6
|
function getVerificationModel() {
|
|
14
|
-
|
|
15
|
-
appConnection.
|
|
7
|
+
if (appConnection.models["EmailVerification"])
|
|
8
|
+
return appConnection.models["EmailVerification"];
|
|
9
|
+
const { Schema } = mongoose;
|
|
10
|
+
const verificationSchema = new Schema({
|
|
11
|
+
token: { type: String, required: true, unique: true },
|
|
12
|
+
userId: { type: String, required: true },
|
|
13
|
+
email: { type: String, required: true },
|
|
14
|
+
expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
|
|
15
|
+
}, { collection: "email_verifications" });
|
|
16
|
+
return appConnection.model("EmailVerification", verificationSchema);
|
|
16
17
|
}
|
|
17
18
|
let _store = "redis";
|
|
18
19
|
export const setEmailVerificationStore = (store) => { _store = store; };
|
package/dist/lib/mongo.d.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Connection, Mongoose } from "mongoose";
|
|
2
|
+
type MongooseModule = Mongoose;
|
|
2
3
|
/**
|
|
3
4
|
* Named connection used exclusively for auth data (AuthUser model).
|
|
4
5
|
* Connected via connectAuthMongo() or connectMongo() (backward compat).
|
|
5
6
|
*/
|
|
6
|
-
export declare const authConnection:
|
|
7
|
+
export declare const authConnection: Connection;
|
|
7
8
|
/**
|
|
8
9
|
* Named connection for app/tenant data.
|
|
9
10
|
* Connected via connectAppMongo() or connectMongo() (backward compat).
|
|
10
11
|
* Use this when registering your own models: appConnection.model("Product", schema).
|
|
11
12
|
*/
|
|
12
|
-
export declare const appConnection:
|
|
13
|
+
export declare const appConnection: Connection;
|
|
14
|
+
/**
|
|
15
|
+
* The mongoose instance. Available after connectMongo() / connectAuthMongo() is called.
|
|
16
|
+
*/
|
|
17
|
+
export declare const mongoose: MongooseModule;
|
|
13
18
|
/**
|
|
14
19
|
* Connect the auth connection to its dedicated MongoDB server.
|
|
15
20
|
* Uses MONGO_AUTH_USER_*, MONGO_AUTH_PW_*, MONGO_AUTH_HOST_*, MONGO_AUTH_DB_* env vars.
|
|
@@ -31,4 +36,4 @@ export declare const connectMongo: () => Promise<void>;
|
|
|
31
36
|
* Useful for one-off scripts that need a clean exit.
|
|
32
37
|
*/
|
|
33
38
|
export declare const disconnectMongo: () => Promise<void>;
|
|
34
|
-
export {
|
|
39
|
+
export {};
|