@daloyjs/core 0.34.3 → 0.35.1

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 (186) hide show
  1. package/README.md +151 -32
  2. package/dist/adapters/bun.d.ts +0 -1
  3. package/dist/adapters/bun.js +0 -1
  4. package/dist/adapters/cloudflare.d.ts +0 -1
  5. package/dist/adapters/cloudflare.js +0 -1
  6. package/dist/adapters/deno.d.ts +0 -1
  7. package/dist/adapters/deno.js +0 -1
  8. package/dist/adapters/fastly.d.ts +0 -1
  9. package/dist/adapters/fastly.js +0 -1
  10. package/dist/adapters/lambda.d.ts +0 -1
  11. package/dist/adapters/lambda.js +0 -1
  12. package/dist/adapters/node.d.ts +0 -1
  13. package/dist/adapters/node.js +175 -35
  14. package/dist/adapters/vercel.d.ts +0 -1
  15. package/dist/adapters/vercel.js +0 -1
  16. package/dist/app.d.ts +18 -1
  17. package/dist/app.js +396 -118
  18. package/dist/banner.d.ts +0 -1
  19. package/dist/banner.js +0 -1
  20. package/dist/cli.d.ts +32 -1
  21. package/dist/cli.js +62 -5
  22. package/dist/client.d.ts +0 -1
  23. package/dist/client.js +0 -1
  24. package/dist/combine.d.ts +0 -1
  25. package/dist/combine.js +0 -1
  26. package/dist/compression.d.ts +0 -1
  27. package/dist/compression.js +0 -1
  28. package/dist/config.d.ts +0 -1
  29. package/dist/config.js +0 -1
  30. package/dist/conn-info.d.ts +0 -1
  31. package/dist/conn-info.js +0 -1
  32. package/dist/contract.d.ts +0 -1
  33. package/dist/contract.js +0 -1
  34. package/dist/cookie.d.ts +0 -1
  35. package/dist/cookie.js +0 -1
  36. package/dist/dependency.d.ts +0 -1
  37. package/dist/dependency.js +0 -1
  38. package/dist/discriminator.d.ts +0 -1
  39. package/dist/discriminator.js +0 -1
  40. package/dist/docs.d.ts +0 -1
  41. package/dist/docs.js +0 -1
  42. package/dist/errors.d.ts +0 -1
  43. package/dist/errors.js +0 -1
  44. package/dist/etag.d.ts +0 -1
  45. package/dist/etag.js +0 -1
  46. package/dist/fetch-guard.d.ts +50 -6
  47. package/dist/fetch-guard.js +83 -18
  48. package/dist/hashing.d.ts +0 -1
  49. package/dist/hashing.js +0 -1
  50. package/dist/index.d.ts +5 -4
  51. package/dist/index.js +3 -3
  52. package/dist/ip-restriction.d.ts +0 -1
  53. package/dist/ip-restriction.js +0 -1
  54. package/dist/jwk.d.ts +0 -1
  55. package/dist/jwk.js +0 -1
  56. package/dist/jwt.d.ts +15 -1
  57. package/dist/jwt.js +56 -3
  58. package/dist/load-shedding.d.ts +0 -1
  59. package/dist/load-shedding.js +0 -1
  60. package/dist/logger.d.ts +0 -1
  61. package/dist/logger.js +0 -1
  62. package/dist/middleware.d.ts +78 -1
  63. package/dist/middleware.js +150 -18
  64. package/dist/multipart.d.ts +0 -1
  65. package/dist/multipart.js +0 -1
  66. package/dist/openapi.d.ts +0 -1
  67. package/dist/openapi.js +0 -1
  68. package/dist/rate-limit-redis.d.ts +0 -1
  69. package/dist/rate-limit-redis.js +0 -1
  70. package/dist/router.d.ts +0 -1
  71. package/dist/router.js +0 -1
  72. package/dist/safe-redirect.d.ts +91 -0
  73. package/dist/safe-redirect.js +168 -0
  74. package/dist/sbom.cdx.json +13 -9
  75. package/dist/sbom.spdx.json +5 -5
  76. package/dist/schema.d.ts +0 -1
  77. package/dist/schema.js +0 -1
  78. package/dist/security-schemes.d.ts +0 -1
  79. package/dist/security-schemes.js +0 -1
  80. package/dist/security.d.ts +134 -3
  81. package/dist/security.js +265 -6
  82. package/dist/session.d.ts +0 -1
  83. package/dist/session.js +0 -1
  84. package/dist/streaming.d.ts +0 -1
  85. package/dist/streaming.js +0 -1
  86. package/dist/subdomains.d.ts +0 -1
  87. package/dist/subdomains.js +0 -1
  88. package/dist/time-claims.d.ts +0 -1
  89. package/dist/time-claims.js +0 -1
  90. package/dist/tracing.d.ts +0 -1
  91. package/dist/tracing.js +0 -1
  92. package/dist/types.d.ts +0 -1
  93. package/dist/types.js +0 -1
  94. package/dist/websocket.d.ts +0 -1
  95. package/dist/websocket.js +0 -1
  96. package/package.json +35 -15
  97. package/dist/adapters/bun.d.ts.map +0 -1
  98. package/dist/adapters/bun.js.map +0 -1
  99. package/dist/adapters/cloudflare.d.ts.map +0 -1
  100. package/dist/adapters/cloudflare.js.map +0 -1
  101. package/dist/adapters/deno.d.ts.map +0 -1
  102. package/dist/adapters/deno.js.map +0 -1
  103. package/dist/adapters/fastly.d.ts.map +0 -1
  104. package/dist/adapters/fastly.js.map +0 -1
  105. package/dist/adapters/lambda.d.ts.map +0 -1
  106. package/dist/adapters/lambda.js.map +0 -1
  107. package/dist/adapters/node.d.ts.map +0 -1
  108. package/dist/adapters/node.js.map +0 -1
  109. package/dist/adapters/vercel.d.ts.map +0 -1
  110. package/dist/adapters/vercel.js.map +0 -1
  111. package/dist/app.d.ts.map +0 -1
  112. package/dist/app.js.map +0 -1
  113. package/dist/banner.d.ts.map +0 -1
  114. package/dist/banner.js.map +0 -1
  115. package/dist/cli.d.ts.map +0 -1
  116. package/dist/cli.js.map +0 -1
  117. package/dist/client.d.ts.map +0 -1
  118. package/dist/client.js.map +0 -1
  119. package/dist/combine.d.ts.map +0 -1
  120. package/dist/combine.js.map +0 -1
  121. package/dist/compression.d.ts.map +0 -1
  122. package/dist/compression.js.map +0 -1
  123. package/dist/config.d.ts.map +0 -1
  124. package/dist/config.js.map +0 -1
  125. package/dist/conn-info.d.ts.map +0 -1
  126. package/dist/conn-info.js.map +0 -1
  127. package/dist/contract.d.ts.map +0 -1
  128. package/dist/contract.js.map +0 -1
  129. package/dist/cookie.d.ts.map +0 -1
  130. package/dist/cookie.js.map +0 -1
  131. package/dist/dependency.d.ts.map +0 -1
  132. package/dist/dependency.js.map +0 -1
  133. package/dist/discriminator.d.ts.map +0 -1
  134. package/dist/discriminator.js.map +0 -1
  135. package/dist/docs.d.ts.map +0 -1
  136. package/dist/docs.js.map +0 -1
  137. package/dist/errors.d.ts.map +0 -1
  138. package/dist/errors.js.map +0 -1
  139. package/dist/etag.d.ts.map +0 -1
  140. package/dist/etag.js.map +0 -1
  141. package/dist/fetch-guard.d.ts.map +0 -1
  142. package/dist/fetch-guard.js.map +0 -1
  143. package/dist/hashing.d.ts.map +0 -1
  144. package/dist/hashing.js.map +0 -1
  145. package/dist/index.d.ts.map +0 -1
  146. package/dist/index.js.map +0 -1
  147. package/dist/ip-restriction.d.ts.map +0 -1
  148. package/dist/ip-restriction.js.map +0 -1
  149. package/dist/jwk.d.ts.map +0 -1
  150. package/dist/jwk.js.map +0 -1
  151. package/dist/jwt.d.ts.map +0 -1
  152. package/dist/jwt.js.map +0 -1
  153. package/dist/load-shedding.d.ts.map +0 -1
  154. package/dist/load-shedding.js.map +0 -1
  155. package/dist/logger.d.ts.map +0 -1
  156. package/dist/logger.js.map +0 -1
  157. package/dist/middleware.d.ts.map +0 -1
  158. package/dist/middleware.js.map +0 -1
  159. package/dist/multipart.d.ts.map +0 -1
  160. package/dist/multipart.js.map +0 -1
  161. package/dist/openapi.d.ts.map +0 -1
  162. package/dist/openapi.js.map +0 -1
  163. package/dist/rate-limit-redis.d.ts.map +0 -1
  164. package/dist/rate-limit-redis.js.map +0 -1
  165. package/dist/router.d.ts.map +0 -1
  166. package/dist/router.js.map +0 -1
  167. package/dist/schema.d.ts.map +0 -1
  168. package/dist/schema.js.map +0 -1
  169. package/dist/security-schemes.d.ts.map +0 -1
  170. package/dist/security-schemes.js.map +0 -1
  171. package/dist/security.d.ts.map +0 -1
  172. package/dist/security.js.map +0 -1
  173. package/dist/session.d.ts.map +0 -1
  174. package/dist/session.js.map +0 -1
  175. package/dist/streaming.d.ts.map +0 -1
  176. package/dist/streaming.js.map +0 -1
  177. package/dist/subdomains.d.ts.map +0 -1
  178. package/dist/subdomains.js.map +0 -1
  179. package/dist/time-claims.d.ts.map +0 -1
  180. package/dist/time-claims.js.map +0 -1
  181. package/dist/tracing.d.ts.map +0 -1
  182. package/dist/tracing.js.map +0 -1
  183. package/dist/types.d.ts.map +0 -1
  184. package/dist/types.js.map +0 -1
  185. package/dist/websocket.d.ts.map +0 -1
  186. package/dist/websocket.js.map +0 -1
@@ -0,0 +1,168 @@
1
+ /**
2
+ * First-party `safeRedirect()` helper that refuses open-redirect inputs.
3
+ *
4
+ * Open redirects (Aikido "Top 10 app security problems" #10, OWASP
5
+ * "Unvalidated Redirects and Forwards") happen when an app blindly trusts
6
+ * a `?next=...` / `?returnTo=...` query parameter and emits a `Location`
7
+ * header pointing wherever the attacker wants. The fix the industry has
8
+ * settled on is the same one Daloy enforces here: validate every
9
+ * candidate URL against an explicit allowlist of internal paths and
10
+ * external origins **before** building the redirect response.
11
+ *
12
+ * Defaults are deliberately strict:
13
+ *
14
+ * - Same-origin paths must start with `/` and must not start with `//`
15
+ * or `/\` (which browsers interpret as protocol-relative URLs that
16
+ * escape your origin).
17
+ * - Backslashes, control characters, and `CR`/`LF` are rejected to
18
+ * stop response-splitting and homograph tricks.
19
+ * - Absolute URLs are only allowed when their `origin` exactly matches
20
+ * one of the entries in `allowedOrigins`.
21
+ * - `javascript:`, `data:`, `vbscript:`, and `file:` schemes are always
22
+ * refused, even if the caller wrote a bug into the allowlist.
23
+ * - The default status is `303 See Other`, which is the
24
+ * POST-redirect-GET-safe choice. Override with `status` when you
25
+ * genuinely need `301`/`302`/`307`/`308`.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { safeRedirect } from "@daloyjs/core";
30
+ *
31
+ * app.get("/login/callback", (ctx) => {
32
+ * const next = new URL(ctx.request.url).searchParams.get("next") ?? "/";
33
+ * return safeRedirect(next, {
34
+ * allowedPaths: ["/", "/dashboard", "/account"],
35
+ * allowedOrigins: ["https://app.example.com"],
36
+ * fallback: "/",
37
+ * });
38
+ * });
39
+ * ```
40
+ *
41
+ * @since 0.35.1
42
+ */
43
+ /** Thrown when {@link safeRedirect} refuses a candidate URL and no `fallback` is configured. */
44
+ export class OpenRedirectBlockedError extends Error {
45
+ reason;
46
+ target;
47
+ constructor(reason, target) {
48
+ super(`safeRedirect: refused redirect (${reason})`);
49
+ this.name = "OpenRedirectBlockedError";
50
+ this.reason = reason;
51
+ this.target = target;
52
+ }
53
+ }
54
+ const FORBIDDEN_SCHEMES = new Set([
55
+ "javascript:",
56
+ "data:",
57
+ "vbscript:",
58
+ "file:",
59
+ ]);
60
+ const ALLOWED_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
61
+ // Reject NUL, CR, LF, and other C0/C1 control characters; they enable
62
+ // response-splitting via the `Location` header.
63
+ // eslint-disable-next-line no-control-regex
64
+ const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f-\u009f]/;
65
+ function buildResponse(location, status, headers) {
66
+ const merged = new Headers(headers);
67
+ merged.set("Location", location);
68
+ // Belt and suspenders: cache directives so browsers and shared caches
69
+ // never reuse a redirect that was scoped to a single request.
70
+ if (!merged.has("Cache-Control"))
71
+ merged.set("Cache-Control", "no-store");
72
+ return new Response(null, { status, headers: merged });
73
+ }
74
+ function classify(target, allowedPaths, allowedOrigins) {
75
+ if (typeof target !== "string" || target.length === 0) {
76
+ return { ok: false, reason: "empty-target" };
77
+ }
78
+ if (CONTROL_CHAR_RE.test(target)) {
79
+ return { ok: false, reason: "invalid-control-characters" };
80
+ }
81
+ // Protocol-relative (`//evil.com`) is the classic open-redirect bypass.
82
+ if (target.startsWith("//"))
83
+ return { ok: false, reason: "protocol-relative" };
84
+ // `/\evil.com` is interpreted by some browsers as protocol-relative too.
85
+ if (target.startsWith("/\\"))
86
+ return { ok: false, reason: "backslash-path" };
87
+ if (target.startsWith("/")) {
88
+ // Same-origin path. Backslashes anywhere in the path can confuse
89
+ // user agents and proxies — refuse them outright.
90
+ if (target.includes("\\"))
91
+ return { ok: false, reason: "backslash-path" };
92
+ if (allowedPaths.length === 0) {
93
+ return { ok: false, reason: "path-not-allowed" };
94
+ }
95
+ if (allowedPaths.includes("/*"))
96
+ return { ok: true, location: target };
97
+ // Match on pathname only; preserve any user-provided query/fragment.
98
+ const qIdx = target.search(/[?#]/);
99
+ const pathname = qIdx === -1 ? target : target.slice(0, qIdx);
100
+ if (!allowedPaths.includes(pathname)) {
101
+ return { ok: false, reason: "path-not-allowed" };
102
+ }
103
+ return { ok: true, location: target };
104
+ }
105
+ // Absolute URL path. Parse and compare origin.
106
+ let parsed;
107
+ try {
108
+ parsed = new URL(target);
109
+ }
110
+ catch {
111
+ return { ok: false, reason: "parse-failed" };
112
+ }
113
+ if (FORBIDDEN_SCHEMES.has(parsed.protocol.toLowerCase())) {
114
+ return { ok: false, reason: "scheme-not-allowed" };
115
+ }
116
+ if (!allowedOrigins.includes(parsed.origin)) {
117
+ return { ok: false, reason: "origin-not-allowed" };
118
+ }
119
+ return { ok: true, location: parsed.toString() };
120
+ }
121
+ /**
122
+ * Build a redirect `Response` after validating the target against an
123
+ * explicit allowlist. Throws {@link OpenRedirectBlockedError} when the
124
+ * candidate fails validation and no `fallback` is configured.
125
+ *
126
+ * @param target - User-supplied URL candidate (path or absolute URL).
127
+ * @param options - Allowlist + response configuration.
128
+ *
129
+ * @since 0.35.1
130
+ */
131
+ export function safeRedirect(target, options = {}) {
132
+ const allowedPaths = options.allowedPaths ?? [];
133
+ const allowedOrigins = options.allowedOrigins ?? [];
134
+ const status = options.status ?? 303;
135
+ if (!ALLOWED_REDIRECT_STATUSES.has(status)) {
136
+ throw new TypeError(`safeRedirect: status ${String(status)} is not a redirect status (allowed: 301, 302, 303, 307, 308)`);
137
+ }
138
+ for (const p of allowedPaths) {
139
+ if (typeof p !== "string" || (p !== "/*" && !p.startsWith("/"))) {
140
+ throw new TypeError(`safeRedirect: allowedPaths entries must start with "/" (got ${JSON.stringify(p)})`);
141
+ }
142
+ }
143
+ for (const o of allowedOrigins) {
144
+ if (typeof o !== "string" || o.length === 0) {
145
+ throw new TypeError("safeRedirect: allowedOrigins entries must be non-empty strings");
146
+ }
147
+ let parsed;
148
+ try {
149
+ parsed = new URL(o);
150
+ }
151
+ catch {
152
+ throw new TypeError(`safeRedirect: allowedOrigins entry is not a valid URL: ${o}`);
153
+ }
154
+ if (parsed.origin !== o) {
155
+ throw new TypeError(`safeRedirect: allowedOrigins entry must be a bare origin (scheme + host [+ port]); got ${o}`);
156
+ }
157
+ }
158
+ const result = classify(target, allowedPaths, allowedOrigins);
159
+ if (result.ok)
160
+ return buildResponse(result.location, status, options.headers);
161
+ if (options.fallback !== undefined) {
162
+ if (!options.fallback.startsWith("/") || options.fallback.startsWith("//")) {
163
+ throw new TypeError(`safeRedirect: fallback must be a same-origin path starting with "/"; got ${options.fallback}`);
164
+ }
165
+ return buildResponse(options.fallback, status, options.headers);
166
+ }
167
+ throw new OpenRedirectBlockedError(result.reason, target);
168
+ }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:83195aa9-d821-5a41-9fbb-6c69378e250d",
4
+ "serialNumber": "urn:uuid:3d46c2b6-0b38-564b-9dd2-79098e99155b",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-23T13:34:50.159Z",
7
+ "timestamp": "2026-05-26T23:20:20.188Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "0.34.3"
12
+ "version": "0.35.1"
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@0.34.3",
22
+ "bom-ref": "pkg:npm/@daloyjs/core@0.35.1",
23
23
  "name": "@daloyjs/core",
24
- "version": "0.34.3",
24
+ "version": "0.35.1",
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@0.34.3",
26
+ "purl": "pkg:npm/@daloyjs/core@0.35.1",
27
27
  "licenses": [
28
28
  {
29
29
  "license": {
@@ -39,12 +39,16 @@
39
39
  {
40
40
  "type": "website",
41
41
  "url": "https://daloyjs.dev"
42
+ },
43
+ {
44
+ "type": "distribution",
45
+ "url": "https://www.npmjs.com/package/@daloyjs/core"
42
46
  }
43
47
  ],
44
48
  "swid": {
45
- "tagId": "swidtag--daloyjs-core-0.34.3",
49
+ "tagId": "swidtag--daloyjs-core-0.35.1",
46
50
  "name": "@daloyjs/core",
47
- "version": "0.34.3",
51
+ "version": "0.35.1",
48
52
  "tagVersion": 0,
49
53
  "patch": false
50
54
  }
@@ -53,7 +57,7 @@
53
57
  "components": [],
54
58
  "dependencies": [
55
59
  {
56
- "ref": "pkg:npm/@daloyjs/core@0.34.3",
60
+ "ref": "pkg:npm/@daloyjs/core@0.35.1",
57
61
  "dependsOn": []
58
62
  }
59
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-0.34.3",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.34.3-83195aa9-d821-5a41-9fbb-6c69378e250d",
5
+ "name": "@daloyjs/core-0.35.1",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.35.1-3d46c2b6-0b38-564b-9dd2-79098e99155b",
7
7
  "creationInfo": {
8
- "created": "2026-05-23T13:34:50.159Z",
8
+ "created": "2026-05-26T23:20:20.188Z",
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": "0.34.3",
19
+ "versionInfo": "0.35.1",
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@0.34.3"
30
+ "referenceLocator": "pkg:npm/@daloyjs/core@0.35.1"
31
31
  }
32
32
  ]
33
33
  }
package/dist/schema.d.ts CHANGED
@@ -73,4 +73,3 @@ export declare function validate<S extends StandardSchemaV1>(schema: S, value: u
73
73
  * @since 0.1.0
74
74
  */
75
75
  export declare function isStandardSchema(x: unknown): x is StandardSchemaV1;
76
- //# sourceMappingURL=schema.d.ts.map
package/dist/schema.js CHANGED
@@ -41,4 +41,3 @@ export function isStandardSchema(x) {
41
41
  "~standard" in x &&
42
42
  typeof x["~standard"]?.validate === "function");
43
43
  }
44
- //# sourceMappingURL=schema.js.map
@@ -210,4 +210,3 @@ export declare function oauth2Scheme(options: OAuth2SchemeOptions): OAuth2Scheme
210
210
  * @since 0.1.0
211
211
  */
212
212
  export declare function openIdConnectScheme(options: OpenIdConnectSchemeOptions): OpenIdConnectScheme;
213
- //# sourceMappingURL=security-schemes.d.ts.map
@@ -177,4 +177,3 @@ export function openIdConnectScheme(options) {
177
177
  scheme.description = options.description;
178
178
  return markRequirePayloadAuth(scheme, options);
179
179
  }
180
- //# sourceMappingURL=security-schemes.js.map
@@ -104,8 +104,8 @@ export declare function randomId(): string;
104
104
  /**
105
105
  * Header names that MUST appear at most once on a request per RFC 7230.
106
106
  * Duplicate values for these headers are a classic HTTP-request-smuggling
107
- * vector — front-end and back-end proxies can disagree on which value
108
- * "wins", letting an attacker desync the connection.
107
+ * vector — front-end and back-end proxies can disagree on framing or
108
+ * routing, letting an attacker desync the connection.
109
109
  *
110
110
  * Exported for adapter authors that need the same set.
111
111
  *
@@ -202,6 +202,14 @@ export declare function assertStrongSecret(secret: unknown, scope: string): void
202
202
  * @since 0.15.0
203
203
  */
204
204
  export type WebhookHmacAlgorithm = "sha256" | "sha384" | "sha512";
205
+ /**
206
+ * Default tolerance window (in seconds) for webhook timestamp verification.
207
+ * Matches Stripe / Standard Webhooks defaults — five minutes either side of
208
+ * the receiver's clock. Override per call via `toleranceSeconds`.
209
+ *
210
+ * @since 0.21.0
211
+ */
212
+ export declare const WEBHOOK_DEFAULT_TOLERANCE_SECONDS = 300;
205
213
  /**
206
214
  * Verify an HMAC signature for a webhook payload in constant time.
207
215
  *
@@ -243,6 +251,35 @@ export declare function verifyWebhookSignature(opts: {
243
251
  signature: string | Uint8Array;
244
252
  secret: string | Uint8Array;
245
253
  algorithm?: WebhookHmacAlgorithm;
254
+ /**
255
+ * Optional event timestamp (Unix seconds; string or number). When
256
+ * supplied, the HMAC is computed over `"<timestamp>.<payload>"` instead
257
+ * of just `payload`, which binds the signature to a specific point in
258
+ * time and lets the verifier reject replays whose timestamp drifts
259
+ * outside `toleranceSeconds`. Matches the Stripe / Standard Webhooks
260
+ * convention. Pass the value extracted from the producer's signature
261
+ * header (e.g. the `t=` field of `Stripe-Signature`).
262
+ *
263
+ * @since 0.21.0
264
+ */
265
+ timestamp?: string | number;
266
+ /**
267
+ * Maximum drift (in seconds) between `timestamp` and `now()` before the
268
+ * signature is rejected as a likely replay. Only consulted when
269
+ * `timestamp` is supplied. Defaults to
270
+ * {@link WEBHOOK_DEFAULT_TOLERANCE_SECONDS} (5 minutes).
271
+ *
272
+ * @since 0.21.0
273
+ */
274
+ toleranceSeconds?: number;
275
+ /**
276
+ * Clock used for replay-window checks. Returns milliseconds since the
277
+ * Unix epoch. Defaults to {@link Date.now}; override in tests to make
278
+ * the clock deterministic.
279
+ *
280
+ * @since 0.21.0
281
+ */
282
+ now?: () => number;
246
283
  }): Promise<boolean>;
247
284
  /**
248
285
  * Compute an HMAC signature over `payload` and return it hex-encoded. The
@@ -255,5 +292,99 @@ export declare function signWebhookPayload(opts: {
255
292
  payload: Uint8Array | string;
256
293
  secret: string | Uint8Array;
257
294
  algorithm?: WebhookHmacAlgorithm;
295
+ /**
296
+ * Optional event timestamp (Unix seconds; string or number). When
297
+ * supplied, the HMAC is computed over `"<timestamp>.<payload>"` so the
298
+ * receiver can use {@link verifyWebhookSignature} with the same
299
+ * `timestamp` and a `toleranceSeconds` window to defeat replay attacks.
300
+ *
301
+ * @since 0.21.0
302
+ */
303
+ timestamp?: string | number;
258
304
  }): Promise<string>;
259
- //# sourceMappingURL=security.d.ts.map
305
+ /**
306
+ * Return a single-segment, storage-safe basename derived from a
307
+ * (potentially attacker-controlled) filename — for example, the `name`
308
+ * field on a multipart `UploadedFile` before passing it to `fs.writeFile`
309
+ * or any other filesystem sink.
310
+ *
311
+ * The transformation:
312
+ *
313
+ * 1. Takes the basename only — anything before the last `/` or `\` is
314
+ * discarded (`../../etc/passwd` → `passwd`, `C:\\foo\\bar.txt` →
315
+ * `bar.txt`).
316
+ * 2. Strips NUL bytes and other control characters (NUL truncation is
317
+ * the classic `evil.png\0.exe` bypass).
318
+ * 3. Strips Windows-reserved characters (`<>:"|?*`) and replaces them
319
+ * with `_`.
320
+ * 4. Strips leading dots so the result cannot be `.`, `..`, or a hidden
321
+ * dotfile (`.htaccess` → `htaccess`).
322
+ * 5. Trims trailing dots and spaces (Windows silently strips these on
323
+ * `CreateFile`, so `file.txt ` and `file.txt` collide).
324
+ * 6. Refuses Windows-reserved names (`CON`, `PRN`, `AUX`, `NUL`,
325
+ * `COM1`–`COM9`, `LPT1`–`LPT9`) — case-insensitive, with or without
326
+ * extension.
327
+ * 7. Throws {@link BadRequestError} if the result is empty.
328
+ *
329
+ * Returns a basename only — never includes a directory. Callers are
330
+ * expected to combine it with a trusted base directory via
331
+ * `path.join(baseDir, sanitized)`. Combined with a final
332
+ * `path.resolve(baseDir, sanitized).startsWith(path.resolve(baseDir))`
333
+ * check this fully closes the Zip-Slip and arbitrary-write class of bug.
334
+ *
335
+ * @param name - The candidate filename (typically from
336
+ * `UploadedFile.name`, a `Content-Disposition` header, or any other
337
+ * untrusted source).
338
+ * @returns A sanitized basename safe to combine with a trusted directory.
339
+ * @throws {BadRequestError} When the input reduces to an empty string or
340
+ * matches a Windows-reserved device name.
341
+ * @since 0.35.1
342
+ */
343
+ export declare function sanitizeFilename(name: string): string;
344
+ /**
345
+ * Validate that a candidate relative path is safe to combine with a
346
+ * trusted base directory — i.e. it cannot escape via `..` segments,
347
+ * absolute roots, drive letters, NUL truncation, or mixed
348
+ * POSIX/Windows separators.
349
+ *
350
+ * Throws {@link BadRequestError} on any of:
351
+ *
352
+ * - Empty input
353
+ * - NUL bytes (`evil\0../../etc/passwd` truncation)
354
+ * - Backslash characters (treated as Windows separators; rejecting
355
+ * them prevents POSIX callers being bypassed by `..\\..\\etc`)
356
+ * - POSIX absolute paths (`/etc/passwd`)
357
+ * - Windows drive letters or UNC roots (`C:\\foo`, `\\\\server\\share`)
358
+ * - Any segment equal to `..` after `/`-splitting
359
+ *
360
+ * Returns the normalized POSIX-style relative path on success. Designed
361
+ * for the Aikido / OWASP "Directory Traversal & File Exposure" class
362
+ * (`CVE-2023-26111` `node-static`, Zip Slip, `req.query.file` →
363
+ * `sendFile`).
364
+ *
365
+ * @param input - The candidate relative path.
366
+ * @returns The input unchanged when safe.
367
+ * @throws {BadRequestError} When the path could escape its base directory.
368
+ * @since 0.35.1
369
+ */
370
+ export declare function assertSafeRelativePath(input: string): string;
371
+ /**
372
+ * Recursively scan a parsed JSON value (object / array / scalar) for any
373
+ * property key that starts with `$` — the MongoDB / NoSQL operator
374
+ * namespace (`$ne`, `$gt`, `$regex`, `$where`, …).
375
+ *
376
+ * Returns `true` on the first hit. Use before passing untrusted data
377
+ * into a query object that may be interpreted as an operator expression.
378
+ *
379
+ * @since 0.35.1
380
+ */
381
+ export declare function hasMongoOperatorKeys(value: unknown): boolean;
382
+ /**
383
+ * Throw {@link BadRequestError} when {@link hasMongoOperatorKeys}
384
+ * returns `true`. Safe to call on the parsed request body before
385
+ * threading it into a NoSQL driver — closes the
386
+ * `{"password": {"$ne": null}}` authentication-bypass class of bug.
387
+ *
388
+ * @since 0.35.1
389
+ */
390
+ export declare function assertNoMongoOperators(value: unknown): void;