@jini-ai/http-kit 0.2.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 (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +107 -0
  3. package/dist/active-context.d.ts +99 -0
  4. package/dist/active-context.d.ts.map +1 -0
  5. package/dist/active-context.js +77 -0
  6. package/dist/active-context.js.map +1 -0
  7. package/dist/adapter.d.ts +36 -0
  8. package/dist/adapter.d.ts.map +1 -0
  9. package/dist/adapter.js +92 -0
  10. package/dist/adapter.js.map +1 -0
  11. package/dist/agents.d.ts +55 -0
  12. package/dist/agents.d.ts.map +1 -0
  13. package/dist/agents.js +23 -0
  14. package/dist/agents.js.map +1 -0
  15. package/dist/api-security-middleware.d.ts +146 -0
  16. package/dist/api-security-middleware.d.ts.map +1 -0
  17. package/dist/api-security-middleware.js +276 -0
  18. package/dist/api-security-middleware.js.map +1 -0
  19. package/dist/attachments.d.ts +317 -0
  20. package/dist/attachments.d.ts.map +1 -0
  21. package/dist/attachments.js +664 -0
  22. package/dist/attachments.js.map +1 -0
  23. package/dist/cancel-owned-runs.d.ts +44 -0
  24. package/dist/cancel-owned-runs.d.ts.map +1 -0
  25. package/dist/cancel-owned-runs.js +35 -0
  26. package/dist/cancel-owned-runs.js.map +1 -0
  27. package/dist/compat.d.ts +22 -0
  28. package/dist/compat.d.ts.map +1 -0
  29. package/dist/compat.js +17 -0
  30. package/dist/compat.js.map +1 -0
  31. package/dist/component-catalog.d.ts +74 -0
  32. package/dist/component-catalog.d.ts.map +1 -0
  33. package/dist/component-catalog.js +74 -0
  34. package/dist/component-catalog.js.map +1 -0
  35. package/dist/connectors.d.ts +165 -0
  36. package/dist/connectors.d.ts.map +1 -0
  37. package/dist/connectors.js +474 -0
  38. package/dist/connectors.js.map +1 -0
  39. package/dist/daemon-status.d.ts +72 -0
  40. package/dist/daemon-status.d.ts.map +1 -0
  41. package/dist/daemon-status.js +60 -0
  42. package/dist/daemon-status.js.map +1 -0
  43. package/dist/db-ops.d.ts +116 -0
  44. package/dist/db-ops.d.ts.map +1 -0
  45. package/dist/db-ops.js +191 -0
  46. package/dist/db-ops.js.map +1 -0
  47. package/dist/delegated-tools.d.ts +46 -0
  48. package/dist/delegated-tools.d.ts.map +1 -0
  49. package/dist/delegated-tools.js +155 -0
  50. package/dist/delegated-tools.js.map +1 -0
  51. package/dist/express/run-stream.d.ts +15 -0
  52. package/dist/express/run-stream.d.ts.map +1 -0
  53. package/dist/express/run-stream.js +11 -0
  54. package/dist/express/run-stream.js.map +1 -0
  55. package/dist/express-index.d.ts +32 -0
  56. package/dist/express-index.d.ts.map +1 -0
  57. package/dist/express-index.js +14 -0
  58. package/dist/express-index.js.map +1 -0
  59. package/dist/fastify/adapter.d.ts +31 -0
  60. package/dist/fastify/adapter.d.ts.map +1 -0
  61. package/dist/fastify/adapter.js +64 -0
  62. package/dist/fastify/adapter.js.map +1 -0
  63. package/dist/fastify/agents.d.ts +13 -0
  64. package/dist/fastify/agents.d.ts.map +1 -0
  65. package/dist/fastify/agents.js +7 -0
  66. package/dist/fastify/agents.js.map +1 -0
  67. package/dist/fastify/api-security-middleware.d.ts +64 -0
  68. package/dist/fastify/api-security-middleware.d.ts.map +1 -0
  69. package/dist/fastify/api-security-middleware.js +139 -0
  70. package/dist/fastify/api-security-middleware.js.map +1 -0
  71. package/dist/fastify/compat.d.ts +22 -0
  72. package/dist/fastify/compat.d.ts.map +1 -0
  73. package/dist/fastify/compat.js +16 -0
  74. package/dist/fastify/compat.js.map +1 -0
  75. package/dist/fastify/daemon-status.d.ts +22 -0
  76. package/dist/fastify/daemon-status.d.ts.map +1 -0
  77. package/dist/fastify/daemon-status.js +9 -0
  78. package/dist/fastify/daemon-status.js.map +1 -0
  79. package/dist/fastify/host-tools.d.ts +13 -0
  80. package/dist/fastify/host-tools.d.ts.map +1 -0
  81. package/dist/fastify/host-tools.js +8 -0
  82. package/dist/fastify/host-tools.js.map +1 -0
  83. package/dist/fastify/index.d.ts +36 -0
  84. package/dist/fastify/index.d.ts.map +1 -0
  85. package/dist/fastify/index.js +18 -0
  86. package/dist/fastify/index.js.map +1 -0
  87. package/dist/fastify/local-daemon-request.d.ts +43 -0
  88. package/dist/fastify/local-daemon-request.d.ts.map +1 -0
  89. package/dist/fastify/local-daemon-request.js +155 -0
  90. package/dist/fastify/local-daemon-request.js.map +1 -0
  91. package/dist/fastify/origin.d.ts +21 -0
  92. package/dist/fastify/origin.d.ts.map +1 -0
  93. package/dist/fastify/origin.js +14 -0
  94. package/dist/fastify/origin.js.map +1 -0
  95. package/dist/fastify/request.d.ts +20 -0
  96. package/dist/fastify/request.d.ts.map +1 -0
  97. package/dist/fastify/request.js +25 -0
  98. package/dist/fastify/request.js.map +1 -0
  99. package/dist/fastify/response.d.ts +20 -0
  100. package/dist/fastify/response.d.ts.map +1 -0
  101. package/dist/fastify/response.js +41 -0
  102. package/dist/fastify/response.js.map +1 -0
  103. package/dist/fastify/route-registration-guard.d.ts +70 -0
  104. package/dist/fastify/route-registration-guard.d.ts.map +1 -0
  105. package/dist/fastify/route-registration-guard.js +69 -0
  106. package/dist/fastify/route-registration-guard.js.map +1 -0
  107. package/dist/fastify/run-stream.d.ts +18 -0
  108. package/dist/fastify/run-stream.d.ts.map +1 -0
  109. package/dist/fastify/run-stream.js +10 -0
  110. package/dist/fastify/run-stream.js.map +1 -0
  111. package/dist/fastify/runs.d.ts +17 -0
  112. package/dist/fastify/runs.d.ts.map +1 -0
  113. package/dist/fastify/runs.js +33 -0
  114. package/dist/fastify/runs.js.map +1 -0
  115. package/dist/frontend-control.d.ts +131 -0
  116. package/dist/frontend-control.d.ts.map +1 -0
  117. package/dist/frontend-control.js +77 -0
  118. package/dist/frontend-control.js.map +1 -0
  119. package/dist/frontend-sessions.d.ts +100 -0
  120. package/dist/frontend-sessions.d.ts.map +1 -0
  121. package/dist/frontend-sessions.js +180 -0
  122. package/dist/frontend-sessions.js.map +1 -0
  123. package/dist/health.d.ts +91 -0
  124. package/dist/health.d.ts.map +1 -0
  125. package/dist/health.js +53 -0
  126. package/dist/health.js.map +1 -0
  127. package/dist/host-tools.d.ts +174 -0
  128. package/dist/host-tools.d.ts.map +1 -0
  129. package/dist/host-tools.js +338 -0
  130. package/dist/host-tools.js.map +1 -0
  131. package/dist/index.d.ts +86 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +40 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/local-daemon-request.d.ts +44 -0
  136. package/dist/local-daemon-request.d.ts.map +1 -0
  137. package/dist/local-daemon-request.js +145 -0
  138. package/dist/local-daemon-request.js.map +1 -0
  139. package/dist/media.d.ts +131 -0
  140. package/dist/media.d.ts.map +1 -0
  141. package/dist/media.js +352 -0
  142. package/dist/media.js.map +1 -0
  143. package/dist/memory.d.ts +230 -0
  144. package/dist/memory.d.ts.map +1 -0
  145. package/dist/memory.js +331 -0
  146. package/dist/memory.js.map +1 -0
  147. package/dist/model-proxy.d.ts +45 -0
  148. package/dist/model-proxy.d.ts.map +1 -0
  149. package/dist/model-proxy.js +619 -0
  150. package/dist/model-proxy.js.map +1 -0
  151. package/dist/origin-validation.d.ts +64 -0
  152. package/dist/origin-validation.d.ts.map +1 -0
  153. package/dist/origin-validation.js +226 -0
  154. package/dist/origin-validation.js.map +1 -0
  155. package/dist/origin.d.ts +32 -0
  156. package/dist/origin.d.ts.map +1 -0
  157. package/dist/origin.js +14 -0
  158. package/dist/origin.js.map +1 -0
  159. package/dist/pack-http.d.ts +30 -0
  160. package/dist/pack-http.d.ts.map +1 -0
  161. package/dist/pack-http.js +12 -0
  162. package/dist/pack-http.js.map +1 -0
  163. package/dist/raw-sse.d.ts +44 -0
  164. package/dist/raw-sse.d.ts.map +1 -0
  165. package/dist/raw-sse.js +93 -0
  166. package/dist/raw-sse.js.map +1 -0
  167. package/dist/remote-run-events.d.ts +66 -0
  168. package/dist/remote-run-events.d.ts.map +1 -0
  169. package/dist/remote-run-events.js +230 -0
  170. package/dist/remote-run-events.js.map +1 -0
  171. package/dist/request.d.ts +19 -0
  172. package/dist/request.d.ts.map +1 -0
  173. package/dist/request.js +25 -0
  174. package/dist/request.js.map +1 -0
  175. package/dist/research.d.ts +42 -0
  176. package/dist/research.d.ts.map +1 -0
  177. package/dist/research.js +316 -0
  178. package/dist/research.js.map +1 -0
  179. package/dist/response.d.ts +14 -0
  180. package/dist/response.d.ts.map +1 -0
  181. package/dist/response.js +45 -0
  182. package/dist/response.js.map +1 -0
  183. package/dist/route-manifest.d.ts +31 -0
  184. package/dist/route-manifest.d.ts.map +1 -0
  185. package/dist/route-manifest.js +103 -0
  186. package/dist/route-manifest.js.map +1 -0
  187. package/dist/route-registration-guard.d.ts +65 -0
  188. package/dist/route-registration-guard.d.ts.map +1 -0
  189. package/dist/route-registration-guard.js +71 -0
  190. package/dist/route-registration-guard.js.map +1 -0
  191. package/dist/routines.d.ts +114 -0
  192. package/dist/routines.d.ts.map +1 -0
  193. package/dist/routines.js +320 -0
  194. package/dist/routines.js.map +1 -0
  195. package/dist/run-stream.d.ts +60 -0
  196. package/dist/run-stream.d.ts.map +1 -0
  197. package/dist/run-stream.js +108 -0
  198. package/dist/run-stream.js.map +1 -0
  199. package/dist/runs.d.ts +79 -0
  200. package/dist/runs.d.ts.map +1 -0
  201. package/dist/runs.js +232 -0
  202. package/dist/runs.js.map +1 -0
  203. package/dist/sse.d.ts +139 -0
  204. package/dist/sse.d.ts.map +1 -0
  205. package/dist/sse.js +157 -0
  206. package/dist/sse.js.map +1 -0
  207. package/dist/terminals.d.ts +125 -0
  208. package/dist/terminals.d.ts.map +1 -0
  209. package/dist/terminals.js +349 -0
  210. package/dist/terminals.js.map +1 -0
  211. package/dist/tool-catalog.d.ts +68 -0
  212. package/dist/tool-catalog.d.ts.map +1 -0
  213. package/dist/tool-catalog.js +68 -0
  214. package/dist/tool-catalog.js.map +1 -0
  215. package/dist/types.d.ts +58 -0
  216. package/dist/types.d.ts.map +1 -0
  217. package/dist/types.js +5 -0
  218. package/dist/types.js.map +1 -0
  219. package/dist/workspace-root.d.ts +69 -0
  220. package/dist/workspace-root.d.ts.map +1 -0
  221. package/dist/workspace-root.js +63 -0
  222. package/dist/workspace-root.js.map +1 -0
  223. package/dist/xai.d.ts +86 -0
  224. package/dist/xai.d.ts.map +1 -0
  225. package/dist/xai.js +668 -0
  226. package/dist/xai.js.map +1 -0
  227. package/package.json +58 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @module agents
3
+ *
4
+ * `GET /api/agents` — lists the agents a host can expose to a client,
5
+ * including host-probed availability/model metadata when supplied.
6
+ * `POST /api/agents/rescan` asks the host to invalidate its discovery cache
7
+ * and probe again.
8
+ *
9
+ * `listAgents` is injected (matching `daemon-status.ts`/`active-context.ts`'s
10
+ * DI convention) rather than this module importing `@jini-ai/agent-runtime`
11
+ * directly — a host typically already has that package's `AGENT_DEFS` array
12
+ * in scope and just needs to project it, and this keeps `@jini-ai/http-kit` from
13
+ * taking on a dependency on subprocess discovery. The host owns probing,
14
+ * timeouts, caching, PATH/env policy, and the projection of spawn-only
15
+ * metadata; this transport only serializes the safe summary.
16
+ */
17
+ import type { Express } from 'express';
18
+ import { type AdapterContext } from './adapter.js';
19
+ export interface AgentModelSummary {
20
+ readonly id: string;
21
+ readonly label: string;
22
+ }
23
+ /**
24
+ * Client-safe agent discovery data. Optional probe fields preserve the
25
+ * original static-registry contract for hosts that only expose `{id, name}`.
26
+ * Spawn internals (`bin`, resolved path, argv builders, env) never cross HTTP.
27
+ */
28
+ export interface AgentSummary {
29
+ readonly id: string;
30
+ readonly name: string;
31
+ readonly available?: boolean;
32
+ readonly version?: string | null;
33
+ readonly authStatus?: 'ok' | 'missing' | 'unknown';
34
+ readonly models?: readonly AgentModelSummary[];
35
+ readonly reasoningOptions?: readonly AgentModelSummary[];
36
+ readonly modelsSource?: 'live' | 'fallback';
37
+ readonly supportsCustomModel?: boolean;
38
+ readonly diagnostic?: string;
39
+ }
40
+ export interface AgentsHttpDeps {
41
+ /** Returns the host's cached or freshly resolved client-safe agent inventory. */
42
+ readonly listAgents: () => Promise<readonly AgentSummary[]> | readonly AgentSummary[];
43
+ /** Forces host-owned discovery to run again. Falls back to `listAgents` when omitted. */
44
+ readonly rescanAgents?: () => Promise<readonly AgentSummary[]> | readonly AgentSummary[];
45
+ }
46
+ export interface AgentListResponse {
47
+ readonly agents: readonly AgentSummary[];
48
+ }
49
+ /** `GET /api/agents` — read-only, no side effects; matches `runListRoute`/`runStatusRoute`'s posture of not requiring same-origin. */
50
+ export declare const agentListRoute: import("./types.js").JsonRouteSpec<void, AgentListResponse, AgentsHttpDeps>;
51
+ /** `POST /api/agents/rescan` — explicit state refresh, protected by the local same-origin gate. */
52
+ export declare const agentRescanRoute: import("./types.js").JsonRouteSpec<void, AgentListResponse, AgentsHttpDeps>;
53
+ /** Mounts the read and explicit-rescan agent discovery routes. */
54
+ export declare function registerAgentRoutes(app: Express, deps: AgentsHttpDeps, adapter: AdapterContext): void;
55
+ //# sourceMappingURL=agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAmC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAGpF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,GAAG,SAAS,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACzD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5C,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,GAAG,SAAS,YAAY,EAAE,CAAC;IACtF,yFAAyF;IACzF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,GAAG,SAAS,YAAY,EAAE,CAAC;CAC1F;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;CAC1C;AAED,sIAAsI;AACtI,eAAO,MAAM,cAAc,6EAKzB,CAAC;AAEH,mGAAmG;AACnG,eAAO,MAAM,gBAAgB,6EAM3B,CAAC;AAEH,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAGrG"}
package/dist/agents.js ADDED
@@ -0,0 +1,23 @@
1
+ import { defineJsonRoute, mountJsonRoute } from './adapter.js';
2
+ import { ok } from './types.js';
3
+ /** `GET /api/agents` — read-only, no side effects; matches `runListRoute`/`runStatusRoute`'s posture of not requiring same-origin. */
4
+ export const agentListRoute = defineJsonRoute({
5
+ method: 'get',
6
+ path: '/api/agents',
7
+ parse: () => ok(undefined),
8
+ handle: async (_input, deps) => ok({ agents: await deps.listAgents() }),
9
+ });
10
+ /** `POST /api/agents/rescan` — explicit state refresh, protected by the local same-origin gate. */
11
+ export const agentRescanRoute = defineJsonRoute({
12
+ method: 'post',
13
+ path: '/api/agents/rescan',
14
+ requireSameOrigin: true,
15
+ parse: () => ok(undefined),
16
+ handle: async (_input, deps) => ok({ agents: await (deps.rescanAgents ?? deps.listAgents)() }),
17
+ });
18
+ /** Mounts the read and explicit-rescan agent discovery routes. */
19
+ export function registerAgentRoutes(app, deps, adapter) {
20
+ mountJsonRoute(app, agentListRoute, deps, adapter);
21
+ mountJsonRoute(app, agentRescanRoute, deps, adapter);
22
+ }
23
+ //# sourceMappingURL=agents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.js","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAuB,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAoChC,sIAAsI;AACtI,MAAM,CAAC,MAAM,cAAc,GAAG,eAAe,CAA0C;IACrF,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;CACxE,CAAC,CAAC;AAEH,mGAAmG;AACnG,MAAM,CAAC,MAAM,gBAAgB,GAAG,eAAe,CAA0C;IACvF,MAAM,EAAE,MAAM;IACd,IAAI,EAAE,oBAAoB;IAC1B,iBAAiB,EAAE,IAAI;IACvB,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;CAC/F,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,UAAU,mBAAmB,CAAC,GAAY,EAAE,IAAoB,EAAE,OAAuB;IAC7F,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACnD,cAAc,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AACvD,CAAC"}
@@ -0,0 +1,146 @@
1
+ import type { Express, NextFunction, Request, Response } from 'express';
2
+ import { type ApiTokenAuthEnvConfig } from '@jini-ai/core';
3
+ /**
4
+ * Extracts the token from an `Authorization: Bearer <token>` header value. The scheme is
5
+ * case-insensitive per RFC 7235 §2.1; the token itself is not.
6
+ *
7
+ * Shared by every bearer gate in this package so the accepted header grammar is defined exactly
8
+ * once — three separate copies of this regex is three chances for them to disagree about what
9
+ * counts as a well-formed header.
10
+ *
11
+ * @param header - The raw `Authorization` header value, or `undefined` when absent.
12
+ * @returns The token, or `null` when the header is absent or not a well-formed bearer header.
13
+ * @complexity O(n) in the header length.
14
+ * @overallScore 100/100
15
+ */
16
+ export declare function bearerTokenFromHeader(header: string | undefined): string | null;
17
+ /**
18
+ * Constant-time token comparison. Length is compared first and NOT in constant time — that leaks
19
+ * only the expected token's length, which for a generated secret is fixed and not itself a secret,
20
+ * never any of its bytes. `timingSafeEqual` throws on a length mismatch, so the early return is
21
+ * required rather than merely an optimization.
22
+ *
23
+ * Every bearer gate in this package routes through here instead of `===`. A plain string compare
24
+ * short-circuits on the first differing byte, which makes the comparison's duration a function of
25
+ * how many leading bytes the presented token got right — enough, over many requests, to recover a
26
+ * token one byte at a time.
27
+ *
28
+ * @param presented - The token from the request's `Authorization` header.
29
+ * @param expected - The configured token to match against.
30
+ * @returns `true` only on an exact match.
31
+ * @complexity O(n) in the token length, with no data-dependent early exit.
32
+ * @overallScore 100/100
33
+ */
34
+ export declare function timingSafeTokenMatch(presented: string, expected: string): boolean;
35
+ export interface ApiBearerAuthMiddlewareDeps {
36
+ /** Env var names for the token/disable flags. Defaults to `JINI_API_TOKEN` / `JINI_DISABLE_API_AUTH`. */
37
+ tokenConfig?: ApiTokenAuthEnvConfig;
38
+ /** Defaults to `process.env`. Threaded through so tests never have to mutate real process env. */
39
+ env?: NodeJS.ProcessEnv;
40
+ /**
41
+ * Whether an unproxied loopback peer may skip the bearer check. Defaults to `true` — the
42
+ * affordance a desktop UI or local CLI talking to its own daemon relies on.
43
+ *
44
+ * Set to `false` for any deployment where a process other than a trusted local client can open a
45
+ * loopback connection to this daemon, which includes every same-host reverse-proxy deployment
46
+ * (see the forwarded-header note at the check itself) and any multi-tenant or shared host. There
47
+ * is no way to tell those peers apart by socket address alone, so this is the host's call to
48
+ * make, not one this middleware can infer. {@link requireStrictBearerToken} is the same posture
49
+ * for a daemon that should never have had the exemption in the first place.
50
+ */
51
+ trustLoopbackPeers?: boolean;
52
+ }
53
+ /**
54
+ * Registers a bearer-token gate on every `/api/*` route, active only when
55
+ * {@link isApiTokenMiddlewareEnabled} says a token is configured and auth hasn't been disabled.
56
+ * When active: open-probe paths and loopback peers skip the check unconditionally; every other
57
+ * request must send `Authorization: Bearer <token>` matching the configured token exactly, or the
58
+ * request is rejected with 401 before reaching any route handler.
59
+ *
60
+ * @param app - The Express app to register the gate on.
61
+ * @param deps - See {@link ApiBearerAuthMiddlewareDeps}. Both fields are optional; omitting `deps`
62
+ * entirely reads `JINI_API_TOKEN`/`JINI_DISABLE_API_AUTH` from real `process.env`.
63
+ * @returns Nothing. Registers zero middleware (a deliberate no-op, not a bug) when no token is configured.
64
+ * @complexity Setup is O(1). Each gated request is O(1) (one Set lookup, one regex match).
65
+ * @overallScore 100/100
66
+ */
67
+ export declare function registerApiBearerAuthMiddleware(app: Express, deps?: ApiBearerAuthMiddlewareDeps): void;
68
+ export interface StrictBearerTokenDeps {
69
+ /**
70
+ * Env var carrying the expected token. Required with no default: a gate this strict must name the
71
+ * secret it enforces, and this package has no business guessing a host's env-var name.
72
+ */
73
+ readonly tokenEnvVar: string;
74
+ /** Defaults to `process.env`. Threaded through so tests never have to mutate real process env. */
75
+ readonly env?: NodeJS.ProcessEnv;
76
+ /**
77
+ * Exact request paths this gate does not apply to. Defaults to none — this is a gate-everything
78
+ * primitive, and each exemption must be opted into explicitly at the mount site with a stated
79
+ * reason. Matched by exact equality, never by prefix, so a longer path that merely starts with an
80
+ * exempt one stays gated.
81
+ */
82
+ readonly exemptPaths?: readonly string[];
83
+ }
84
+ /**
85
+ * Builds a bearer gate with **no loopback exemption and no disable flag** — for a daemon whose
86
+ * threat model is "another process running as the same OS user", not "a remote attacker".
87
+ *
88
+ * This is deliberately not {@link registerApiBearerAuthMiddleware}. That gate short-circuits for any
89
+ * loopback peer before it ever reads the `Authorization` header, which is the right affordance for a
90
+ * desktop UI or local CLI talking to its own daemon, and a no-op against the threat here: a
91
+ * `127.0.0.1` bind keeps remote hosts out but does nothing about a co-resident process, and a
92
+ * sidecar daemon that can start real agent runs and execute real tools is exactly the kind of thing
93
+ * such a process would want to reach. This gate treats a request from `127.0.0.1` like any other.
94
+ *
95
+ * The posture matches `remote-run-events.ts`'s own token gate, which made the same call for the same
96
+ * reason on a single route; this generalizes it to a whole `/api` surface.
97
+ *
98
+ * Fail-closed contract:
99
+ * - token env var unset/empty -> **503**, never a silent pass-through. A misconfigured daemon
100
+ * refuses to serve rather than serving unauthenticated callers.
101
+ * - missing / malformed / wrong token -> **401**.
102
+ * - exact match -> `next()`.
103
+ *
104
+ * The env var is read on every request rather than captured at factory time: a host that mints a
105
+ * per-boot token may not have done so yet when its module graph is first evaluated, and re-reading
106
+ * costs one property lookup.
107
+ *
108
+ * @param deps - See {@link StrictBearerTokenDeps}. `tokenEnvVar` is required.
109
+ * @returns Express middleware. Mount it **unprefixed** — `app.use(handler)` — before any route
110
+ * registrar and before the JSON body parser, so an unauthenticated caller's body is never parsed.
111
+ * @complexity O(1) per request plus O(n) in the token length for the constant-time comparison.
112
+ * @overallScore 95/100 — Medium: mounting under a prefix (`app.use('/api', handler)`) silently breaks
113
+ * `exemptPaths`. Express strips the mount prefix from `req.path`, so an entry written as a full path
114
+ * (`/api/delegated-tool-calls`) stops matching and the gate rejects a caller the mount site meant to
115
+ * exempt. Nothing type-checks that, and the failure is a 401 on a route that is supposed to be open
116
+ * rather than the reverse — noisy rather than dangerous, but confusing to diagnose. Documented rather
117
+ * than defended against in code: normalizing `req.baseUrl + req.path` here would silently accept both
118
+ * mount styles and remove the caller's ability to reason about which paths its entries refer to.
119
+ */
120
+ export declare function requireStrictBearerToken(deps: StrictBearerTokenDeps): (req: Request, res: Response, next: NextFunction) => void;
121
+ export interface ApiOriginGuardMiddlewareDeps {
122
+ /** The host this daemon is bound to — compared against a request's `Host`/`Origin` headers. */
123
+ host: string;
124
+ /** Extra allow-listed origins (e.g. a reverse-proxy's public origin). Defaults to none. */
125
+ extraAllowedOrigins?: readonly string[];
126
+ /** Returns the daemon's resolved listen port, or a falsy value before it has resolved. */
127
+ getResolvedPort: () => number | null | undefined;
128
+ /** Defaults to `process.env`. Threaded through so `JINI_WEB_PORT` is testable without mutating real process env. */
129
+ env?: NodeJS.ProcessEnv;
130
+ }
131
+ /**
132
+ * Registers an unconditional cross-origin gate on every `/api/*` route: non-browser clients (no
133
+ * `Origin` header) and requests whose `Origin` resolves to a loopback, private-LAN, or explicitly
134
+ * allow-listed origin are let through; everything else is rejected with 403. `Origin: null`
135
+ * (typically a sandboxed iframe) is always rejected — see this module's doc for why the origin
136
+ * daemon's safe-GET exemption for that case was dropped.
137
+ *
138
+ * @param app - The Express app to register the gate on.
139
+ * @param deps - See {@link ApiOriginGuardMiddlewareDeps}.
140
+ * @returns Nothing. Unlike the bearer-token gate, this always registers its middleware — there is
141
+ * no "disabled" state.
142
+ * @complexity Setup is O(1). Each gated request is O(p) in the number of allowed ports (typically 1-2).
143
+ * @overallScore 100/100
144
+ */
145
+ export declare function registerApiOriginGuardMiddleware(app: Express, deps: ApiOriginGuardMiddlewareDeps): void;
146
+ //# sourceMappingURL=api-security-middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-security-middleware.d.ts","sourceRoot":"","sources":["../src/api-security-middleware.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAC;AAgBvB;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAE/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKjF;AAED,MAAM,WAAW,2BAA2B;IAC1C,yGAAyG;IACzG,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,kGAAkG;IAClG,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAuBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,GAAE,2BAAgC,GAAG,IAAI,CA6C1G;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kGAAkG;IAClG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,qBAAqB,IAKf,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CA2B1G;AAED,MAAM,WAAW,4BAA4B;IAC3C,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb,2FAA2F;IAC3F,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,0FAA0F;IAC1F,eAAe,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjD,oHAAoH;IACpH,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAWD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gCAAgC,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,4BAA4B,GAAG,IAAI,CAmCvG"}
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @module api-security-middleware
3
+ *
4
+ * The `/api` request gates a locally-bound daemon needs before any route handler runs:
5
+ * bearer-token authentication (optional, active only when a token is configured), cross-origin
6
+ * rejection (always active), and — for a daemon whose threat model is a co-resident process rather
7
+ * than a remote one — a strict bearer gate with no loopback exemption and no disable flag
8
+ * ({@link requireStrictBearerToken}). The first two were genericized from an origin daemon's inline
9
+ * `startServer` middleware pair — see `source-map.md` for the exact drop-list. All are plain Express
10
+ * middleware factories with configuration injected, so none reads a hardcoded env var name or an
11
+ * OD-specific request shape.
12
+ *
13
+ * The two token gates differ in exactly one load-bearing way and are not interchangeable: the
14
+ * optional one lets any loopback peer through unauthenticated, the strict one does not. Picking the
15
+ * wrong one is a silent no-op rather than a visible error, so see each function's own doc before
16
+ * choosing.
17
+ *
18
+ * **Dropped, not carried over** (see `source-map.md`'s transformation table for the full
19
+ * reasoning): the project-preview-scope GET exemption, the zero-config browser-extension
20
+ * ("clipper") bypass, the live-artifacts-preview bypass, and the `Origin: null` safe-GET
21
+ * allow-list regex — all four name or exist solely for OD product routes with no meaning in the
22
+ * generic engine. `Origin: null` is therefore always rejected here, not conditionally allowed.
23
+ */
24
+ import { timingSafeEqual } from 'node:crypto';
25
+ import { apiTokenFromEnv, isApiTokenMiddlewareEnabled, } from '@jini-ai/core';
26
+ import { isLoopbackPeerAddress } from './local-daemon-request.js';
27
+ import { allowedBrowserPorts, isAllowedBrowserOrigin } from './origin-validation.js';
28
+ /** Health/readiness/version probes stay reachable without a bearer token so monitoring never needs one. Both the mount-relative and `/api`-prefixed forms are listed because this middleware is always registered via `app.use('/api', ...)`, under which Express strips the `/api` prefix from `req.path` for a request to `/api/health` — the prefixed form is kept for parity with the origin module's own set rather than dropped as dead, in case a future caller mounts this middleware unprefixed. */
29
+ const OPEN_PROBE_PATHS = new Set([
30
+ '/health',
31
+ '/api/health',
32
+ '/ready',
33
+ '/api/ready',
34
+ '/version',
35
+ '/api/version',
36
+ ]);
37
+ const BEARER_TOKEN_PATTERN = /^Bearer\s+(\S+)\s*$/i;
38
+ /**
39
+ * Extracts the token from an `Authorization: Bearer <token>` header value. The scheme is
40
+ * case-insensitive per RFC 7235 §2.1; the token itself is not.
41
+ *
42
+ * Shared by every bearer gate in this package so the accepted header grammar is defined exactly
43
+ * once — three separate copies of this regex is three chances for them to disagree about what
44
+ * counts as a well-formed header.
45
+ *
46
+ * @param header - The raw `Authorization` header value, or `undefined` when absent.
47
+ * @returns The token, or `null` when the header is absent or not a well-formed bearer header.
48
+ * @complexity O(n) in the header length.
49
+ * @overallScore 100/100
50
+ */
51
+ export function bearerTokenFromHeader(header) {
52
+ return BEARER_TOKEN_PATTERN.exec(header ?? '')?.[1] ?? null;
53
+ }
54
+ /**
55
+ * Constant-time token comparison. Length is compared first and NOT in constant time — that leaks
56
+ * only the expected token's length, which for a generated secret is fixed and not itself a secret,
57
+ * never any of its bytes. `timingSafeEqual` throws on a length mismatch, so the early return is
58
+ * required rather than merely an optimization.
59
+ *
60
+ * Every bearer gate in this package routes through here instead of `===`. A plain string compare
61
+ * short-circuits on the first differing byte, which makes the comparison's duration a function of
62
+ * how many leading bytes the presented token got right — enough, over many requests, to recover a
63
+ * token one byte at a time.
64
+ *
65
+ * @param presented - The token from the request's `Authorization` header.
66
+ * @param expected - The configured token to match against.
67
+ * @returns `true` only on an exact match.
68
+ * @complexity O(n) in the token length, with no data-dependent early exit.
69
+ * @overallScore 100/100
70
+ */
71
+ export function timingSafeTokenMatch(presented, expected) {
72
+ const presentedBytes = Buffer.from(presented, 'utf8');
73
+ const expectedBytes = Buffer.from(expected, 'utf8');
74
+ if (presentedBytes.length !== expectedBytes.length)
75
+ return false;
76
+ return timingSafeEqual(presentedBytes, expectedBytes);
77
+ }
78
+ /**
79
+ * Headers a reverse proxy adds when it forwards a request on behalf of some other client. Their
80
+ * *values* are deliberately never read — this package does not trust them to identify anyone (that
81
+ * is the `X-Forwarded-For` spoofing trap the loopback check was written to avoid). Only their
82
+ * presence is used, and only to withhold a privilege: a request carrying one was relayed by a
83
+ * proxy, so it is not the local desktop/CLI peer the loopback exemption exists for.
84
+ */
85
+ const PROXY_FORWARDING_HEADERS = ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'forwarded'];
86
+ function looksProxied(req) {
87
+ return PROXY_FORWARDING_HEADERS.some((header) => {
88
+ const value = req.headers[header];
89
+ return typeof value === 'string' ? value.length > 0 : Array.isArray(value) && value.length > 0;
90
+ });
91
+ }
92
+ const DEFAULT_TOKEN_CONFIG = {
93
+ tokenEnvVar: 'JINI_API_TOKEN',
94
+ disableEnvVar: 'JINI_DISABLE_API_AUTH',
95
+ };
96
+ /**
97
+ * Registers a bearer-token gate on every `/api/*` route, active only when
98
+ * {@link isApiTokenMiddlewareEnabled} says a token is configured and auth hasn't been disabled.
99
+ * When active: open-probe paths and loopback peers skip the check unconditionally; every other
100
+ * request must send `Authorization: Bearer <token>` matching the configured token exactly, or the
101
+ * request is rejected with 401 before reaching any route handler.
102
+ *
103
+ * @param app - The Express app to register the gate on.
104
+ * @param deps - See {@link ApiBearerAuthMiddlewareDeps}. Both fields are optional; omitting `deps`
105
+ * entirely reads `JINI_API_TOKEN`/`JINI_DISABLE_API_AUTH` from real `process.env`.
106
+ * @returns Nothing. Registers zero middleware (a deliberate no-op, not a bug) when no token is configured.
107
+ * @complexity Setup is O(1). Each gated request is O(1) (one Set lookup, one regex match).
108
+ * @overallScore 100/100
109
+ */
110
+ export function registerApiBearerAuthMiddleware(app, deps = {}) {
111
+ const tokenConfig = deps.tokenConfig ?? DEFAULT_TOKEN_CONFIG;
112
+ const env = deps.env ?? process.env;
113
+ if (!isApiTokenMiddlewareEnabled(tokenConfig, env))
114
+ return;
115
+ const apiToken = apiTokenFromEnv(tokenConfig, env);
116
+ const trustLoopbackPeers = deps.trustLoopbackPeers ?? true;
117
+ app.use('/api', (req, res, next) => {
118
+ if (OPEN_PROBE_PATHS.has(req.path)) {
119
+ next();
120
+ return;
121
+ }
122
+ // Loopback short-circuit: the desktop UI / local CLI never carry a bearer, and the peer address
123
+ // is read from the socket rather than `X-Forwarded-For` so a spoofed forwarding header can
124
+ // never *grant* the exemption.
125
+ //
126
+ // That alone is not enough. A reverse proxy running on the same host as the daemon — nginx or
127
+ // caddy on the same box, proxying to a bind on 127.0.0.1, an extremely common deployment —
128
+ // terminates the client's connection itself and opens its own from loopback, so every
129
+ // externally-originated request through it arrives with a loopback socket address. Trusting the
130
+ // socket address alone would hand the exemption to the entire public internet.
131
+ //
132
+ // So the exemption is withheld from any request carrying a forwarding header: those are added
133
+ // by the proxy, not by the local desktop/CLI clients this exemption exists for. The header's
134
+ // value is still never read — presence only ever *removes* the exemption, so a caller who
135
+ // forges one gains nothing and simply has to present the bearer like anyone else. A proxy
136
+ // deployment that strips its own forwarding headers is not covered by this and must set
137
+ // `trustLoopbackPeers: false`, which is also the correct setting for any host where an
138
+ // untrusted co-resident process can reach the port.
139
+ if (trustLoopbackPeers && isLoopbackPeerAddress(req.socket?.remoteAddress) && !looksProxied(req)) {
140
+ next();
141
+ return;
142
+ }
143
+ const presented = bearerTokenFromHeader(req.get('authorization'));
144
+ if (presented === null || !timingSafeTokenMatch(presented, apiToken)) {
145
+ res.status(401).json({
146
+ error: {
147
+ code: 'API_TOKEN_REQUIRED',
148
+ message: `Authorization: Bearer <${tokenConfig.tokenEnvVar}> required`,
149
+ },
150
+ });
151
+ return;
152
+ }
153
+ next();
154
+ });
155
+ }
156
+ /**
157
+ * Builds a bearer gate with **no loopback exemption and no disable flag** — for a daemon whose
158
+ * threat model is "another process running as the same OS user", not "a remote attacker".
159
+ *
160
+ * This is deliberately not {@link registerApiBearerAuthMiddleware}. That gate short-circuits for any
161
+ * loopback peer before it ever reads the `Authorization` header, which is the right affordance for a
162
+ * desktop UI or local CLI talking to its own daemon, and a no-op against the threat here: a
163
+ * `127.0.0.1` bind keeps remote hosts out but does nothing about a co-resident process, and a
164
+ * sidecar daemon that can start real agent runs and execute real tools is exactly the kind of thing
165
+ * such a process would want to reach. This gate treats a request from `127.0.0.1` like any other.
166
+ *
167
+ * The posture matches `remote-run-events.ts`'s own token gate, which made the same call for the same
168
+ * reason on a single route; this generalizes it to a whole `/api` surface.
169
+ *
170
+ * Fail-closed contract:
171
+ * - token env var unset/empty -> **503**, never a silent pass-through. A misconfigured daemon
172
+ * refuses to serve rather than serving unauthenticated callers.
173
+ * - missing / malformed / wrong token -> **401**.
174
+ * - exact match -> `next()`.
175
+ *
176
+ * The env var is read on every request rather than captured at factory time: a host that mints a
177
+ * per-boot token may not have done so yet when its module graph is first evaluated, and re-reading
178
+ * costs one property lookup.
179
+ *
180
+ * @param deps - See {@link StrictBearerTokenDeps}. `tokenEnvVar` is required.
181
+ * @returns Express middleware. Mount it **unprefixed** — `app.use(handler)` — before any route
182
+ * registrar and before the JSON body parser, so an unauthenticated caller's body is never parsed.
183
+ * @complexity O(1) per request plus O(n) in the token length for the constant-time comparison.
184
+ * @overallScore 95/100 — Medium: mounting under a prefix (`app.use('/api', handler)`) silently breaks
185
+ * `exemptPaths`. Express strips the mount prefix from `req.path`, so an entry written as a full path
186
+ * (`/api/delegated-tool-calls`) stops matching and the gate rejects a caller the mount site meant to
187
+ * exempt. Nothing type-checks that, and the failure is a 401 on a route that is supposed to be open
188
+ * rather than the reverse — noisy rather than dangerous, but confusing to diagnose. Documented rather
189
+ * than defended against in code: normalizing `req.baseUrl + req.path` here would silently accept both
190
+ * mount styles and remove the caller's ability to reason about which paths its entries refer to.
191
+ */
192
+ export function requireStrictBearerToken(deps) {
193
+ const { tokenEnvVar } = deps;
194
+ const env = deps.env ?? process.env;
195
+ const exempt = new Set(deps.exemptPaths ?? []);
196
+ return function requireStrictBearerTokenMiddleware(req, res, next) {
197
+ if (exempt.has(req.path)) {
198
+ next();
199
+ return;
200
+ }
201
+ const expected = env[tokenEnvVar];
202
+ if (typeof expected !== 'string' || expected.length === 0) {
203
+ res.status(503).json({
204
+ error: {
205
+ code: 'API_TOKEN_NOT_CONFIGURED',
206
+ message: `${tokenEnvVar} is not set — this daemon refuses to serve unauthenticated callers`,
207
+ },
208
+ });
209
+ return;
210
+ }
211
+ // Deliberately no loopback/peer-address exemption — see this function's doc.
212
+ const presented = bearerTokenFromHeader(req.get('authorization'));
213
+ if (presented === null || !timingSafeTokenMatch(presented, expected)) {
214
+ res.status(401).json({
215
+ error: { code: 'API_TOKEN_REQUIRED', message: `Authorization: Bearer <${tokenEnvVar}> required` },
216
+ });
217
+ return;
218
+ }
219
+ next();
220
+ };
221
+ }
222
+ /**
223
+ * Chrome may strip the port from the `Origin` header on same-origin GET requests. Used only as a
224
+ * narrow fallback for safe, idempotent GET requests once the exact-match check below has already
225
+ * failed — mutating routes always require an exact origin/host match.
226
+ */
227
+ function isPortlessLoopbackOrigin(origin) {
228
+ return /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])$/.test(origin);
229
+ }
230
+ /**
231
+ * Registers an unconditional cross-origin gate on every `/api/*` route: non-browser clients (no
232
+ * `Origin` header) and requests whose `Origin` resolves to a loopback, private-LAN, or explicitly
233
+ * allow-listed origin are let through; everything else is rejected with 403. `Origin: null`
234
+ * (typically a sandboxed iframe) is always rejected — see this module's doc for why the origin
235
+ * daemon's safe-GET exemption for that case was dropped.
236
+ *
237
+ * @param app - The Express app to register the gate on.
238
+ * @param deps - See {@link ApiOriginGuardMiddlewareDeps}.
239
+ * @returns Nothing. Unlike the bearer-token gate, this always registers its middleware — there is
240
+ * no "disabled" state.
241
+ * @complexity Setup is O(1). Each gated request is O(p) in the number of allowed ports (typically 1-2).
242
+ * @overallScore 100/100
243
+ */
244
+ export function registerApiOriginGuardMiddleware(app, deps) {
245
+ const { host, getResolvedPort } = deps;
246
+ const extraAllowedOrigins = deps.extraAllowedOrigins ?? [];
247
+ const env = deps.env ?? process.env;
248
+ app.use('/api', (req, res, next) => {
249
+ const origin = req.headers.origin;
250
+ if (origin == null || origin === '') {
251
+ next();
252
+ return;
253
+ }
254
+ if (origin === 'null') {
255
+ res.status(403).json({ error: 'Origin: null not allowed for this route' });
256
+ return;
257
+ }
258
+ // Fail-closed: block every browser origin until the daemon's real listen port is known, so a
259
+ // request arriving in the brief window before `.listen()` resolves can never be compared
260
+ // against a wrong or default port.
261
+ const resolvedPort = getResolvedPort();
262
+ if (!resolvedPort) {
263
+ res.status(403).json({ error: 'Server initializing' });
264
+ return;
265
+ }
266
+ const ports = allowedBrowserPorts(resolvedPort, env);
267
+ if (!isAllowedBrowserOrigin(origin, req.headers.host, ports, host, [...extraAllowedOrigins])) {
268
+ if (req.method !== 'GET' || !isPortlessLoopbackOrigin(String(origin))) {
269
+ res.status(403).json({ error: 'Cross-origin requests are not allowed' });
270
+ return;
271
+ }
272
+ }
273
+ next();
274
+ });
275
+ }
276
+ //# sourceMappingURL=api-security-middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-security-middleware.js","sourceRoot":"","sources":["../src/api-security-middleware.ts"],"names":[],"mappings":"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"}