@spfn/core 0.2.0-beta.57 → 0.2.0-beta.59

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.
@@ -1,5 +1,5 @@
1
1
  import { Context, Next, MiddlewareHandler } from 'hono';
2
- import { b as NamedMiddlewareFactory } from '../define-middleware-DuXD8Hvu.js';
2
+ import { b as NamedMiddlewareFactory, N as NamedMiddleware } from '../define-middleware-DuXD8Hvu.js';
3
3
 
4
4
  /**
5
5
  * Error Handler Middleware
@@ -285,8 +285,17 @@ declare function createCacheNonceStore(cache: CacheClient, prefix?: string): Non
285
285
  */
286
286
  declare function createInMemoryNonceStore(): NonceStore;
287
287
 
288
+ /**
289
+ * One identity dimension to limit on. A bare string uses the top-level `limit`;
290
+ * an object can carry its own `limit` so dimensions can differ (e.g. a loose
291
+ * per-IP cap alongside a tight per-account cap).
292
+ */
293
+ type RateLimitDimension = string | {
294
+ key: string;
295
+ limit?: number;
296
+ };
288
297
  interface RateLimitOptions {
289
- /** Max requests allowed per window, applied to each identity dimension. */
298
+ /** Max requests allowed per window, applied to each dimension without its own `limit`. */
290
299
  limit: number;
291
300
  /** Window length in milliseconds. */
292
301
  windowMs: number;
@@ -297,10 +306,11 @@ interface RateLimitOptions {
297
306
  scope?: string;
298
307
  /**
299
308
  * Identity dimensions to limit on. Each non-empty value is counted
300
- * separately and the strictest wins (e.g. by IP and by account). Defaults
301
- * to the client IP only.
309
+ * separately and the strictest wins (e.g. by IP and by account). A dimension
310
+ * may override the top-level `limit` via `{ key, limit }`. Defaults to the
311
+ * client IP only.
302
312
  */
303
- by?: (c: Context) => (string | null | undefined)[] | Promise<(string | null | undefined)[]>;
313
+ by?: (c: Context) => (RateLimitDimension | null | undefined)[] | Promise<(RateLimitDimension | null | undefined)[]>;
304
314
  /** Reject with 429 instead of allowing through when the cache is unavailable. */
305
315
  failClosed?: boolean;
306
316
  /** Message for the 429 response. */
@@ -316,6 +326,10 @@ interface RateLimitOptions {
316
326
  * the forwarded header is attacker-settable, so fall back to the raw chain — whose
317
327
  * leftmost hop is itself spoofable, so still pair with an account/target dimension
318
328
  * for anything security-sensitive.
329
+ *
330
+ * Last resort before giving up is the TCP peer address from the Node adapter
331
+ * socket. Without it, a deployment that sets no forwarding header would collapse
332
+ * every client onto a single `'unknown'` bucket.
319
333
  */
320
334
  declare function getClientIp(c: Context): string;
321
335
  /**
@@ -329,5 +343,41 @@ declare function getClientIp(c: Context): string;
329
343
  * ```
330
344
  */
331
345
  declare const rateLimit: NamedMiddlewareFactory<"rateLimit", [options: RateLimitOptions]>;
346
+ /** Set the fail-closed default for named policies. Called by the server at boot. */
347
+ declare function setRateLimitFailClosedDefault(failClosed: boolean): void;
348
+ /**
349
+ * Replace the named-policy registry. Called by the server at boot; passing
350
+ * undefined clears it (so a restart without policies doesn't keep stale ones).
351
+ */
352
+ declare function setRateLimitPolicies(policies?: Record<string, RateLimitOptions>): void;
353
+ /** Look up a configured policy by name — undefined when the app didn't set it. */
354
+ declare function getRateLimitPolicy(name: string): RateLimitOptions | undefined;
355
+ /**
356
+ * Rate-limit a route under a named policy.
357
+ *
358
+ * A package author tags a sensitive route with a policy name plus a safe
359
+ * fallback; the consuming app tunes the numbers centrally via
360
+ * `defineServerConfig().rateLimit({ policies: { [name]: {...} } })`. When the
361
+ * app configures the policy, its fields override the fallback (shallow merge);
362
+ * otherwise the fallback applies, so the route is protected out of the box.
363
+ *
364
+ * LAYERS on top of the global default limiter rather than replacing it: the tag
365
+ * has a distinct name (`rateLimit:<name>`) and does not skip the global, so a
366
+ * route gets both the global per-IP floor (when enabled) and this policy's own
367
+ * bucket — whichever is stricter trips first. Opt out of the global floor on a
368
+ * route with `.skip(['rateLimit'])`.
369
+ *
370
+ * The counter scope defaults to the policy name, so (a) every route sharing a
371
+ * policy shares one bucket, and (b) the key never collides with the global
372
+ * default's per-route `${method} ${path}` scope.
373
+ *
374
+ * @example
375
+ * ```typescript
376
+ * route.post('/_auth/login')
377
+ * .use([rateLimitPolicy('auth-login', { limit: 5, windowMs: 60_000 })])
378
+ * .handler(...);
379
+ * ```
380
+ */
381
+ declare function rateLimitPolicy(name: string, fallback: RateLimitOptions): NamedMiddleware<string>;
332
382
 
333
- export { type ClientType, ErrorHandler, type ErrorHandlerOptions, type NonceStore, type OnErrorContext, type ProxyGuardConfig, type ProxyGuardMode, type RateLimitOptions, RequestLogger, type RequestLoggerConfig, type RequestLoggerOptions, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, maskSensitiveData, rateLimit };
383
+ export { type ClientType, ErrorHandler, type ErrorHandlerOptions, type NonceStore, type OnErrorContext, type ProxyGuardConfig, type ProxyGuardMode, type RateLimitDimension, type RateLimitOptions, RequestLogger, type RequestLoggerConfig, type RequestLoggerOptions, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, getRateLimitPolicy, maskSensitiveData, rateLimit, rateLimitPolicy, setRateLimitFailClosedDefault, setRateLimitPolicies };
@@ -237,6 +237,42 @@ function verifyProxyRequest(input) {
237
237
  }
238
238
 
239
239
  // src/route/define-middleware.ts
240
+ function defineMiddleware(name, handlerOrFactory, options) {
241
+ const skips = options?.skips;
242
+ if (typeof handlerOrFactory === "function") {
243
+ const paramCount = handlerOrFactory.length;
244
+ if (paramCount === 2) {
245
+ return {
246
+ name,
247
+ handler: handlerOrFactory,
248
+ _name: name,
249
+ ...skips
250
+ };
251
+ } else {
252
+ const factory = handlerOrFactory;
253
+ const wrapper = (...args) => factory(...args);
254
+ Object.defineProperty(wrapper, "name", {
255
+ value: name,
256
+ writable: false,
257
+ enumerable: false,
258
+ configurable: true
259
+ });
260
+ Object.defineProperty(wrapper, "_name", {
261
+ value: name,
262
+ writable: false,
263
+ enumerable: false,
264
+ configurable: true
265
+ });
266
+ return wrapper;
267
+ }
268
+ }
269
+ return {
270
+ name,
271
+ handler: handlerOrFactory,
272
+ _name: name,
273
+ ...skips
274
+ };
275
+ }
240
276
  function defineMiddlewareFactory(name, factory) {
241
277
  const wrapper = (...args) => factory(...args);
242
278
  Object.defineProperty(wrapper, "name", {
@@ -1066,7 +1102,12 @@ function getClientIp(c) {
1066
1102
  }
1067
1103
  }
1068
1104
  const forwardedFor = c.req.header("x-forwarded-for");
1069
- return forwardedFor?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
1105
+ return forwardedFor?.split(",")[0]?.trim() || c.req.header("x-real-ip") || socketRemoteAddress(c) || "unknown";
1106
+ }
1107
+ function socketRemoteAddress(c) {
1108
+ const env4 = c.env;
1109
+ const bindings = env4?.server ?? env4;
1110
+ return bindings?.incoming?.socket?.remoteAddress;
1070
1111
  }
1071
1112
  var rateLimit = defineMiddlewareFactory(
1072
1113
  "rateLimit",
@@ -1086,13 +1127,18 @@ var rateLimit = defineMiddlewareFactory(
1086
1127
  const dimensions = (by ? await by(c) : [getClientIp(c)]).filter((d) => Boolean(d));
1087
1128
  const ns = scope || `${c.req.method} ${c.req.routePath || c.req.path}`;
1088
1129
  for (const dimension of dimensions) {
1130
+ const key = typeof dimension === "string" ? dimension : dimension.key;
1131
+ if (!key) {
1132
+ continue;
1133
+ }
1134
+ const dimLimit = typeof dimension === "string" ? limit : dimension.limit ?? limit;
1089
1135
  const [count, pttl] = await cache.eval(
1090
1136
  FIXED_WINDOW_LUA,
1091
1137
  1,
1092
- `ratelimit:${ns}:${dimension}`,
1138
+ `ratelimit:${ns}:${key}`,
1093
1139
  String(windowMs)
1094
1140
  );
1095
- if (count > limit) {
1141
+ if (count > dimLimit) {
1096
1142
  const retryAfter = Math.max(1, Math.ceil((pttl > 0 ? pttl : windowMs) / 1e3));
1097
1143
  c.header("Retry-After", String(retryAfter));
1098
1144
  throw new TooManyRequestsError({
@@ -1105,6 +1151,41 @@ var rateLimit = defineMiddlewareFactory(
1105
1151
  };
1106
1152
  }
1107
1153
  );
1154
+ var policyRegistry = /* @__PURE__ */ new Map();
1155
+ var policyFailClosedDefault = false;
1156
+ function setRateLimitFailClosedDefault(failClosed) {
1157
+ policyFailClosedDefault = failClosed;
1158
+ }
1159
+ function setRateLimitPolicies(policies) {
1160
+ policyRegistry.clear();
1161
+ if (!policies) {
1162
+ return;
1163
+ }
1164
+ for (const [name, options] of Object.entries(policies)) {
1165
+ policyRegistry.set(name, options);
1166
+ }
1167
+ }
1168
+ function getRateLimitPolicy(name) {
1169
+ return policyRegistry.get(name);
1170
+ }
1171
+ function rateLimitPolicy(name, fallback) {
1172
+ let resolved;
1173
+ const handler = (c, next) => {
1174
+ if (!resolved) {
1175
+ const configured = getRateLimitPolicy(name);
1176
+ const merged = configured ? { ...fallback, ...configured } : { ...fallback };
1177
+ if (merged.scope === void 0) {
1178
+ merged.scope = name;
1179
+ }
1180
+ if (merged.failClosed === void 0) {
1181
+ merged.failClosed = policyFailClosedDefault;
1182
+ }
1183
+ resolved = rateLimit(merged);
1184
+ }
1185
+ return resolved(c, next);
1186
+ };
1187
+ return defineMiddleware(`rateLimit:${name}`, handler);
1188
+ }
1108
1189
 
1109
1190
  // src/middleware/request-logger.ts
1110
1191
  var DEFAULT_CONFIG = {
@@ -1383,6 +1464,6 @@ function reject(c, mode, next, reason) {
1383
1464
  return next();
1384
1465
  }
1385
1466
 
1386
- export { ErrorHandler, RequestLogger, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, maskSensitiveData2 as maskSensitiveData, rateLimit };
1467
+ export { ErrorHandler, RequestLogger, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, getRateLimitPolicy, maskSensitiveData2 as maskSensitiveData, rateLimit, rateLimitPolicy, setRateLimitFailClosedDefault, setRateLimitPolicies };
1387
1468
  //# sourceMappingURL=index.js.map
1388
1469
  //# sourceMappingURL=index.js.map