@graphorin/server 0.5.0

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 (204) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/dist/app.d.ts +174 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +684 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/commentary/audit-bridge.d.ts +57 -0
  9. package/dist/commentary/audit-bridge.d.ts.map +1 -0
  10. package/dist/commentary/audit-bridge.js +92 -0
  11. package/dist/commentary/audit-bridge.js.map +1 -0
  12. package/dist/commentary/built-in-patterns.d.ts +24 -0
  13. package/dist/commentary/built-in-patterns.d.ts.map +1 -0
  14. package/dist/commentary/built-in-patterns.js +62 -0
  15. package/dist/commentary/built-in-patterns.js.map +1 -0
  16. package/dist/commentary/index.d.ts +5 -0
  17. package/dist/commentary/index.js +5 -0
  18. package/dist/commentary/sanitizer.d.ts +37 -0
  19. package/dist/commentary/sanitizer.d.ts.map +1 -0
  20. package/dist/commentary/sanitizer.js +182 -0
  21. package/dist/commentary/sanitizer.js.map +1 -0
  22. package/dist/commentary/types.d.ts +120 -0
  23. package/dist/commentary/types.d.ts.map +1 -0
  24. package/dist/config.d.ts +760 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +217 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/consolidator/daemon.d.ts +88 -0
  29. package/dist/consolidator/daemon.d.ts.map +1 -0
  30. package/dist/consolidator/daemon.js +54 -0
  31. package/dist/consolidator/daemon.js.map +1 -0
  32. package/dist/consolidator/index.d.ts +2 -0
  33. package/dist/consolidator/index.js +3 -0
  34. package/dist/errors/index.d.ts +104 -0
  35. package/dist/errors/index.d.ts.map +1 -0
  36. package/dist/errors/index.js +131 -0
  37. package/dist/errors/index.js.map +1 -0
  38. package/dist/health/checks.d.ts +120 -0
  39. package/dist/health/checks.d.ts.map +1 -0
  40. package/dist/health/checks.js +127 -0
  41. package/dist/health/checks.js.map +1 -0
  42. package/dist/health/index.d.ts +3 -0
  43. package/dist/health/index.js +4 -0
  44. package/dist/health/routes.d.ts +66 -0
  45. package/dist/health/routes.d.ts.map +1 -0
  46. package/dist/health/routes.js +96 -0
  47. package/dist/health/routes.js.map +1 -0
  48. package/dist/index.d.ts +88 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +86 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/internal/context.d.ts +66 -0
  53. package/dist/internal/context.d.ts.map +1 -0
  54. package/dist/internal/ids.js +21 -0
  55. package/dist/internal/ids.js.map +1 -0
  56. package/dist/internal/json.js +46 -0
  57. package/dist/internal/json.js.map +1 -0
  58. package/dist/lifecycle/hooks.d.ts +57 -0
  59. package/dist/lifecycle/hooks.d.ts.map +1 -0
  60. package/dist/lifecycle/index.d.ts +2 -0
  61. package/dist/lifecycle/index.js +3 -0
  62. package/dist/lifecycle/pre-bind.d.ts +38 -0
  63. package/dist/lifecycle/pre-bind.d.ts.map +1 -0
  64. package/dist/lifecycle/pre-bind.js +62 -0
  65. package/dist/lifecycle/pre-bind.js.map +1 -0
  66. package/dist/metrics/catalog.d.ts +34 -0
  67. package/dist/metrics/catalog.d.ts.map +1 -0
  68. package/dist/metrics/catalog.js +61 -0
  69. package/dist/metrics/catalog.js.map +1 -0
  70. package/dist/metrics/index.d.ts +3 -0
  71. package/dist/metrics/index.js +4 -0
  72. package/dist/metrics/registry.d.ts +63 -0
  73. package/dist/metrics/registry.d.ts.map +1 -0
  74. package/dist/metrics/registry.js +222 -0
  75. package/dist/metrics/registry.js.map +1 -0
  76. package/dist/middleware/audit.d.ts +47 -0
  77. package/dist/middleware/audit.d.ts.map +1 -0
  78. package/dist/middleware/audit.js +71 -0
  79. package/dist/middleware/audit.js.map +1 -0
  80. package/dist/middleware/auth.d.ts +50 -0
  81. package/dist/middleware/auth.d.ts.map +1 -0
  82. package/dist/middleware/auth.js +164 -0
  83. package/dist/middleware/auth.js.map +1 -0
  84. package/dist/middleware/cors.d.ts +15 -0
  85. package/dist/middleware/cors.d.ts.map +1 -0
  86. package/dist/middleware/cors.js +45 -0
  87. package/dist/middleware/cors.js.map +1 -0
  88. package/dist/middleware/csrf.d.ts +15 -0
  89. package/dist/middleware/csrf.d.ts.map +1 -0
  90. package/dist/middleware/csrf.js +77 -0
  91. package/dist/middleware/csrf.js.map +1 -0
  92. package/dist/middleware/idempotency.d.ts +41 -0
  93. package/dist/middleware/idempotency.d.ts.map +1 -0
  94. package/dist/middleware/idempotency.js +198 -0
  95. package/dist/middleware/idempotency.js.map +1 -0
  96. package/dist/middleware/index.d.ts +9 -0
  97. package/dist/middleware/index.js +10 -0
  98. package/dist/middleware/rate-limit.d.ts +17 -0
  99. package/dist/middleware/rate-limit.d.ts.map +1 -0
  100. package/dist/middleware/rate-limit.js +47 -0
  101. package/dist/middleware/rate-limit.js.map +1 -0
  102. package/dist/middleware/request-state.d.ts +27 -0
  103. package/dist/middleware/request-state.d.ts.map +1 -0
  104. package/dist/middleware/request-state.js +42 -0
  105. package/dist/middleware/request-state.js.map +1 -0
  106. package/dist/middleware/scope.d.ts +24 -0
  107. package/dist/middleware/scope.d.ts.map +1 -0
  108. package/dist/middleware/scope.js +50 -0
  109. package/dist/middleware/scope.js.map +1 -0
  110. package/dist/registry/index.d.ts +135 -0
  111. package/dist/registry/index.d.ts.map +1 -0
  112. package/dist/registry/index.js +96 -0
  113. package/dist/registry/index.js.map +1 -0
  114. package/dist/replay/index.d.ts +2 -0
  115. package/dist/replay/index.js +3 -0
  116. package/dist/replay/routes.d.ts +46 -0
  117. package/dist/replay/routes.d.ts.map +1 -0
  118. package/dist/replay/routes.js +189 -0
  119. package/dist/replay/routes.js.map +1 -0
  120. package/dist/routes/agents.d.ts +41 -0
  121. package/dist/routes/agents.d.ts.map +1 -0
  122. package/dist/routes/agents.js +213 -0
  123. package/dist/routes/agents.js.map +1 -0
  124. package/dist/routes/audit.d.ts +57 -0
  125. package/dist/routes/audit.d.ts.map +1 -0
  126. package/dist/routes/audit.js +116 -0
  127. package/dist/routes/audit.js.map +1 -0
  128. package/dist/routes/auth.d.ts +26 -0
  129. package/dist/routes/auth.d.ts.map +1 -0
  130. package/dist/routes/auth.js +54 -0
  131. package/dist/routes/auth.js.map +1 -0
  132. package/dist/routes/health.d.ts +22 -0
  133. package/dist/routes/health.d.ts.map +1 -0
  134. package/dist/routes/health.js +33 -0
  135. package/dist/routes/health.js.map +1 -0
  136. package/dist/routes/index.d.ts +10 -0
  137. package/dist/routes/index.js +12 -0
  138. package/dist/routes/mcp.d.ts +32 -0
  139. package/dist/routes/mcp.d.ts.map +1 -0
  140. package/dist/routes/mcp.js +61 -0
  141. package/dist/routes/mcp.js.map +1 -0
  142. package/dist/routes/memory.d.ts +141 -0
  143. package/dist/routes/memory.d.ts.map +1 -0
  144. package/dist/routes/memory.js +102 -0
  145. package/dist/routes/memory.js.map +1 -0
  146. package/dist/routes/sessions.d.ts +54 -0
  147. package/dist/routes/sessions.d.ts.map +1 -0
  148. package/dist/routes/sessions.js +135 -0
  149. package/dist/routes/sessions.js.map +1 -0
  150. package/dist/routes/skills.d.ts +34 -0
  151. package/dist/routes/skills.d.ts.map +1 -0
  152. package/dist/routes/skills.js +54 -0
  153. package/dist/routes/skills.js.map +1 -0
  154. package/dist/routes/tokens.d.ts +25 -0
  155. package/dist/routes/tokens.d.ts.map +1 -0
  156. package/dist/routes/tokens.js +87 -0
  157. package/dist/routes/tokens.js.map +1 -0
  158. package/dist/routes/workflows.d.ts +27 -0
  159. package/dist/routes/workflows.d.ts.map +1 -0
  160. package/dist/routes/workflows.js +220 -0
  161. package/dist/routes/workflows.js.map +1 -0
  162. package/dist/runtime/run-state.d.ts +158 -0
  163. package/dist/runtime/run-state.d.ts.map +1 -0
  164. package/dist/runtime/run-state.js +182 -0
  165. package/dist/runtime/run-state.js.map +1 -0
  166. package/dist/sse/events.d.ts +44 -0
  167. package/dist/sse/events.d.ts.map +1 -0
  168. package/dist/sse/events.js +200 -0
  169. package/dist/sse/events.js.map +1 -0
  170. package/dist/sse/index.d.ts +2 -0
  171. package/dist/sse/index.js +3 -0
  172. package/dist/triggers/daemon.d.ts +74 -0
  173. package/dist/triggers/daemon.d.ts.map +1 -0
  174. package/dist/triggers/daemon.js +114 -0
  175. package/dist/triggers/daemon.js.map +1 -0
  176. package/dist/triggers/index.d.ts +3 -0
  177. package/dist/triggers/index.js +4 -0
  178. package/dist/triggers/routes.d.ts +21 -0
  179. package/dist/triggers/routes.d.ts.map +1 -0
  180. package/dist/triggers/routes.js +117 -0
  181. package/dist/triggers/routes.js.map +1 -0
  182. package/dist/ws/dispatcher.d.ts +169 -0
  183. package/dist/ws/dispatcher.d.ts.map +1 -0
  184. package/dist/ws/dispatcher.js +303 -0
  185. package/dist/ws/dispatcher.js.map +1 -0
  186. package/dist/ws/index.d.ts +6 -0
  187. package/dist/ws/index.js +7 -0
  188. package/dist/ws/replay-buffer.d.ts +47 -0
  189. package/dist/ws/replay-buffer.d.ts.map +1 -0
  190. package/dist/ws/replay-buffer.js +88 -0
  191. package/dist/ws/replay-buffer.js.map +1 -0
  192. package/dist/ws/subjects.d.ts +71 -0
  193. package/dist/ws/subjects.d.ts.map +1 -0
  194. package/dist/ws/subjects.js +112 -0
  195. package/dist/ws/subjects.js.map +1 -0
  196. package/dist/ws/ticket.d.ts +74 -0
  197. package/dist/ws/ticket.d.ts.map +1 -0
  198. package/dist/ws/ticket.js +112 -0
  199. package/dist/ws/ticket.js.map +1 -0
  200. package/dist/ws/upgrade.d.ts +63 -0
  201. package/dist/ws/upgrade.d.ts.map +1 -0
  202. package/dist/ws/upgrade.js +324 -0
  203. package/dist/ws/upgrade.js.map +1 -0
  204. package/package.json +156 -0
@@ -0,0 +1,45 @@
1
+ //#region src/middleware/cors.ts
2
+ /**
3
+ * @stable
4
+ */
5
+ function createCorsMiddleware(config) {
6
+ const allowSet = new Set(config.allowOrigins);
7
+ const allowAny = allowSet.has("*");
8
+ const allowMethods = config.allowMethods.join(", ");
9
+ const allowHeaders = config.allowHeaders.join(", ");
10
+ const maxAge = config.maxAgeSeconds.toString();
11
+ return async (c, next) => {
12
+ const origin = c.req.header("origin");
13
+ const isPreflight = c.req.method === "OPTIONS" && c.req.header("access-control-request-method") !== void 0;
14
+ if (origin === void 0) {
15
+ if (isPreflight) return c.body(null, 204);
16
+ await next();
17
+ return;
18
+ }
19
+ if (!(allowAny || allowSet.has(origin))) {
20
+ if (isPreflight) return c.body(null, 204);
21
+ await next();
22
+ return;
23
+ }
24
+ if (allowAny && config.allowCredentials) {
25
+ c.header("Access-Control-Allow-Origin", origin);
26
+ c.header("Vary", "Origin");
27
+ } else if (allowAny) c.header("Access-Control-Allow-Origin", "*");
28
+ else {
29
+ c.header("Access-Control-Allow-Origin", origin);
30
+ c.header("Vary", "Origin");
31
+ }
32
+ if (config.allowCredentials) c.header("Access-Control-Allow-Credentials", "true");
33
+ if (isPreflight) {
34
+ c.header("Access-Control-Allow-Methods", allowMethods);
35
+ c.header("Access-Control-Allow-Headers", allowHeaders);
36
+ c.header("Access-Control-Max-Age", maxAge);
37
+ return c.body(null, 204);
38
+ }
39
+ await next();
40
+ };
41
+ }
42
+
43
+ //#endregion
44
+ export { createCorsMiddleware };
45
+ //# sourceMappingURL=cors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.js","names":[],"sources":["../../src/middleware/cors.ts"],"sourcesContent":["/**\n * Deny-by-default CORS middleware. The framework refuses to echo back\n * arbitrary origins; allowlist semantics match the threat-model\n * checklist in the runtime architecture.\n *\n * @packageDocumentation\n */\n\nimport type { MiddlewareHandler } from 'hono';\n\nimport type { ServerConfigSpec } from '../config.js';\nimport type { ServerVariables } from '../internal/context.js';\n\n/**\n * @stable\n */\nexport function createCorsMiddleware(\n config: ServerConfigSpec['server']['cors'],\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n const allowSet = new Set(config.allowOrigins);\n const allowAny = allowSet.has('*');\n const allowMethods = config.allowMethods.join(', ');\n const allowHeaders = config.allowHeaders.join(', ');\n const maxAge = config.maxAgeSeconds.toString();\n\n return async (c, next) => {\n const origin = c.req.header('origin');\n const isPreflight =\n c.req.method === 'OPTIONS' && c.req.header('access-control-request-method') !== undefined;\n if (origin === undefined) {\n // Same-origin request — proceed without CORS headers.\n if (isPreflight) {\n return c.body(null, 204);\n }\n await next();\n return;\n }\n const allowed = allowAny || allowSet.has(origin);\n if (!allowed) {\n if (isPreflight) {\n return c.body(null, 204);\n }\n // Pass the request through but withhold the CORS allow header so\n // the browser blocks it; do NOT short-circuit the request — the\n // server-to-server flow must still receive a normal response.\n await next();\n return;\n }\n\n if (allowAny && config.allowCredentials) {\n // Specs forbid `Access-Control-Allow-Origin: *` with credentials;\n // mirror the request origin instead.\n c.header('Access-Control-Allow-Origin', origin);\n c.header('Vary', 'Origin');\n } else if (allowAny) {\n c.header('Access-Control-Allow-Origin', '*');\n } else {\n c.header('Access-Control-Allow-Origin', origin);\n c.header('Vary', 'Origin');\n }\n if (config.allowCredentials) {\n c.header('Access-Control-Allow-Credentials', 'true');\n }\n if (isPreflight) {\n c.header('Access-Control-Allow-Methods', allowMethods);\n c.header('Access-Control-Allow-Headers', allowHeaders);\n c.header('Access-Control-Max-Age', maxAge);\n return c.body(null, 204);\n }\n await next();\n };\n}\n"],"mappings":";;;;AAgBA,SAAgB,qBACd,QACmD;CACnD,MAAM,WAAW,IAAI,IAAI,OAAO,aAAa;CAC7C,MAAM,WAAW,SAAS,IAAI,IAAI;CAClC,MAAM,eAAe,OAAO,aAAa,KAAK,KAAK;CACnD,MAAM,eAAe,OAAO,aAAa,KAAK,KAAK;CACnD,MAAM,SAAS,OAAO,cAAc,UAAU;AAE9C,QAAO,OAAO,GAAG,SAAS;EACxB,MAAM,SAAS,EAAE,IAAI,OAAO,SAAS;EACrC,MAAM,cACJ,EAAE,IAAI,WAAW,aAAa,EAAE,IAAI,OAAO,gCAAgC,KAAK;AAClF,MAAI,WAAW,QAAW;AAExB,OAAI,YACF,QAAO,EAAE,KAAK,MAAM,IAAI;AAE1B,SAAM,MAAM;AACZ;;AAGF,MAAI,EADY,YAAY,SAAS,IAAI,OAAO,GAClC;AACZ,OAAI,YACF,QAAO,EAAE,KAAK,MAAM,IAAI;AAK1B,SAAM,MAAM;AACZ;;AAGF,MAAI,YAAY,OAAO,kBAAkB;AAGvC,KAAE,OAAO,+BAA+B,OAAO;AAC/C,KAAE,OAAO,QAAQ,SAAS;aACjB,SACT,GAAE,OAAO,+BAA+B,IAAI;OACvC;AACL,KAAE,OAAO,+BAA+B,OAAO;AAC/C,KAAE,OAAO,QAAQ,SAAS;;AAE5B,MAAI,OAAO,iBACT,GAAE,OAAO,oCAAoC,OAAO;AAEtD,MAAI,aAAa;AACf,KAAE,OAAO,gCAAgC,aAAa;AACtD,KAAE,OAAO,gCAAgC,aAAa;AACtD,KAAE,OAAO,0BAA0B,OAAO;AAC1C,UAAO,EAAE,KAAK,MAAM,IAAI;;AAE1B,QAAM,MAAM"}
@@ -0,0 +1,15 @@
1
+ import { ServerConfigSpec } from "../config.js";
2
+ import { ServerVariables } from "../internal/context.js";
3
+ import { MiddlewareHandler } from "hono";
4
+
5
+ //#region src/middleware/csrf.d.ts
6
+
7
+ /**
8
+ * @stable
9
+ */
10
+ declare function createCsrfMiddleware(config: ServerConfigSpec['server']['csrf']): MiddlewareHandler<{
11
+ Variables: ServerVariables;
12
+ }>;
13
+ //#endregion
14
+ export { createCsrfMiddleware };
15
+ //# sourceMappingURL=csrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.d.ts","names":[],"sources":["../../src/middleware/csrf.ts"],"sourcesContent":[],"mappings":";;;;;;;;;iBAsBgB,oBAAA,SACN,qCACP;aAA+B"}
@@ -0,0 +1,77 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { Buffer } from "node:buffer";
3
+
4
+ //#region src/middleware/csrf.ts
5
+ /**
6
+ * Double-submit CSRF middleware. Targets browser flows: when a
7
+ * request carries a session-cookie style credential we require the
8
+ * client to echo the value of the `graphorin_csrf` cookie back in
9
+ * the `X-CSRF-Token` header. Bearer-token (Authorization header)
10
+ * requests are exempt because they are not vulnerable to the
11
+ * confused-deputy attack the cookie pattern guards against.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ /**
16
+ * @stable
17
+ */
18
+ function createCsrfMiddleware(config) {
19
+ if (!config.enabled) return async (_, next) => {
20
+ await next();
21
+ };
22
+ const safeMethods = new Set(config.safeMethods.map((m) => m.toUpperCase()));
23
+ return async (c, next) => {
24
+ const method = c.req.method.toUpperCase();
25
+ const cookieValue = parseCookies(c.req.header("cookie")).get(config.cookieName);
26
+ const authHeader = c.req.header("authorization");
27
+ const isBearer = authHeader !== void 0 && /^bearer\s+/i.test(authHeader);
28
+ if (safeMethods.has(method) || isBearer) {
29
+ if (!isBearer && cookieValue === void 0) {
30
+ const token = randomBytes(24).toString("base64url");
31
+ c.header("Set-Cookie", `${config.cookieName}=${token}; Path=/; SameSite=Strict; Secure`);
32
+ }
33
+ await next();
34
+ return;
35
+ }
36
+ if (cookieValue === void 0) return c.json({
37
+ error: "csrf-denied",
38
+ message: "CSRF cookie missing on state-changing request."
39
+ }, 403);
40
+ const headerValue = c.req.header(config.headerName);
41
+ if (headerValue === void 0) return c.json({
42
+ error: "csrf-denied",
43
+ message: `CSRF header '${config.headerName}' missing.`
44
+ }, 403);
45
+ if (!constantTimeEqual(cookieValue, headerValue)) return c.json({
46
+ error: "csrf-denied",
47
+ message: "CSRF token mismatch."
48
+ }, 403);
49
+ await next();
50
+ };
51
+ }
52
+ function parseCookies(header) {
53
+ const out = /* @__PURE__ */ new Map();
54
+ if (header === void 0) return out;
55
+ const parts = header.split(";");
56
+ for (const part of parts) {
57
+ const trimmed = part.trim();
58
+ if (trimmed.length === 0) continue;
59
+ const eq = trimmed.indexOf("=");
60
+ if (eq < 0) continue;
61
+ const name = trimmed.slice(0, eq).trim();
62
+ const value = trimmed.slice(eq + 1).trim();
63
+ out.set(name, value);
64
+ }
65
+ return out;
66
+ }
67
+ function constantTimeEqual(a, b) {
68
+ if (a.length !== b.length) return false;
69
+ const aBuf = Buffer.from(a, "utf8");
70
+ const bBuf = Buffer.from(b, "utf8");
71
+ if (aBuf.length !== bBuf.length) return false;
72
+ return timingSafeEqual(aBuf, bBuf);
73
+ }
74
+
75
+ //#endregion
76
+ export { createCsrfMiddleware };
77
+ //# sourceMappingURL=csrf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.js","names":[],"sources":["../../src/middleware/csrf.ts"],"sourcesContent":["/**\n * Double-submit CSRF middleware. Targets browser flows: when a\n * request carries a session-cookie style credential we require the\n * client to echo the value of the `graphorin_csrf` cookie back in\n * the `X-CSRF-Token` header. Bearer-token (Authorization header)\n * requests are exempt because they are not vulnerable to the\n * confused-deputy attack the cookie pattern guards against.\n *\n * @packageDocumentation\n */\n\nimport { Buffer } from 'node:buffer';\nimport { randomBytes, timingSafeEqual } from 'node:crypto';\n\nimport type { MiddlewareHandler } from 'hono';\n\nimport type { ServerConfigSpec } from '../config.js';\nimport type { ServerVariables } from '../internal/context.js';\n\n/**\n * @stable\n */\nexport function createCsrfMiddleware(\n config: ServerConfigSpec['server']['csrf'],\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n if (!config.enabled) {\n return async (_, next) => {\n await next();\n };\n }\n const safeMethods = new Set(config.safeMethods.map((m) => m.toUpperCase()));\n\n return async (c, next) => {\n const method = c.req.method.toUpperCase();\n const cookieHeader = c.req.header('cookie');\n const cookies = parseCookies(cookieHeader);\n const cookieValue = cookies.get(config.cookieName);\n const authHeader = c.req.header('authorization');\n const isBearer = authHeader !== undefined && /^bearer\\s+/i.test(authHeader);\n\n if (safeMethods.has(method) || isBearer) {\n // Always issue a fresh CSRF cookie on browser flows so SPAs\n // can read it on first load. Skip the cookie issuance for\n // bearer-only callers — they don't need it.\n if (!isBearer && cookieValue === undefined) {\n const token = randomBytes(24).toString('base64url');\n // IP-12: the CSRF token must be readable by the SPA for the\n // double-submit echo, so the cookie carries NO HttpOnly attribute —\n // RFC 6265 enables HttpOnly on its mere presence, even `HttpOnly=false`.\n c.header('Set-Cookie', `${config.cookieName}=${token}; Path=/; SameSite=Strict; Secure`);\n }\n await next();\n return;\n }\n\n if (cookieValue === undefined) {\n return c.json(\n { error: 'csrf-denied', message: 'CSRF cookie missing on state-changing request.' },\n 403,\n );\n }\n const headerValue = c.req.header(config.headerName);\n if (headerValue === undefined) {\n return c.json(\n { error: 'csrf-denied', message: `CSRF header '${config.headerName}' missing.` },\n 403,\n );\n }\n if (!constantTimeEqual(cookieValue, headerValue)) {\n return c.json({ error: 'csrf-denied', message: 'CSRF token mismatch.' }, 403);\n }\n await next();\n };\n}\n\nfunction parseCookies(header: string | undefined): Map<string, string> {\n const out = new Map<string, string>();\n if (header === undefined) return out;\n const parts = header.split(';');\n for (const part of parts) {\n const trimmed = part.trim();\n if (trimmed.length === 0) continue;\n const eq = trimmed.indexOf('=');\n if (eq < 0) continue;\n const name = trimmed.slice(0, eq).trim();\n const value = trimmed.slice(eq + 1).trim();\n out.set(name, value);\n }\n return out;\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return timingSafeEqual(aBuf, bBuf);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,SAAgB,qBACd,QACmD;AACnD,KAAI,CAAC,OAAO,QACV,QAAO,OAAO,GAAG,SAAS;AACxB,QAAM,MAAM;;CAGhB,MAAM,cAAc,IAAI,IAAI,OAAO,YAAY,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3E,QAAO,OAAO,GAAG,SAAS;EACxB,MAAM,SAAS,EAAE,IAAI,OAAO,aAAa;EAGzC,MAAM,cADU,aADK,EAAE,IAAI,OAAO,SAAS,CACD,CACd,IAAI,OAAO,WAAW;EAClD,MAAM,aAAa,EAAE,IAAI,OAAO,gBAAgB;EAChD,MAAM,WAAW,eAAe,UAAa,cAAc,KAAK,WAAW;AAE3E,MAAI,YAAY,IAAI,OAAO,IAAI,UAAU;AAIvC,OAAI,CAAC,YAAY,gBAAgB,QAAW;IAC1C,MAAM,QAAQ,YAAY,GAAG,CAAC,SAAS,YAAY;AAInD,MAAE,OAAO,cAAc,GAAG,OAAO,WAAW,GAAG,MAAM,mCAAmC;;AAE1F,SAAM,MAAM;AACZ;;AAGF,MAAI,gBAAgB,OAClB,QAAO,EAAE,KACP;GAAE,OAAO;GAAe,SAAS;GAAkD,EACnF,IACD;EAEH,MAAM,cAAc,EAAE,IAAI,OAAO,OAAO,WAAW;AACnD,MAAI,gBAAgB,OAClB,QAAO,EAAE,KACP;GAAE,OAAO;GAAe,SAAS,gBAAgB,OAAO,WAAW;GAAa,EAChF,IACD;AAEH,MAAI,CAAC,kBAAkB,aAAa,YAAY,CAC9C,QAAO,EAAE,KAAK;GAAE,OAAO;GAAe,SAAS;GAAwB,EAAE,IAAI;AAE/E,QAAM,MAAM;;;AAIhB,SAAS,aAAa,QAAiD;CACrE,MAAM,sBAAM,IAAI,KAAqB;AACrC,KAAI,WAAW,OAAW,QAAO;CACjC,MAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAC3B,MAAI,QAAQ,WAAW,EAAG;EAC1B,MAAM,KAAK,QAAQ,QAAQ,IAAI;AAC/B,MAAI,KAAK,EAAG;EACZ,MAAM,OAAO,QAAQ,MAAM,GAAG,GAAG,CAAC,MAAM;EACxC,MAAM,QAAQ,QAAQ,MAAM,KAAK,EAAE,CAAC,MAAM;AAC1C,MAAI,IAAI,MAAM,MAAM;;AAEtB,QAAO;;AAGT,SAAS,kBAAkB,GAAW,GAAoB;AACxD,KAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;CAClC,MAAM,OAAO,OAAO,KAAK,GAAG,OAAO;CACnC,MAAM,OAAO,OAAO,KAAK,GAAG,OAAO;AACnC,KAAI,KAAK,WAAW,KAAK,OAAQ,QAAO;AACxC,QAAO,gBAAgB,MAAM,KAAK"}
@@ -0,0 +1,41 @@
1
+ import { ServerConfigSpec } from "../config.js";
2
+ import { ServerVariables } from "../internal/context.js";
3
+ import { MetricRegistry } from "../metrics/registry.js";
4
+ import { IdempotencyStore } from "@graphorin/store-sqlite";
5
+ import { MiddlewareHandler } from "hono";
6
+
7
+ //#region src/middleware/idempotency.d.ts
8
+
9
+ /**
10
+ * Options accepted by {@link createIdempotencyMiddleware}.
11
+ *
12
+ * @stable
13
+ */
14
+ interface IdempotencyMiddlewareOptions {
15
+ /**
16
+ * Paths whose responses are NEVER cached or replayed (IP-6) — the
17
+ * middleware passes straight through. Used for secret-bearing
18
+ * endpoints (`POST /v1/tokens` returns a raw token; caching it would
19
+ * persist the secret plaintext in durable SQLite for the TTL).
20
+ */
21
+ readonly excludeResponseCachePaths?: ReadonlyArray<string>;
22
+ readonly store: IdempotencyStore;
23
+ readonly config: ServerConfigSpec['server']['idempotency'];
24
+ /** Wall-clock provider; tests inject a deterministic generator. */
25
+ readonly now?: () => number;
26
+ /**
27
+ * IP-15: when supplied, the middleware publishes a live
28
+ * `graphorin_idempotency_cache_hit_ratio` gauge (replays / replays+executes)
29
+ * instead of leaving the registered metric a permanently-empty series.
30
+ */
31
+ readonly metricRegistry?: MetricRegistry;
32
+ }
33
+ /**
34
+ * @stable
35
+ */
36
+ declare function createIdempotencyMiddleware(options: IdempotencyMiddlewareOptions): MiddlewareHandler<{
37
+ Variables: ServerVariables;
38
+ }>;
39
+ //#endregion
40
+ export { IdempotencyMiddlewareOptions, createIdempotencyMiddleware };
41
+ //# sourceMappingURL=idempotency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.d.ts","names":[],"sources":["../../src/middleware/idempotency.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;UA2CiB,4BAAA;;;;;;;uCAOsB;kBACrB;mBACC;;;;;;;;4BAQS;;;;;iBAWZ,2BAAA,UACL,+BACR;aAA+B"}
@@ -0,0 +1,198 @@
1
+ import { SERVER_METRIC_NAMES } from "../metrics/catalog.js";
2
+ import { fingerprintRequest } from "../internal/json.js";
3
+
4
+ //#region src/middleware/idempotency.ts
5
+ const HEADER_NAME = "idempotency-key";
6
+ const SAFE_METHODS = new Set([
7
+ "GET",
8
+ "HEAD",
9
+ "OPTIONS",
10
+ "TRACE"
11
+ ]);
12
+ const NON_CACHEABLE_STATUS = new Set([
13
+ 408,
14
+ 425,
15
+ 429,
16
+ 500,
17
+ 502,
18
+ 503,
19
+ 504
20
+ ]);
21
+ const KEY_RE = /^[A-Za-z0-9_\-:]{8,255}$/;
22
+ /**
23
+ * @stable
24
+ */
25
+ function createIdempotencyMiddleware(options) {
26
+ const { store, config } = options;
27
+ if (!config.enabled) return async (_, next) => {
28
+ await next();
29
+ };
30
+ const now = options.now ?? Date.now;
31
+ const lru = new SimpleLru(config.lruCacheSize);
32
+ const excluded = new Set(options.excludeResponseCachePaths ?? []);
33
+ const registry = options.metricRegistry;
34
+ let hits = 0;
35
+ let lookups = 0;
36
+ const recordCacheOutcome = (hit) => {
37
+ if (registry === void 0) return;
38
+ lookups += 1;
39
+ if (hit) hits += 1;
40
+ registry.set(SERVER_METRIC_NAMES.idempotencyCacheHitRatio, hits / lookups);
41
+ };
42
+ return async (c, next) => {
43
+ if (SAFE_METHODS.has(c.req.method.toUpperCase())) {
44
+ await next();
45
+ return;
46
+ }
47
+ if (excluded.has(c.req.path)) {
48
+ await next();
49
+ return;
50
+ }
51
+ const key = c.req.header(HEADER_NAME)?.trim();
52
+ if (key === void 0 || key.length === 0) {
53
+ if (config.requireKey === "enforce") return c.json({
54
+ error: "idempotency-key-required",
55
+ message: "Idempotency-Key header is required for this endpoint.",
56
+ hint: "Set 'Idempotency-Key: <uuid>' on the request."
57
+ }, 400);
58
+ if (config.requireKey === "warn") c.header("Idempotency-Status", "header-missing");
59
+ await next();
60
+ return;
61
+ }
62
+ if (!KEY_RE.test(key)) return c.json({
63
+ error: "idempotency-key-required",
64
+ message: "Idempotency-Key must be 8-255 chars of [A-Za-z0-9_:-]."
65
+ }, 400);
66
+ c.set("state", {
67
+ ...c.get("state"),
68
+ idempotencyKey: key
69
+ });
70
+ const fingerprint = await computeFingerprint(c);
71
+ const cached = lru.get(key);
72
+ let record;
73
+ if (cached !== void 0 && now() - cached.cachedAt < 300 * 1e3) record = cached.record;
74
+ else {
75
+ record = await store.get(key);
76
+ if (record !== null) lru.set(key, {
77
+ record,
78
+ cachedAt: now()
79
+ });
80
+ }
81
+ const auth = c.get("state").auth;
82
+ const principal = auth.kind === "token" ? auth.token.tokenId : "anonymous";
83
+ if (record !== null) {
84
+ if (record.scope !== void 0 && record.scope !== principal) return c.json({
85
+ error: "idempotency-conflict",
86
+ message: `Idempotency-Key '${key}' is bound to a different principal.`
87
+ }, 409);
88
+ if (config.checkBodyFingerprint && record.requestHash !== fingerprint) return c.json({
89
+ error: "idempotency-conflict",
90
+ message: `Idempotency-Key '${key}' was previously used with a different request body.`
91
+ }, 409);
92
+ if (record.expiresAt > now()) {
93
+ c.set("state", {
94
+ ...c.get("state"),
95
+ idempotencyReplay: true
96
+ });
97
+ const headers = record.responseHeaders ?? {};
98
+ for (const [name, value] of Object.entries(headers)) c.header(name, value);
99
+ c.header("Idempotency-Replayed", "true");
100
+ recordCacheOutcome(true);
101
+ return new Response(JSON.stringify(record.response), {
102
+ status: record.statusCode,
103
+ headers: {
104
+ ...headers,
105
+ "Content-Type": "application/json",
106
+ "Idempotency-Replayed": "true"
107
+ }
108
+ });
109
+ }
110
+ lru.delete(key);
111
+ await store.delete(key);
112
+ }
113
+ recordCacheOutcome(false);
114
+ await next();
115
+ const status = c.res.status;
116
+ if (NON_CACHEABLE_STATUS.has(status)) return;
117
+ const responseBody = await captureJsonResponse(c);
118
+ if (responseBody === null) return;
119
+ const responseHeaders = collectResponseHeaders(c);
120
+ const record_ = {
121
+ key,
122
+ requestHash: fingerprint,
123
+ statusCode: status,
124
+ response: responseBody,
125
+ ...responseHeaders !== void 0 ? { responseHeaders } : {},
126
+ scope: principal,
127
+ createdAt: now(),
128
+ expiresAt: now() + config.ttlSeconds * 1e3
129
+ };
130
+ try {
131
+ await store.put(record_);
132
+ lru.set(key, {
133
+ record: record_,
134
+ cachedAt: now()
135
+ });
136
+ } catch {}
137
+ };
138
+ }
139
+ async function computeFingerprint(c) {
140
+ let body = null;
141
+ if (c.req.header("content-type")?.toLowerCase().includes("application/json")) try {
142
+ body = await c.req.json();
143
+ } catch {
144
+ body = await c.req.text();
145
+ }
146
+ else body = await c.req.text();
147
+ return fingerprintRequest(c.req.method, c.req.path, body);
148
+ }
149
+ async function captureJsonResponse(c) {
150
+ if (c.res === void 0) return null;
151
+ if (!(c.res.headers.get("content-type") ?? "").toLowerCase().includes("application/json")) return null;
152
+ try {
153
+ const text = await c.res.clone().text();
154
+ if (text.length === 0) return null;
155
+ return JSON.parse(text);
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+ function collectResponseHeaders(c) {
161
+ if (c.res === void 0) return void 0;
162
+ const out = {};
163
+ c.res.headers.forEach((value, name) => {
164
+ if (name.toLowerCase() === "content-length") return;
165
+ out[name] = value;
166
+ });
167
+ return Object.keys(out).length === 0 ? void 0 : Object.freeze(out);
168
+ }
169
+ var SimpleLru = class {
170
+ #capacity;
171
+ #map = /* @__PURE__ */ new Map();
172
+ constructor(capacity) {
173
+ this.#capacity = Math.max(1, capacity);
174
+ }
175
+ get(key) {
176
+ const value = this.#map.get(key);
177
+ if (value === void 0) return void 0;
178
+ this.#map.delete(key);
179
+ this.#map.set(key, value);
180
+ return value;
181
+ }
182
+ set(key, value) {
183
+ if (this.#map.has(key)) this.#map.delete(key);
184
+ this.#map.set(key, value);
185
+ while (this.#map.size > this.#capacity) {
186
+ const oldestKey = this.#map.keys().next().value;
187
+ if (oldestKey === void 0) break;
188
+ this.#map.delete(oldestKey);
189
+ }
190
+ }
191
+ delete(key) {
192
+ return this.#map.delete(key);
193
+ }
194
+ };
195
+
196
+ //#endregion
197
+ export { createIdempotencyMiddleware };
198
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.js","names":["record: IdempotencyRecord | null","record_: IdempotencyRecord","body: unknown","out: Record<string, string>","#capacity","#map"],"sources":["../../src/middleware/idempotency.ts"],"sourcesContent":["/**\n * REST `Idempotency-Key` middleware per IETF\n * draft-ietf-httpapi-idempotency-key-header-07. Layers an LRU\n * read-cache over a durable {@link IdempotencyStore} so:\n *\n * - Replays of a successful request return the cached response body\n * AND the original status code, with a synthetic\n * `Idempotency-Replayed: true` header so clients can distinguish\n * the cached path.\n * - Replays of a key with a different request body return `409\n * Conflict` and a typed error body (per ADR-036 / DEC-142).\n * - 5xx + 408 + 429 + 503 responses are NOT cached so transient\n * errors do not pin the operator into a non-recoverable state.\n *\n * Streaming endpoints (`/v1/agents/:id/stream`,\n * `/v1/workflows/:id/execute` when invoked with the `stream=true`\n * query) cache only the initial 202 + `runId` envelope; the actual\n * event stream is replayed via the WebSocket layer (Phase 14b).\n *\n * @packageDocumentation\n */\n\nimport type { IdempotencyRecord, IdempotencyStore } from '@graphorin/store-sqlite';\nimport type { Context, MiddlewareHandler } from 'hono';\n\nimport type { ServerConfigSpec } from '../config.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { fingerprintRequest } from '../internal/json.js';\nimport { SERVER_METRIC_NAMES } from '../metrics/catalog.js';\nimport type { MetricRegistry } from '../metrics/registry.js';\n\nconst HEADER_NAME = 'idempotency-key';\nconst SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);\n// Status codes the IETF draft + Stripe convention exclude from\n// idempotent caching (always retry-eligible).\nconst NON_CACHEABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);\nconst KEY_RE = /^[A-Za-z0-9_\\-:]{8,255}$/;\n\n/**\n * Options accepted by {@link createIdempotencyMiddleware}.\n *\n * @stable\n */\nexport interface IdempotencyMiddlewareOptions {\n /**\n * Paths whose responses are NEVER cached or replayed (IP-6) — the\n * middleware passes straight through. Used for secret-bearing\n * endpoints (`POST /v1/tokens` returns a raw token; caching it would\n * persist the secret plaintext in durable SQLite for the TTL).\n */\n readonly excludeResponseCachePaths?: ReadonlyArray<string>;\n readonly store: IdempotencyStore;\n readonly config: ServerConfigSpec['server']['idempotency'];\n /** Wall-clock provider; tests inject a deterministic generator. */\n readonly now?: () => number;\n /**\n * IP-15: when supplied, the middleware publishes a live\n * `graphorin_idempotency_cache_hit_ratio` gauge (replays / replays+executes)\n * instead of leaving the registered metric a permanently-empty series.\n */\n readonly metricRegistry?: MetricRegistry;\n}\n\ninterface CacheEntry {\n readonly record: IdempotencyRecord;\n readonly cachedAt: number;\n}\n\n/**\n * @stable\n */\nexport function createIdempotencyMiddleware(\n options: IdempotencyMiddlewareOptions,\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n const { store, config } = options;\n if (!config.enabled) {\n return async (_, next) => {\n await next();\n };\n }\n const now = options.now ?? Date.now;\n const lru = new SimpleLru<string, CacheEntry>(config.lruCacheSize);\n\n const excluded = new Set(options.excludeResponseCachePaths ?? []);\n\n // IP-15: publish the cache hit ratio as a live gauge. A \"hit\" is a replay\n // served from a stored record; a \"miss\" is a keyed request that executed\n // fresh. Conflicts (409) are abnormal and excluded from the ratio.\n const registry = options.metricRegistry;\n let hits = 0;\n let lookups = 0;\n const recordCacheOutcome = (hit: boolean): void => {\n if (registry === undefined) return;\n lookups += 1;\n if (hit) hits += 1;\n registry.set(SERVER_METRIC_NAMES.idempotencyCacheHitRatio, hits / lookups);\n };\n\n return async (c, next) => {\n if (SAFE_METHODS.has(c.req.method.toUpperCase())) {\n await next();\n return;\n }\n // IP-6: secret-bearing endpoints opt out of response caching\n // entirely — repeats re-execute and no body is ever persisted.\n if (excluded.has(c.req.path)) {\n await next();\n return;\n }\n const key = c.req.header(HEADER_NAME)?.trim();\n if (key === undefined || key.length === 0) {\n if (config.requireKey === 'enforce') {\n return c.json(\n {\n error: 'idempotency-key-required',\n message: 'Idempotency-Key header is required for this endpoint.',\n hint: \"Set 'Idempotency-Key: <uuid>' on the request.\",\n },\n 400,\n );\n }\n // 'warn' / 'off' — pass-through with a hint header.\n if (config.requireKey === 'warn') {\n c.header('Idempotency-Status', 'header-missing');\n }\n await next();\n return;\n }\n if (!KEY_RE.test(key)) {\n return c.json(\n {\n error: 'idempotency-key-required',\n message: 'Idempotency-Key must be 8-255 chars of [A-Za-z0-9_:-].',\n },\n 400,\n );\n }\n c.set('state', { ...c.get('state'), idempotencyKey: key });\n const fingerprint = await computeFingerprint(c);\n const cached = lru.get(key);\n let record: IdempotencyRecord | null;\n if (cached !== undefined && now() - cached.cachedAt < 5 * 60 * 1000) {\n record = cached.record;\n } else {\n record = await store.get(key);\n if (record !== null) {\n lru.set(key, { record, cachedAt: now() });\n }\n }\n // IP-6: the verified principal the record is bound to. Replays are\n // served only to the SAME principal that originally executed the\n // request (who passed the route's scope checks at execute time) —\n // a different token cannot fetch someone else's cached response,\n // closing the scope-bypass the pre-router mount opened.\n const auth = c.get('state').auth;\n const principal = auth.kind === 'token' ? auth.token.tokenId : 'anonymous';\n if (record !== null) {\n if (record.scope !== undefined && record.scope !== principal) {\n return c.json(\n {\n error: 'idempotency-conflict',\n message: `Idempotency-Key '${key}' is bound to a different principal.`,\n },\n 409,\n );\n }\n if (config.checkBodyFingerprint && record.requestHash !== fingerprint) {\n return c.json(\n {\n error: 'idempotency-conflict',\n message: `Idempotency-Key '${key}' was previously used with a different request body.`,\n },\n 409,\n );\n }\n if (record.expiresAt > now()) {\n c.set('state', { ...c.get('state'), idempotencyReplay: true });\n const headers = record.responseHeaders ?? {};\n for (const [name, value] of Object.entries(headers)) {\n c.header(name, value);\n }\n c.header('Idempotency-Replayed', 'true');\n recordCacheOutcome(true);\n return new Response(JSON.stringify(record.response), {\n status: record.statusCode,\n headers: {\n ...headers,\n 'Content-Type': 'application/json',\n 'Idempotency-Replayed': 'true',\n },\n });\n }\n // Expired — fall through to re-execute.\n lru.delete(key);\n await store.delete(key);\n }\n\n // A keyed request that reaches execution is a cache miss.\n recordCacheOutcome(false);\n await next();\n\n const status = c.res.status;\n if (NON_CACHEABLE_STATUS.has(status)) {\n // Retry-eligible — leave nothing in the cache.\n return;\n }\n const responseBody = await captureJsonResponse(c);\n if (responseBody === null) {\n // Non-JSON or empty body — skip caching to keep the schema\n // simple; clients that need response replay can use bodied\n // POSTs.\n return;\n }\n const responseHeaders = collectResponseHeaders(c);\n const record_: IdempotencyRecord = {\n key,\n requestHash: fingerprint,\n statusCode: status,\n response: responseBody,\n ...(responseHeaders !== undefined ? { responseHeaders } : {}),\n // IP-6: bind the record to the executing principal.\n scope: principal,\n createdAt: now(),\n expiresAt: now() + config.ttlSeconds * 1000,\n };\n try {\n await store.put(record_);\n lru.set(key, { record: record_, cachedAt: now() });\n } catch {\n // Best-effort cache. Persistence failures must not break the\n // hot path — operators see them via the audit log + logger.\n }\n };\n}\n\nasync function computeFingerprint(c: Context<{ Variables: ServerVariables }>): Promise<string> {\n let body: unknown = null;\n const ct = c.req.header('content-type');\n if (ct?.toLowerCase().includes('application/json')) {\n try {\n body = await c.req.json();\n } catch {\n body = await c.req.text();\n }\n } else {\n body = await c.req.text();\n }\n return fingerprintRequest(c.req.method, c.req.path, body);\n}\n\nasync function captureJsonResponse(\n c: Context<{ Variables: ServerVariables }>,\n): Promise<unknown | null> {\n if (c.res === undefined) return null;\n const contentType = c.res.headers.get('content-type') ?? '';\n if (!contentType.toLowerCase().includes('application/json')) return null;\n try {\n const cloned = c.res.clone();\n const text = await cloned.text();\n if (text.length === 0) return null;\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\nfunction collectResponseHeaders(\n c: Context<{ Variables: ServerVariables }>,\n): Readonly<Record<string, string>> | undefined {\n if (c.res === undefined) return undefined;\n const out: Record<string, string> = {};\n c.res.headers.forEach((value, name) => {\n if (name.toLowerCase() === 'content-length') return;\n out[name] = value;\n });\n return Object.keys(out).length === 0 ? undefined : Object.freeze(out);\n}\n\nclass SimpleLru<K, V> {\n readonly #capacity: number;\n readonly #map: Map<K, V> = new Map();\n\n constructor(capacity: number) {\n this.#capacity = Math.max(1, capacity);\n }\n\n get(key: K): V | undefined {\n const value = this.#map.get(key);\n if (value === undefined) return undefined;\n this.#map.delete(key);\n this.#map.set(key, value);\n return value;\n }\n\n set(key: K, value: V): void {\n if (this.#map.has(key)) this.#map.delete(key);\n this.#map.set(key, value);\n while (this.#map.size > this.#capacity) {\n const oldestKey = this.#map.keys().next().value as K | undefined;\n if (oldestKey === undefined) break;\n this.#map.delete(oldestKey);\n }\n }\n\n delete(key: K): boolean {\n return this.#map.delete(key);\n }\n}\n"],"mappings":";;;;AA+BA,MAAM,cAAc;AACpB,MAAM,eAAe,IAAI,IAAI;CAAC;CAAO;CAAQ;CAAW;CAAQ,CAAC;AAGjE,MAAM,uBAAuB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAI,CAAC;AACzE,MAAM,SAAS;;;;AAmCf,SAAgB,4BACd,SACmD;CACnD,MAAM,EAAE,OAAO,WAAW;AAC1B,KAAI,CAAC,OAAO,QACV,QAAO,OAAO,GAAG,SAAS;AACxB,QAAM,MAAM;;CAGhB,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,MAAM,IAAI,UAA8B,OAAO,aAAa;CAElE,MAAM,WAAW,IAAI,IAAI,QAAQ,6BAA6B,EAAE,CAAC;CAKjE,MAAM,WAAW,QAAQ;CACzB,IAAI,OAAO;CACX,IAAI,UAAU;CACd,MAAM,sBAAsB,QAAuB;AACjD,MAAI,aAAa,OAAW;AAC5B,aAAW;AACX,MAAI,IAAK,SAAQ;AACjB,WAAS,IAAI,oBAAoB,0BAA0B,OAAO,QAAQ;;AAG5E,QAAO,OAAO,GAAG,SAAS;AACxB,MAAI,aAAa,IAAI,EAAE,IAAI,OAAO,aAAa,CAAC,EAAE;AAChD,SAAM,MAAM;AACZ;;AAIF,MAAI,SAAS,IAAI,EAAE,IAAI,KAAK,EAAE;AAC5B,SAAM,MAAM;AACZ;;EAEF,MAAM,MAAM,EAAE,IAAI,OAAO,YAAY,EAAE,MAAM;AAC7C,MAAI,QAAQ,UAAa,IAAI,WAAW,GAAG;AACzC,OAAI,OAAO,eAAe,UACxB,QAAO,EAAE,KACP;IACE,OAAO;IACP,SAAS;IACT,MAAM;IACP,EACD,IACD;AAGH,OAAI,OAAO,eAAe,OACxB,GAAE,OAAO,sBAAsB,iBAAiB;AAElD,SAAM,MAAM;AACZ;;AAEF,MAAI,CAAC,OAAO,KAAK,IAAI,CACnB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;AAEH,IAAE,IAAI,SAAS;GAAE,GAAG,EAAE,IAAI,QAAQ;GAAE,gBAAgB;GAAK,CAAC;EAC1D,MAAM,cAAc,MAAM,mBAAmB,EAAE;EAC/C,MAAM,SAAS,IAAI,IAAI,IAAI;EAC3B,IAAIA;AACJ,MAAI,WAAW,UAAa,KAAK,GAAG,OAAO,WAAW,MAAS,IAC7D,UAAS,OAAO;OACX;AACL,YAAS,MAAM,MAAM,IAAI,IAAI;AAC7B,OAAI,WAAW,KACb,KAAI,IAAI,KAAK;IAAE;IAAQ,UAAU,KAAK;IAAE,CAAC;;EAQ7C,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;EAC5B,MAAM,YAAY,KAAK,SAAS,UAAU,KAAK,MAAM,UAAU;AAC/D,MAAI,WAAW,MAAM;AACnB,OAAI,OAAO,UAAU,UAAa,OAAO,UAAU,UACjD,QAAO,EAAE,KACP;IACE,OAAO;IACP,SAAS,oBAAoB,IAAI;IAClC,EACD,IACD;AAEH,OAAI,OAAO,wBAAwB,OAAO,gBAAgB,YACxD,QAAO,EAAE,KACP;IACE,OAAO;IACP,SAAS,oBAAoB,IAAI;IAClC,EACD,IACD;AAEH,OAAI,OAAO,YAAY,KAAK,EAAE;AAC5B,MAAE,IAAI,SAAS;KAAE,GAAG,EAAE,IAAI,QAAQ;KAAE,mBAAmB;KAAM,CAAC;IAC9D,MAAM,UAAU,OAAO,mBAAmB,EAAE;AAC5C,SAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,CACjD,GAAE,OAAO,MAAM,MAAM;AAEvB,MAAE,OAAO,wBAAwB,OAAO;AACxC,uBAAmB,KAAK;AACxB,WAAO,IAAI,SAAS,KAAK,UAAU,OAAO,SAAS,EAAE;KACnD,QAAQ,OAAO;KACf,SAAS;MACP,GAAG;MACH,gBAAgB;MAChB,wBAAwB;MACzB;KACF,CAAC;;AAGJ,OAAI,OAAO,IAAI;AACf,SAAM,MAAM,OAAO,IAAI;;AAIzB,qBAAmB,MAAM;AACzB,QAAM,MAAM;EAEZ,MAAM,SAAS,EAAE,IAAI;AACrB,MAAI,qBAAqB,IAAI,OAAO,CAElC;EAEF,MAAM,eAAe,MAAM,oBAAoB,EAAE;AACjD,MAAI,iBAAiB,KAInB;EAEF,MAAM,kBAAkB,uBAAuB,EAAE;EACjD,MAAMC,UAA6B;GACjC;GACA,aAAa;GACb,YAAY;GACZ,UAAU;GACV,GAAI,oBAAoB,SAAY,EAAE,iBAAiB,GAAG,EAAE;GAE5D,OAAO;GACP,WAAW,KAAK;GAChB,WAAW,KAAK,GAAG,OAAO,aAAa;GACxC;AACD,MAAI;AACF,SAAM,MAAM,IAAI,QAAQ;AACxB,OAAI,IAAI,KAAK;IAAE,QAAQ;IAAS,UAAU,KAAK;IAAE,CAAC;UAC5C;;;AAOZ,eAAe,mBAAmB,GAA6D;CAC7F,IAAIC,OAAgB;AAEpB,KADW,EAAE,IAAI,OAAO,eAAe,EAC/B,aAAa,CAAC,SAAS,mBAAmB,CAChD,KAAI;AACF,SAAO,MAAM,EAAE,IAAI,MAAM;SACnB;AACN,SAAO,MAAM,EAAE,IAAI,MAAM;;KAG3B,QAAO,MAAM,EAAE,IAAI,MAAM;AAE3B,QAAO,mBAAmB,EAAE,IAAI,QAAQ,EAAE,IAAI,MAAM,KAAK;;AAG3D,eAAe,oBACb,GACyB;AACzB,KAAI,EAAE,QAAQ,OAAW,QAAO;AAEhC,KAAI,EADgB,EAAE,IAAI,QAAQ,IAAI,eAAe,IAAI,IACxC,aAAa,CAAC,SAAS,mBAAmB,CAAE,QAAO;AACpE,KAAI;EAEF,MAAM,OAAO,MADE,EAAE,IAAI,OAAO,CACF,MAAM;AAChC,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,SAAO,KAAK,MAAM,KAAK;SACjB;AACN,SAAO;;;AAIX,SAAS,uBACP,GAC8C;AAC9C,KAAI,EAAE,QAAQ,OAAW,QAAO;CAChC,MAAMC,MAA8B,EAAE;AACtC,GAAE,IAAI,QAAQ,SAAS,OAAO,SAAS;AACrC,MAAI,KAAK,aAAa,KAAK,iBAAkB;AAC7C,MAAI,QAAQ;GACZ;AACF,QAAO,OAAO,KAAK,IAAI,CAAC,WAAW,IAAI,SAAY,OAAO,OAAO,IAAI;;AAGvE,IAAM,YAAN,MAAsB;CACpB,CAASC;CACT,CAASC,sBAAkB,IAAI,KAAK;CAEpC,YAAY,UAAkB;AAC5B,QAAKD,WAAY,KAAK,IAAI,GAAG,SAAS;;CAGxC,IAAI,KAAuB;EACzB,MAAM,QAAQ,MAAKC,IAAK,IAAI,IAAI;AAChC,MAAI,UAAU,OAAW,QAAO;AAChC,QAAKA,IAAK,OAAO,IAAI;AACrB,QAAKA,IAAK,IAAI,KAAK,MAAM;AACzB,SAAO;;CAGT,IAAI,KAAQ,OAAgB;AAC1B,MAAI,MAAKA,IAAK,IAAI,IAAI,CAAE,OAAKA,IAAK,OAAO,IAAI;AAC7C,QAAKA,IAAK,IAAI,KAAK,MAAM;AACzB,SAAO,MAAKA,IAAK,OAAO,MAAKD,UAAW;GACtC,MAAM,YAAY,MAAKC,IAAK,MAAM,CAAC,MAAM,CAAC;AAC1C,OAAI,cAAc,OAAW;AAC7B,SAAKA,IAAK,OAAO,UAAU;;;CAI/B,OAAO,KAAiB;AACtB,SAAO,MAAKA,IAAK,OAAO,IAAI"}
@@ -0,0 +1,9 @@
1
+ import { AuditErrorSink, AuditMiddlewareOptions, HTTP_REQUEST_AUDIT_ACTION, createAuditMiddleware } from "./audit.js";
2
+ import { AuthMiddlewareOptions, createAnonymousAuthMiddleware, createAuthMiddleware } from "./auth.js";
3
+ import { createCorsMiddleware } from "./cors.js";
4
+ import { createCsrfMiddleware } from "./csrf.js";
5
+ import { IdempotencyMiddlewareOptions, createIdempotencyMiddleware } from "./idempotency.js";
6
+ import { createRateLimitMiddleware } from "./rate-limit.js";
7
+ import { RequestStateMiddlewareOptions, createRequestStateMiddleware } from "./request-state.js";
8
+ import { ScopeRequirement, createScopeMiddleware } from "./scope.js";
9
+ export { type AuditErrorSink, type AuditMiddlewareOptions, type AuthMiddlewareOptions, HTTP_REQUEST_AUDIT_ACTION, type IdempotencyMiddlewareOptions, type RequestStateMiddlewareOptions, type ScopeRequirement, createAnonymousAuthMiddleware, createAuditMiddleware, createAuthMiddleware, createCorsMiddleware, createCsrfMiddleware, createIdempotencyMiddleware, createRateLimitMiddleware, createRequestStateMiddleware, createScopeMiddleware };
@@ -0,0 +1,10 @@
1
+ import { createScopeMiddleware } from "./scope.js";
2
+ import { HTTP_REQUEST_AUDIT_ACTION, createAuditMiddleware } from "./audit.js";
3
+ import { createAnonymousAuthMiddleware, createAuthMiddleware } from "./auth.js";
4
+ import { createCorsMiddleware } from "./cors.js";
5
+ import { createCsrfMiddleware } from "./csrf.js";
6
+ import { createIdempotencyMiddleware } from "./idempotency.js";
7
+ import { createRateLimitMiddleware } from "./rate-limit.js";
8
+ import { createRequestStateMiddleware } from "./request-state.js";
9
+
10
+ export { HTTP_REQUEST_AUDIT_ACTION, createAnonymousAuthMiddleware, createAuditMiddleware, createAuthMiddleware, createCorsMiddleware, createCsrfMiddleware, createIdempotencyMiddleware, createRateLimitMiddleware, createRequestStateMiddleware, createScopeMiddleware };
@@ -0,0 +1,17 @@
1
+ import { ServerConfigSpec } from "../config.js";
2
+ import { ServerVariables } from "../internal/context.js";
3
+ import { MiddlewareHandler } from "hono";
4
+
5
+ //#region src/middleware/rate-limit.d.ts
6
+
7
+ /**
8
+ * @stable
9
+ */
10
+ declare function createRateLimitMiddleware(config: ServerConfigSpec['server']['rateLimit'], options?: {
11
+ readonly now?: () => number;
12
+ }): MiddlewareHandler<{
13
+ Variables: ServerVariables;
14
+ }>;
15
+ //#endregion
16
+ export { createRateLimitMiddleware };
17
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":[],"mappings":";;;;;;;;;iBAsBgB,yBAAA,SACN;;IAEP;aAA+B"}
@@ -0,0 +1,47 @@
1
+ //#region src/middleware/rate-limit.ts
2
+ /**
3
+ * @stable
4
+ */
5
+ function createRateLimitMiddleware(config, options = {}) {
6
+ if (!config.enabled) return async (_, next) => {
7
+ await next();
8
+ };
9
+ const windows = /* @__PURE__ */ new Map();
10
+ const now = options.now ?? Date.now;
11
+ const SWEEP_THRESHOLD = 1e4;
12
+ const sweep = (ts) => {
13
+ if (windows.size < SWEEP_THRESHOLD) return;
14
+ for (const [key, win] of windows) if (ts - win.start >= config.windowMs) windows.delete(key);
15
+ };
16
+ return async (c, next) => {
17
+ const ip = c.get("state").clientIp ?? "anonymous";
18
+ const ts = now();
19
+ sweep(ts);
20
+ const window = windows.get(ip) ?? {
21
+ count: 0,
22
+ start: ts
23
+ };
24
+ if (ts - window.start >= config.windowMs) {
25
+ window.start = ts;
26
+ window.count = 0;
27
+ }
28
+ window.count += 1;
29
+ windows.set(ip, window);
30
+ const remaining = Math.max(0, config.perIpRequests - window.count);
31
+ c.header("X-RateLimit-Limit", config.perIpRequests.toString());
32
+ c.header("X-RateLimit-Remaining", remaining.toString());
33
+ c.header("X-RateLimit-Reset", Math.max(0, Math.ceil((window.start + config.windowMs - ts) / 1e3)).toString());
34
+ if (window.count > config.perIpRequests) {
35
+ const retryAfter = Math.max(0, Math.ceil((window.start + config.windowMs - ts) / 1e3));
36
+ return c.json({
37
+ error: "rate-limit-exceeded",
38
+ message: "Too many requests."
39
+ }, 429, { "Retry-After": retryAfter.toString() });
40
+ }
41
+ await next();
42
+ };
43
+ }
44
+
45
+ //#endregion
46
+ export { createRateLimitMiddleware };
47
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":["/**\n * Per-IP sliding-window rate limit. Defends against brute-force\n * authentication probes and naive DoS scenarios. The limit is\n * deliberately conservative — operators with multi-tenant traffic\n * should layer a dedicated reverse proxy on top.\n *\n * @packageDocumentation\n */\n\nimport type { MiddlewareHandler } from 'hono';\n\nimport type { ServerConfigSpec } from '../config.js';\nimport type { ServerVariables } from '../internal/context.js';\n\ninterface Window {\n count: number;\n start: number;\n}\n\n/**\n * @stable\n */\nexport function createRateLimitMiddleware(\n config: ServerConfigSpec['server']['rateLimit'],\n options: { readonly now?: () => number } = {},\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n if (!config.enabled) {\n return async (_, next) => {\n await next();\n };\n }\n const windows = new Map<string, Window>();\n const now = options.now ?? Date.now;\n // IP-10: bound the window map — expired entries are swept once the\n // map crosses the cap so an attacker rotating spoofed XFF values\n // (trustProxy=true deployments) cannot grow it without bound.\n const SWEEP_THRESHOLD = 10_000;\n const sweep = (ts: number): void => {\n if (windows.size < SWEEP_THRESHOLD) return;\n for (const [key, win] of windows) {\n if (ts - win.start >= config.windowMs) windows.delete(key);\n }\n };\n\n return async (c, next) => {\n const ip = c.get('state').clientIp ?? 'anonymous';\n const ts = now();\n sweep(ts);\n const window = windows.get(ip) ?? { count: 0, start: ts };\n if (ts - window.start >= config.windowMs) {\n window.start = ts;\n window.count = 0;\n }\n window.count += 1;\n windows.set(ip, window);\n const remaining = Math.max(0, config.perIpRequests - window.count);\n c.header('X-RateLimit-Limit', config.perIpRequests.toString());\n c.header('X-RateLimit-Remaining', remaining.toString());\n c.header(\n 'X-RateLimit-Reset',\n Math.max(0, Math.ceil((window.start + config.windowMs - ts) / 1000)).toString(),\n );\n if (window.count > config.perIpRequests) {\n const retryAfter = Math.max(0, Math.ceil((window.start + config.windowMs - ts) / 1000));\n return c.json({ error: 'rate-limit-exceeded', message: 'Too many requests.' }, 429, {\n 'Retry-After': retryAfter.toString(),\n });\n }\n await next();\n };\n}\n"],"mappings":";;;;AAsBA,SAAgB,0BACd,QACA,UAA2C,EAAE,EACM;AACnD,KAAI,CAAC,OAAO,QACV,QAAO,OAAO,GAAG,SAAS;AACxB,QAAM,MAAM;;CAGhB,MAAM,0BAAU,IAAI,KAAqB;CACzC,MAAM,MAAM,QAAQ,OAAO,KAAK;CAIhC,MAAM,kBAAkB;CACxB,MAAM,SAAS,OAAqB;AAClC,MAAI,QAAQ,OAAO,gBAAiB;AACpC,OAAK,MAAM,CAAC,KAAK,QAAQ,QACvB,KAAI,KAAK,IAAI,SAAS,OAAO,SAAU,SAAQ,OAAO,IAAI;;AAI9D,QAAO,OAAO,GAAG,SAAS;EACxB,MAAM,KAAK,EAAE,IAAI,QAAQ,CAAC,YAAY;EACtC,MAAM,KAAK,KAAK;AAChB,QAAM,GAAG;EACT,MAAM,SAAS,QAAQ,IAAI,GAAG,IAAI;GAAE,OAAO;GAAG,OAAO;GAAI;AACzD,MAAI,KAAK,OAAO,SAAS,OAAO,UAAU;AACxC,UAAO,QAAQ;AACf,UAAO,QAAQ;;AAEjB,SAAO,SAAS;AAChB,UAAQ,IAAI,IAAI,OAAO;EACvB,MAAM,YAAY,KAAK,IAAI,GAAG,OAAO,gBAAgB,OAAO,MAAM;AAClE,IAAE,OAAO,qBAAqB,OAAO,cAAc,UAAU,CAAC;AAC9D,IAAE,OAAO,yBAAyB,UAAU,UAAU,CAAC;AACvD,IAAE,OACA,qBACA,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,OAAO,WAAW,MAAM,IAAK,CAAC,CAAC,UAAU,CAChF;AACD,MAAI,OAAO,QAAQ,OAAO,eAAe;GACvC,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,OAAO,WAAW,MAAM,IAAK,CAAC;AACvF,UAAO,EAAE,KAAK;IAAE,OAAO;IAAuB,SAAS;IAAsB,EAAE,KAAK,EAClF,eAAe,WAAW,UAAU,EACrC,CAAC;;AAEJ,QAAM,MAAM"}
@@ -0,0 +1,27 @@
1
+ import { ServerVariables } from "../internal/context.js";
2
+ import { MiddlewareHandler } from "hono";
3
+
4
+ //#region src/middleware/request-state.d.ts
5
+
6
+ /**
7
+ * @stable
8
+ */
9
+ interface RequestStateMiddlewareOptions {
10
+ /** Override the wall clock; tests inject a fixed value. */
11
+ readonly now?: () => number;
12
+ /** Override the request id generator. */
13
+ readonly newRequestId?: () => string;
14
+ /** Trust `X-Forwarded-For`/`X-Real-IP` for the client IP. Default false. */
15
+ readonly trustProxy?: boolean;
16
+ /** Echo the request id back to the client. Default `'X-Request-Id'`. */
17
+ readonly responseHeader?: string | false;
18
+ }
19
+ /**
20
+ * @stable
21
+ */
22
+ declare function createRequestStateMiddleware(options?: RequestStateMiddlewareOptions): MiddlewareHandler<{
23
+ Variables: ServerVariables;
24
+ }>;
25
+ //#endregion
26
+ export { RequestStateMiddlewareOptions, createRequestStateMiddleware };
27
+ //# sourceMappingURL=request-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-state.d.ts","names":[],"sources":["../../src/middleware/request-state.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAgBiB,6BAAA;;;;;;;;;;;;;iBA8BD,4BAAA,WACL,gCACR;aAA+B"}
@@ -0,0 +1,42 @@
1
+ import { newRequestId } from "../internal/ids.js";
2
+
3
+ //#region src/middleware/request-state.ts
4
+ function clientIp(headerLookup, trustProxy) {
5
+ if (trustProxy) {
6
+ const xff = headerLookup("x-forwarded-for");
7
+ if (xff !== void 0 && xff.length > 0) {
8
+ const [first] = xff.split(",");
9
+ if (first !== void 0) return first.trim();
10
+ }
11
+ const xri = headerLookup("x-real-ip");
12
+ if (xri !== void 0 && xri.length > 0) return xri.trim();
13
+ }
14
+ }
15
+ /**
16
+ * @stable
17
+ */
18
+ function createRequestStateMiddleware(options = {}) {
19
+ const now = options.now ?? Date.now;
20
+ const idFactory = options.newRequestId ?? newRequestId;
21
+ const trustProxy = options.trustProxy ?? false;
22
+ const responseHeader = options.responseHeader === void 0 ? "X-Request-Id" : options.responseHeader;
23
+ return async (c, next) => {
24
+ const headerLookup = (name) => c.req.header(name);
25
+ const requestId = headerLookup("x-request-id")?.trim() || idFactory();
26
+ const socketAddress = c.env?.incoming?.socket?.remoteAddress;
27
+ const ip = clientIp(headerLookup, trustProxy) ?? socketAddress;
28
+ const state = {
29
+ requestId,
30
+ receivedAt: now(),
31
+ clientIp: ip,
32
+ auth: { kind: "unauthenticated" }
33
+ };
34
+ c.set("state", state);
35
+ if (responseHeader !== false) c.header(responseHeader, requestId);
36
+ await next();
37
+ };
38
+ }
39
+
40
+ //#endregion
41
+ export { createRequestStateMiddleware };
42
+ //# sourceMappingURL=request-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-state.js","names":["state: ServerRequestState"],"sources":["../../src/middleware/request-state.ts"],"sourcesContent":["/**\n * Bootstraps the per-request `c.var.state` slot. Every server-owned\n * middleware downstream reads from this object; defaults are kept on\n * a single line per field so the structure is auditable at a glance.\n *\n * @packageDocumentation\n */\n\nimport type { MiddlewareHandler } from 'hono';\n\nimport type { ServerRequestState, ServerVariables } from '../internal/context.js';\nimport { newRequestId } from '../internal/ids.js';\n\n/**\n * @stable\n */\nexport interface RequestStateMiddlewareOptions {\n /** Override the wall clock; tests inject a fixed value. */\n readonly now?: () => number;\n /** Override the request id generator. */\n readonly newRequestId?: () => string;\n /** Trust `X-Forwarded-For`/`X-Real-IP` for the client IP. Default false. */\n readonly trustProxy?: boolean;\n /** Echo the request id back to the client. Default `'X-Request-Id'`. */\n readonly responseHeader?: string | false;\n}\n\nfunction clientIp(\n headerLookup: (name: string) => string | undefined,\n trustProxy: boolean,\n): string | undefined {\n if (trustProxy) {\n const xff = headerLookup('x-forwarded-for');\n if (xff !== undefined && xff.length > 0) {\n const [first] = xff.split(',');\n if (first !== undefined) return first.trim();\n }\n const xri = headerLookup('x-real-ip');\n if (xri !== undefined && xri.length > 0) return xri.trim();\n }\n return undefined;\n}\n\n/**\n * @stable\n */\nexport function createRequestStateMiddleware(\n options: RequestStateMiddlewareOptions = {},\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n const now = options.now ?? Date.now;\n const idFactory = options.newRequestId ?? newRequestId;\n const trustProxy = options.trustProxy ?? false;\n const responseHeader =\n options.responseHeader === undefined ? 'X-Request-Id' : options.responseHeader;\n\n return async (c, next) => {\n const headerLookup = (name: string): string | undefined => c.req.header(name);\n const requestId = headerLookup('x-request-id')?.trim() || idFactory();\n // IP-10: with the default trustProxy=false the proxy headers are\n // ignored — the client IP comes from the SOCKET, so per-IP rate\n // limits and lockouts actually key per client instead of dumping\n // every request into one shared 'anonymous' bucket.\n const socketAddress = (\n c.env as { incoming?: { socket?: { remoteAddress?: string } } } | undefined\n )?.incoming?.socket?.remoteAddress;\n const ip = clientIp(headerLookup, trustProxy) ?? socketAddress;\n const state: ServerRequestState = {\n requestId,\n receivedAt: now(),\n clientIp: ip,\n auth: { kind: 'unauthenticated' },\n };\n c.set('state', state);\n if (responseHeader !== false) {\n c.header(responseHeader, requestId);\n }\n await next();\n };\n}\n"],"mappings":";;;AA2BA,SAAS,SACP,cACA,YACoB;AACpB,KAAI,YAAY;EACd,MAAM,MAAM,aAAa,kBAAkB;AAC3C,MAAI,QAAQ,UAAa,IAAI,SAAS,GAAG;GACvC,MAAM,CAAC,SAAS,IAAI,MAAM,IAAI;AAC9B,OAAI,UAAU,OAAW,QAAO,MAAM,MAAM;;EAE9C,MAAM,MAAM,aAAa,YAAY;AACrC,MAAI,QAAQ,UAAa,IAAI,SAAS,EAAG,QAAO,IAAI,MAAM;;;;;;AAQ9D,SAAgB,6BACd,UAAyC,EAAE,EACQ;CACnD,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,YAAY,QAAQ,gBAAgB;CAC1C,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,iBACJ,QAAQ,mBAAmB,SAAY,iBAAiB,QAAQ;AAElE,QAAO,OAAO,GAAG,SAAS;EACxB,MAAM,gBAAgB,SAAqC,EAAE,IAAI,OAAO,KAAK;EAC7E,MAAM,YAAY,aAAa,eAAe,EAAE,MAAM,IAAI,WAAW;EAKrE,MAAM,gBACJ,EAAE,KACD,UAAU,QAAQ;EACrB,MAAM,KAAK,SAAS,cAAc,WAAW,IAAI;EACjD,MAAMA,QAA4B;GAChC;GACA,YAAY,KAAK;GACjB,UAAU;GACV,MAAM,EAAE,MAAM,mBAAmB;GAClC;AACD,IAAE,IAAI,SAAS,MAAM;AACrB,MAAI,mBAAmB,MACrB,GAAE,OAAO,gBAAgB,UAAU;AAErC,QAAM,MAAM"}