@daloyjs/core 1.0.0-rc.6 → 1.0.0-rc.7

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.
@@ -470,8 +470,22 @@ export interface RateLimitOptions {
470
470
  * Trust x-forwarded-for / x-real-ip when deriving the default key.
471
471
  * Off by default because those headers are client-spoofable unless your
472
472
  * reverse proxy strips and rewrites them.
473
+ *
474
+ * When enabled, the key is the **rightmost** `X-Forwarded-For` entry — the
475
+ * one your immediate proxy appended — never the attacker-influenceable
476
+ * leftmost one, so rotating spoofed left entries cannot evade the limit.
477
+ * Behind more than one proxy hop, set {@link trustedHops} instead.
473
478
  */
474
479
  trustProxyHeaders?: boolean;
480
+ /**
481
+ * Declare exactly how many proxy hops sit between Daloy and the public
482
+ * internet. Implies proxy-header trust and derives the default key that
483
+ * many entries from the right of `X-Forwarded-For` via
484
+ * {@link "./conn-info.js".resolveForwardedClientIp}. Must be an integer in
485
+ * [1, 64]; validated at construction. Ignored when a custom `keyGenerator`
486
+ * is supplied.
487
+ */
488
+ trustedHops?: number;
475
489
  /** When true, set Retry-After header on 429. Default: true. */
476
490
  retryAfter?: boolean;
477
491
  /**
@@ -546,8 +560,24 @@ export interface LoginThrottleOptions {
546
560
  keyGenerator?: (ctx: RateLimitContext) => string;
547
561
  /** Shared store for the hard limit. Uses rateLimit()'s in-memory group bucket by default. */
548
562
  store?: RateLimitStore;
549
- /** Trust x-forwarded-for / x-real-ip when deriving the default key. Default: false. */
563
+ /** Trust x-forwarded-for / x-real-ip when deriving the default key. Default: false.
564
+ *
565
+ * When enabled, the key is the **rightmost** `X-Forwarded-For` entry — the
566
+ * one your immediate proxy appended — never the attacker-influenceable
567
+ * leftmost one. Behind more than one proxy hop, set {@link trustedHops}
568
+ * instead.
569
+ */
550
570
  trustProxyHeaders?: boolean;
571
+ /**
572
+ * Declare exactly how many proxy hops sit between Daloy and the public
573
+ * internet. Implies proxy-header trust and derives the default key that
574
+ * many entries from the right of `X-Forwarded-For` via
575
+ * {@link "./conn-info.js".resolveForwardedClientIp}, so rotating spoofed
576
+ * left entries cannot evade the throttle. Must be an integer in [1, 64];
577
+ * validated at construction. Ignored when a custom `keyGenerator` is
578
+ * supplied.
579
+ */
580
+ trustedHops?: number;
551
581
  /** When true, set Retry-After header on 429. Default: true. */
552
582
  retryAfter?: boolean;
553
583
  /** Start slowing responses after this many attempts in the same window. Default: 2. */
@@ -7,6 +7,7 @@
7
7
  import { assertCookieAttributes, readRequestCookie, serializeCookie } from "./cookie.js";
8
8
  import { TooManyRequestsError, ForbiddenError } from "./errors.js";
9
9
  import { randomId, sanitizeHeaderName, timingSafeEqual } from "./security.js";
10
+ import { resolveForwardedClientIp, resolveForwardedTrust } from "./conn-info.js";
10
11
  /**
11
12
  * Generate or accept a stable `X-Request-ID` for every request. The id is
12
13
  * stamped on `ctx.state.requestId`, mirrored on outgoing response headers,
@@ -775,15 +776,8 @@ export function rateLimit(opts) {
775
776
  store = new MemoryStore();
776
777
  }
777
778
  const groupPrefix = opts.groupId ? `${opts.groupId}:` : "";
778
- const keyOf = opts.keyGenerator ??
779
- ((ctx) => {
780
- if (opts.trustProxyHeaders) {
781
- const xff = ctx.request.headers.get("x-forwarded-for");
782
- const first = xff ? xff.split(",")[0].trim() : "";
783
- return first || ctx.request.headers.get("x-real-ip") || "global";
784
- }
785
- return "global";
786
- });
779
+ const hops = resolveForwardedTrust("rateLimit()", opts);
780
+ const keyOf = opts.keyGenerator ?? defaultForwardedRateLimitKey(hops);
787
781
  const enforce = async (ctx) => {
788
782
  const key = `${groupPrefix}${keyOf(ctx)}`;
789
783
  const { count, resetMs } = await store.hit(key, opts.windowMs);
@@ -811,15 +805,22 @@ function assertPositiveInteger(name, value) {
811
805
  throw new Error(`loginThrottle(): ${name} must be a positive integer.`);
812
806
  }
813
807
  }
814
- function defaultLoginThrottleKey(trustProxyHeaders) {
815
- return (ctx) => {
816
- if (trustProxyHeaders) {
817
- const forwardedFor = ctx.request.headers.get("x-forwarded-for");
818
- const firstForwarded = forwardedFor ? forwardedFor.split(",")[0].trim() : "";
819
- return firstForwarded || ctx.request.headers.get("x-real-ip") || "global";
820
- }
821
- return "global";
822
- };
808
+ /**
809
+ * Default rate-limit / login-throttle key: the spoof-resistant forwarded client
810
+ * IP, or the shared `"global"` bucket when proxy-header trust is off or the
811
+ * request carries no trustworthy forwarded identity.
812
+ *
813
+ * @param hops - Trusted proxy hop count from
814
+ * {@link "./conn-info.js".resolveForwardedTrust}, or `undefined` when
815
+ * forwarded-header trust is disabled.
816
+ * @returns A key generator suitable for {@link rateLimit} and
817
+ * {@link loginThrottle}.
818
+ * @internal
819
+ */
820
+ function defaultForwardedRateLimitKey(hops) {
821
+ if (hops === undefined)
822
+ return () => "global";
823
+ return (ctx) => resolveForwardedClientIp(ctx.request, hops) ?? "global";
823
824
  }
824
825
  function wait(ms) {
825
826
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -852,7 +853,8 @@ export function loginThrottle(opts = {}) {
852
853
  assertNonNegativeInteger("delayMs", delayMs);
853
854
  assertNonNegativeInteger("maxDelayMs", maxDelayMs);
854
855
  const groupId = opts.groupId ?? "login";
855
- const keyGenerator = opts.keyGenerator ?? defaultLoginThrottleKey(opts.trustProxyHeaders);
856
+ const hops = resolveForwardedTrust("loginThrottle()", opts);
857
+ const keyGenerator = opts.keyGenerator ?? defaultForwardedRateLimitKey(hops);
856
858
  const limiter = rateLimit({
857
859
  windowMs,
858
860
  max,
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:1fe1844d-d5f6-5dd3-b35a-8f79f17a35f5",
4
+ "serialNumber": "urn:uuid:01ee81d6-1bb6-55ca-b05e-3e30279d9f5c",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-07-26T17:41:07.733Z",
7
+ "timestamp": "2026-07-29T20:03:05.244Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "1.0.0-rc.6"
12
+ "version": "1.0.0-rc.7"
13
13
  }
14
14
  ],
15
15
  "authors": [
@@ -19,11 +19,11 @@
19
19
  ],
20
20
  "component": {
21
21
  "type": "library",
22
- "bom-ref": "pkg:npm/@daloyjs/core@1.0.0-rc.6",
22
+ "bom-ref": "pkg:npm/@daloyjs/core@1.0.0-rc.7",
23
23
  "name": "@daloyjs/core",
24
- "version": "1.0.0-rc.6",
24
+ "version": "1.0.0-rc.7",
25
25
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
26
- "purl": "pkg:npm/@daloyjs/core@1.0.0-rc.6",
26
+ "purl": "pkg:npm/@daloyjs/core@1.0.0-rc.7",
27
27
  "licenses": [
28
28
  {
29
29
  "license": {
@@ -46,9 +46,9 @@
46
46
  }
47
47
  ],
48
48
  "swid": {
49
- "tagId": "swidtag--daloyjs-core-1.0.0-rc.6",
49
+ "tagId": "swidtag--daloyjs-core-1.0.0-rc.7",
50
50
  "name": "@daloyjs/core",
51
- "version": "1.0.0-rc.6",
51
+ "version": "1.0.0-rc.7",
52
52
  "tagVersion": 0,
53
53
  "patch": false
54
54
  }
@@ -57,7 +57,7 @@
57
57
  "components": [],
58
58
  "dependencies": [
59
59
  {
60
- "ref": "pkg:npm/@daloyjs/core@1.0.0-rc.6",
60
+ "ref": "pkg:npm/@daloyjs/core@1.0.0-rc.7",
61
61
  "dependsOn": []
62
62
  }
63
63
  ]
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@daloyjs/core-1.0.0-rc.6",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.6-1fe1844d-d5f6-5dd3-b35a-8f79f17a35f5",
5
+ "name": "@daloyjs/core-1.0.0-rc.7",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.7-01ee81d6-1bb6-55ca-b05e-3e30279d9f5c",
7
7
  "creationInfo": {
8
- "created": "2026-07-26T17:41:07.733Z",
8
+ "created": "2026-07-29T20:03:05.244Z",
9
9
  "creators": [
10
10
  "Tool: daloy-generate-sbom",
11
11
  "Organization: DaloyJS"
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "SPDXID": "SPDXRef-Package--daloyjs-core",
18
18
  "name": "@daloyjs/core",
19
- "versionInfo": "1.0.0-rc.6",
19
+ "versionInfo": "1.0.0-rc.7",
20
20
  "downloadLocation": "https://github.com/daloyjs/daloy",
21
21
  "filesAnalyzed": false,
22
22
  "licenseConcluded": "MIT",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "referenceCategory": "PACKAGE-MANAGER",
29
29
  "referenceType": "purl",
30
- "referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-rc.6"
30
+ "referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-rc.7"
31
31
  }
32
32
  ]
33
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daloyjs/core",
3
- "version": "1.0.0-rc.6",
3
+ "version": "1.0.0-rc.7",
4
4
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
5
5
  "type": "module",
6
6
  "publishConfig": {