@guren/server 1.0.0-rc.24 → 1.0.0-rc.26

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.
@@ -455,11 +455,31 @@ var ModelUserProvider = class extends BaseUserProvider {
455
455
  getId(user) {
456
456
  return user[this.idColumn];
457
457
  }
458
+ /**
459
+ * Strip the password hash, remember token, and the model's `hidden`
460
+ * fields before the record leaves the auth layer. Credential
461
+ * validation happens on the raw record before sanitizing, so this
462
+ * never affects login — only what `auth.user()` exposes.
463
+ */
464
+ sanitize(user) {
465
+ const blocked = /* @__PURE__ */ new Set([
466
+ this.passwordColumn,
467
+ this.rememberTokenColumn,
468
+ ...this.model.hidden ?? []
469
+ ]);
470
+ const clean = {};
471
+ for (const [key, value] of Object.entries(user)) {
472
+ if (!blocked.has(key)) {
473
+ clean[key] = value;
474
+ }
475
+ }
476
+ return clean;
477
+ }
458
478
  async setRememberToken(user, token) {
459
479
  if (typeof user[this.rememberTokenColumn] !== "undefined") {
460
480
  ;
461
481
  user[this.rememberTokenColumn] = token;
462
- await this.model.update({ [this.idColumn]: this.getId(user) }, { [this.rememberTokenColumn]: token });
482
+ await this.model.forceUpdate({ [this.idColumn]: this.getId(user) }, { [this.rememberTokenColumn]: token });
463
483
  }
464
484
  }
465
485
  async getRememberToken(user) {
@@ -554,9 +574,14 @@ var SessionGuard = class {
554
574
  this.currentSession.forget(this.rememberSessionKey());
555
575
  return null;
556
576
  }
557
- this.cachedUser = user;
558
577
  await this.provider.setRememberToken?.(user, rememberToken);
559
- return user;
578
+ const sanitized = this.sanitizeUser(user);
579
+ this.cachedUser = sanitized;
580
+ return sanitized;
581
+ }
582
+ /** Strip auth-internal fields before a record is cached or exposed. */
583
+ sanitizeUser(user) {
584
+ return this.provider.sanitize ? this.provider.sanitize(user) : user;
560
585
  }
561
586
  async resolveUser() {
562
587
  if (this.cachedUser !== void 0) {
@@ -579,8 +604,9 @@ var SessionGuard = class {
579
604
  this.cachedUser = null;
580
605
  return null;
581
606
  }
582
- this.cachedUser = user;
583
- return user;
607
+ const sanitized = this.sanitizeUser(user);
608
+ this.cachedUser = sanitized;
609
+ return sanitized;
584
610
  }
585
611
  async check() {
586
612
  return await this.resolveUser() !== null;
@@ -616,7 +642,7 @@ var SessionGuard = class {
616
642
  session.regenerate();
617
643
  const identifier = this.provider.getId(castUser);
618
644
  session.set(this.sessionKey(), identifier);
619
- this.cachedUser = castUser;
645
+ this.cachedUser = this.sanitizeUser(castUser);
620
646
  if (remember) {
621
647
  await this.remember(castUser);
622
648
  } else {
@@ -1250,9 +1276,29 @@ var DEFAULT_STATE_LENGTH = 24;
1250
1276
  var DEFAULT_STATE_HASH_ALGORITHM = "sha256";
1251
1277
  var MemoryOAuthStateStore = class {
1252
1278
  states = /* @__PURE__ */ new Map();
1279
+ maxEntries;
1280
+ constructor(options = {}) {
1281
+ this.maxEntries = options.maxEntries ?? 1e4;
1282
+ }
1253
1283
  async store(stateHash, payload) {
1284
+ if (this.states.size >= this.maxEntries) {
1285
+ this.sweepExpired();
1286
+ }
1287
+ while (this.states.size >= this.maxEntries) {
1288
+ const oldest = this.states.keys().next().value;
1289
+ if (oldest === void 0) break;
1290
+ this.states.delete(oldest);
1291
+ }
1254
1292
  this.states.set(stateHash, payload);
1255
1293
  }
1294
+ sweepExpired() {
1295
+ const now = Date.now();
1296
+ for (const [hash, payload] of this.states) {
1297
+ if (payload.expiresAt.getTime() <= now) {
1298
+ this.states.delete(hash);
1299
+ }
1300
+ }
1301
+ }
1256
1302
  async find(stateHash) {
1257
1303
  const payload = this.states.get(stateHash);
1258
1304
  if (!payload) return null;
@@ -6,7 +6,7 @@ import standard from "figlet/importable-fonts/Standard.js";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@guren/server",
9
- version: "1.0.0-rc.24",
9
+ version: "1.0.0-rc.26",
10
10
  type: "module",
11
11
  license: "MIT",
12
12
  repository: {
@@ -102,6 +102,10 @@ var package_default = {
102
102
  "./lambda": {
103
103
  types: "./dist/lambda/index.d.ts",
104
104
  default: "./dist/lambda/index.js"
105
+ },
106
+ "./redis": {
107
+ types: "./dist/redis/index.d.ts",
108
+ default: "./dist/redis/index.js"
105
109
  }
106
110
  },
107
111
  scripts: {
@@ -111,21 +115,21 @@ var package_default = {
111
115
  test: "bun test"
112
116
  },
113
117
  dependencies: {
114
- "@guren/inertia-client": "^1.0.0-rc.23",
115
- "@guren/orm": "^1.0.0-rc.25",
118
+ "@guren/orm": "^1.0.0-rc.27",
116
119
  "@modelcontextprotocol/sdk": "^1.27.1",
117
120
  chalk: "^5.3.0",
118
121
  consola: "^3.4.2",
119
122
  figlet: "^1.7.0",
120
123
  hono: "^4.12.18",
121
124
  ioredis: "^5.9.1",
122
- nodemailer: "^8.0.4"
125
+ nodemailer: "^9.0.1"
123
126
  },
124
127
  peerDependencies: {
125
128
  "@aws-sdk/client-s3": "^3.0.0",
126
129
  "@aws-sdk/client-sqs": "^3.0.0",
127
130
  "@aws-sdk/s3-request-presigner": "^3.0.0",
128
- vite: ">=7.0.0"
131
+ vite: ">=7.0.0",
132
+ "@guren/inertia-client": "^1.0.0-rc.25"
129
133
  },
130
134
  peerDependenciesMeta: {
131
135
  "@aws-sdk/client-s3": {
@@ -136,6 +140,9 @@ var package_default = {
136
140
  },
137
141
  "@aws-sdk/s3-request-presigner": {
138
142
  optional: true
143
+ },
144
+ "@guren/inertia-client": {
145
+ optional: true
139
146
  }
140
147
  },
141
148
  devDependencies: {
@@ -0,0 +1,14 @@
1
+ // src/redis/client.ts
2
+ import Redis from "ioredis";
3
+ function createRedisClient(options = {}) {
4
+ const { url, ...redisOptions } = options;
5
+ if (url) {
6
+ return new Redis(url, redisOptions);
7
+ }
8
+ return new Redis(redisOptions);
9
+ }
10
+
11
+ export {
12
+ Redis,
13
+ createRedisClient
14
+ };
@@ -0,0 +1,232 @@
1
+ import { Context, MiddlewareHandler } from 'hono';
2
+ import Redis, { RedisOptions } from 'ioredis';
3
+
4
+ /**
5
+ * Rate limit entry stored in the backing store.
6
+ */
7
+ interface RateLimitEntry {
8
+ count: number;
9
+ resetAt: number;
10
+ }
11
+ /**
12
+ * Store interface for rate limit data.
13
+ * Implement this for Redis or database-backed storage.
14
+ */
15
+ interface RateLimitStore {
16
+ /**
17
+ * Get the current rate limit entry for a key.
18
+ */
19
+ get(key: string): Promise<RateLimitEntry | null>;
20
+ /**
21
+ * Increment the count for a key and return the new entry.
22
+ * If the key doesn't exist, create it with count=1.
23
+ */
24
+ increment(key: string, windowMs: number): Promise<RateLimitEntry>;
25
+ /**
26
+ * Reset the count for a key.
27
+ */
28
+ reset(key: string): Promise<void>;
29
+ }
30
+ /**
31
+ * Base class for in-memory rate limit stores with automatic cleanup.
32
+ */
33
+ declare abstract class BaseMemoryStore implements RateLimitStore {
34
+ protected cleanupInterval?: ReturnType<typeof setInterval>;
35
+ constructor(cleanupIntervalMs?: number);
36
+ abstract get(key: string): Promise<RateLimitEntry | null>;
37
+ abstract increment(key: string, windowMs: number): Promise<RateLimitEntry>;
38
+ abstract reset(key: string): Promise<void>;
39
+ abstract cleanup(): void;
40
+ abstract clear(): void;
41
+ abstract get size(): number;
42
+ destroy(): void;
43
+ }
44
+ /**
45
+ * In-memory rate limit store.
46
+ * Suitable for single-process development, not for production clusters.
47
+ */
48
+ declare class MemoryRateLimitStore extends BaseMemoryStore {
49
+ private entries;
50
+ get(key: string): Promise<RateLimitEntry | null>;
51
+ increment(key: string, windowMs: number): Promise<RateLimitEntry>;
52
+ reset(key: string): Promise<void>;
53
+ cleanup(): void;
54
+ clear(): void;
55
+ get size(): number;
56
+ }
57
+ /**
58
+ * Configuration options for rate limiting.
59
+ */
60
+ interface RateLimitOptions {
61
+ /**
62
+ * Maximum number of requests allowed in the time window.
63
+ * @default 100
64
+ */
65
+ limit?: number;
66
+ /**
67
+ * Time window in milliseconds.
68
+ * @default 60000 (1 minute)
69
+ */
70
+ windowMs?: number;
71
+ /**
72
+ * Function to extract the rate limit key from the request.
73
+ * Defaults to using the client IP address.
74
+ */
75
+ keyGenerator?: (ctx: Context) => string | Promise<string>;
76
+ /**
77
+ * Rate limit store implementation.
78
+ * Defaults to in-memory store.
79
+ */
80
+ store?: RateLimitStore;
81
+ /**
82
+ * Whether to skip rate limiting for certain requests.
83
+ */
84
+ skip?: (ctx: Context) => boolean | Promise<boolean>;
85
+ /**
86
+ * Custom handler when rate limit is exceeded.
87
+ */
88
+ onRateLimited?: (ctx: Context, retryAfter: number) => Response | Promise<Response>;
89
+ /**
90
+ * Whether to add rate limit headers to all responses.
91
+ * @default true
92
+ */
93
+ headers?: boolean;
94
+ /**
95
+ * Custom message when rate limit is exceeded.
96
+ * @default 'Too many requests, please try again later.'
97
+ */
98
+ message?: string;
99
+ /**
100
+ * HTTP status code when rate limit is exceeded.
101
+ * @default 429
102
+ */
103
+ statusCode?: number;
104
+ /**
105
+ * Prefix for rate limit keys.
106
+ * @default 'rl:'
107
+ */
108
+ keyPrefix?: string;
109
+ /**
110
+ * Trust reverse-proxy headers for client IP resolution, checked in order:
111
+ * `CF-Connecting-IP`, `True-Client-IP`, `X-Real-IP`, then the first entry
112
+ * of `X-Forwarded-For`. Falls back to `server.requestIP()` when none are set.
113
+ *
114
+ * Enable ONLY when every request passes through a proxy that sets or strips
115
+ * these headers — on direct deployments clients can spoof them to bypass
116
+ * per-client limits. Ignored when a custom `keyGenerator` is supplied.
117
+ * @default false
118
+ */
119
+ trustProxy?: boolean;
120
+ }
121
+ /**
122
+ * Create a rate limiting middleware.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * // Basic usage - 100 requests per minute per IP
127
+ * app.use('*', createRateLimitMiddleware())
128
+ *
129
+ * // Stricter limit for login endpoint
130
+ * router.post('/login', [AuthController, 'login'],
131
+ * createRateLimitMiddleware({
132
+ * limit: 5,
133
+ * windowMs: 15 * 60 * 1000, // 15 minutes
134
+ * })
135
+ * )
136
+ *
137
+ * // Custom key based on authenticated user
138
+ * app.use('/api/*', createRateLimitMiddleware({
139
+ * limit: 1000,
140
+ * windowMs: 60 * 60 * 1000, // 1 hour
141
+ * keyGenerator: async (ctx) => {
142
+ * const user = await ctx.get('user')
143
+ * return user?.id?.toString() ?? defaultKeyGenerator(ctx)
144
+ * },
145
+ * }))
146
+ * ```
147
+ */
148
+ declare function createRateLimitMiddleware(options?: RateLimitOptions): MiddlewareHandler;
149
+ /**
150
+ * Rate limit result information.
151
+ */
152
+ interface RateLimitInfo {
153
+ limit: number;
154
+ remaining: number;
155
+ resetAt: Date;
156
+ isLimited: boolean;
157
+ }
158
+ /**
159
+ * Get rate limit information for a key without incrementing.
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * const info = await getRateLimitInfo('user:123', store, { limit: 100 })
164
+ * console.log(`${info.remaining} requests remaining`)
165
+ * ```
166
+ */
167
+ declare function getRateLimitInfo(key: string, store: RateLimitStore, options?: {
168
+ limit: number;
169
+ keyPrefix?: string;
170
+ }): Promise<RateLimitInfo>;
171
+ /**
172
+ * Reset rate limit for a specific key.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * // Reset rate limit after successful captcha
177
+ * await resetRateLimit(clientIp, store)
178
+ * ```
179
+ */
180
+ declare function resetRateLimit(key: string, store: RateLimitStore, options?: {
181
+ keyPrefix?: string;
182
+ }): Promise<void>;
183
+ /**
184
+ * Sliding window rate limit store.
185
+ * More accurate than fixed window but uses more memory.
186
+ */
187
+ declare class SlidingWindowRateLimitStore extends BaseMemoryStore {
188
+ private requests;
189
+ get(key: string): Promise<RateLimitEntry | null>;
190
+ increment(key: string, windowMs: number): Promise<RateLimitEntry>;
191
+ reset(key: string): Promise<void>;
192
+ cleanup(): void;
193
+ clear(): void;
194
+ get size(): number;
195
+ }
196
+
197
+ /**
198
+ * Options for creating a Redis client.
199
+ */
200
+ interface RedisClientOptions extends RedisOptions {
201
+ /**
202
+ * Redis connection URL (e.g., 'redis://localhost:6379').
203
+ * If provided, overrides host/port/password.
204
+ */
205
+ url?: string;
206
+ /**
207
+ * Key prefix for all operations.
208
+ * @default ''
209
+ */
210
+ keyPrefix?: string;
211
+ }
212
+ /**
213
+ * Create a Redis client with the given options.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * // Using URL
218
+ * const redis = createRedisClient({ url: 'redis://localhost:6379' })
219
+ *
220
+ * // Using host/port
221
+ * const redis = createRedisClient({ host: 'localhost', port: 6379 })
222
+ *
223
+ * // With key prefix
224
+ * const redis = createRedisClient({
225
+ * url: process.env.REDIS_URL,
226
+ * keyPrefix: 'myapp:',
227
+ * })
228
+ * ```
229
+ */
230
+ declare function createRedisClient(options?: RedisClientOptions): Redis;
231
+
232
+ export { MemoryRateLimitStore as M, type RateLimitStore as R, SlidingWindowRateLimitStore as S, type RateLimitEntry as a, type RedisClientOptions as b, createRedisClient as c, type RateLimitInfo as d, type RateLimitOptions as e, createRateLimitMiddleware as f, getRateLimitInfo as g, resetRateLimit as r };