@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,316 @@
1
+ /**
2
+ * @module research
3
+ *
4
+ * `POST /api/research/search` — a thin route wrapper over Tavily's real Search API
5
+ * (`POST {baseUrl}/search`), ported from OD's `apps/daemon/src/research/tavily.ts` (the low-level
6
+ * Tavily client) **and** `apps/daemon/src/research/index.ts#searchResearch` (the findings-assembly
7
+ * wrapper the real `/api/research/search` route in `apps/daemon/src/routes/media.ts` actually
8
+ * calls). Verified against a **live** boot of OD's real daemon (`apps/daemon/bin/od.mjs`, from the
9
+ * sibling `/Users/la/Desktop/Programming/OSS-Repos/open-design` checkout on this machine) hitting
10
+ * the real route with a mock Tavily backend, not just static reading — see the field-by-field notes
11
+ * below for what that live diff actually found. The request/response shape and defaults are ported
12
+ * (`search_depth: 'basic'`, `include_answer: true`, `include_raw_content: false`, a 20-result hard
13
+ * cap, the `results[]` -> `ResearchSource[]` mapping, the 1000-char query cap, the
14
+ * `Math.floor`+clamp-to-`[1,20]` `maxSources` normalization, and the `NO_RESEARCH_SOURCES`-style
15
+ * "zero sources is a failure, not an empty success" behavior) — OD's own `TavilyError`/
16
+ * `ResearchError` classes and `requestInit.dispatcher` passthrough are not carried over; this route
17
+ * pack's own SEC-005 error-handling convention (a generic `INTERNAL_ERROR` + correlation id,
18
+ * matching every other route pack in this package) replaces the origin's thrown-error shape for
19
+ * genuinely unexpected failures. OD's `AbortController`-based 30s timeout **is** carried over
20
+ * (added after an external audit flagged its absence — a stalled/malicious upstream could otherwise
21
+ * hold a request indefinitely, since bare `fetch` has no default timeout): `tavilySearch` aborts
22
+ * the request after `DEFAULT_TAVILY_TIMEOUT_MS` and clears the timer in a `finally` regardless of
23
+ * outcome. No other request-level knobs (connection pooling, `dispatcher`) are exposed yet — a
24
+ * future pass can add them the same way `media.ts#MediaGenerationRequestInit` optionally threads a
25
+ * `dispatcher` through, if a real need for one shows up.
26
+ *
27
+ * **`ResearchSource` field names match OD's real `@open-design/contracts/api/research.ts`
28
+ * `ResearchSource` shape** (`title`/`url`/`snippet`/`publishedAt?`/`provider`). **`ResearchSearchResponse`
29
+ * matches that same contract file's `ResearchFindings` shape** (`query`/`summary`/`sources`/
30
+ * `provider`/`depth`/`fetchedAt`) — an earlier version of this port conflated the *raw Tavily
31
+ * client's* return shape (`tavily.ts`'s `{ answer, sources }`) with the *route's actual HTTP
32
+ * response* shape (`research/index.ts#searchResearch`'s `ResearchFindings`), which the live daemon
33
+ * diff caught: hitting the real `/api/research/search` with a mock Tavily backend returned
34
+ * `{"query":"...","summary":"...","sources":[...],"provider":"tavily","depth":"shallow",
35
+ * "fetchedAt":<ms>}`, never `{answer, sources}`. `depth` is hardcoded to the `'shallow'` literal
36
+ * (rather than OD's full `'shallow' | 'medium' | 'deep'` `ResearchDepth` union) since this port, like
37
+ * OD's real Phase-1 `searchResearch`, only ever produces `'shallow'` — there is no depth-selection
38
+ * request field on either side yet.
39
+ *
40
+ * **`providers` request field**: OD's real route accepts an optional `providers: string[]` and only
41
+ * inspects `providers[0]` (defaulting to `'tavily'` when absent/empty); a non-array value is treated
42
+ * as absent (not rejected), and a first element other than `'tavily'` is rejected with a 400 before
43
+ * credentials are even resolved (`UNSUPPORTED_RESEARCH_PROVIDER` in OD; this port's own generic
44
+ * `BAD_REQUEST` validation-error convention here, same as every other `parse`-time rejection in this
45
+ * file). Confirmed live: `providers: ["bing"]` 400s on the real daemon, `providers: ["tavily"]`
46
+ * succeeds identically to omitting the field.
47
+ *
48
+ * **Credential resolution**: `resolveCredentials` defaults to this transport's narrow
49
+ * `TAVILY_API_KEY` environment seam. A host with its own secrets layer supplies its own resolver
50
+ * instead; `@jini-ai/http-kit` deliberately does not depend on the incubating media implementation
51
+ * package. Note OD's real env-var precedence is
52
+ * `['OD_TAVILY_API_KEY', 'TAVILY_API_KEY']` (checked live via `apps/daemon/tests/research.test.ts`'s
53
+ * `TAVILY_ENV_KEYS`) — this package's own `@jini-ai/media#PROVIDER_CREDENTIAL_ENV_VARS.tavily` only
54
+ * lists `['TAVILY_API_KEY']`, deliberately dropping the `OD_`-prefixed variant: this repo's own
55
+ * `AGENTS.md` boundary rule bans product-identity strings (including the `OD_` prefix) inside
56
+ * `packages/@jini-ai/**`, so that omission is a hard constraint, not a port gap.
57
+ *
58
+ * **Missing credentials is a clean `NOT_CONFIGURED` (503), not an `INTERNAL_ERROR`.** No API key
59
+ * configured is an expected, non-exceptional state (matching `connectors.ts`'s "capability exists,
60
+ * reachable, inert until configured" shape) — it never reaches the SEC-005 sink, since there is no
61
+ * real exception or secret to hide, just an honest "not set up yet" answer. OD's real behavior here
62
+ * (confirmed live) is a 400 `TAVILY_API_KEY_MISSING`, not a 503 — this is a deliberate, documented
63
+ * divergence from OD (this package's own generic-error-code convention, applied consistently across
64
+ * every route pack) rather than an oversight, so it is intentionally **not** changed to match OD's
65
+ * 400.
66
+ *
67
+ * **Zero sources is a `NOT_FOUND` (404), not a `200` with an empty array.** OD's real
68
+ * `searchResearch` throws `ResearchError('no sources found', 404, 'NO_RESEARCH_SOURCES')` when
69
+ * Tavily's `results[]` maps to zero usable sources (confirmed live against a mock Tavily backend
70
+ * that returns `results: []`) — this port matches that with the package's own generic `NOT_FOUND`
71
+ * code (mapped to 404 by `response.ts#ERROR_STATUS_BY_CODE`) rather than inventing a
72
+ * research-specific code, same rationale as the `NOT_CONFIGURED` choice above.
73
+ *
74
+ * **Query normalization**: OD's real `searchResearch` trims the query and caps it at 1000 characters
75
+ * before ever calling Tavily (`(input.query?.trim() || '').slice(0, 1000)`) — confirmed live by
76
+ * sending a padded, 1112-char query and observing the daemon forward exactly 1000 trimmed
77
+ * characters to the mock Tavily backend. This port replicates both the trim and the cap in
78
+ * `parseResearchSearch`.
79
+ *
80
+ * **`maxSources` normalization**: OD's real `clampMaxSources` is `Math.max(1, Math.min(Math.floor(value),
81
+ * 20))` — confirmed live by sending `maxSources: 2.7` and observing `max_results: 2` (not `2.7`) on
82
+ * the wire to the mock Tavily backend. An earlier version of this port only clamped the range
83
+ * (`Math.max(0, Math.min(value, 20))`) without flooring, which could forward a fractional
84
+ * `max_results` straight to Tavily's real API for any non-integer input (e.g. `maxSources: 0.5`
85
+ * would have clamped to `0.5`, not `1`); this port now floors first, matching OD.
86
+ *
87
+ * **SEC-005 for everything else**: a genuine Tavily request/response failure is caught, reported
88
+ * through `onInternalError` (defaults to `console.error`, matching every other route pack), and
89
+ * surfaced to the caller as a generic `INTERNAL_ERROR` — the raw error message (which can echo
90
+ * request details) is never sent to the HTTP caller directly. `redactSecrets` (reused from
91
+ * `@jini-ai/agent-runtime`'s `connection-guard.ts`, already reachable via this package's existing
92
+ * `@jini-ai/agent-runtime` dependency — the same "shared primitive, one implementation" reuse this
93
+ * task's agent-runtime work applied to `sse-decode.ts`/`turn-end-guard.ts`) strips the bearer
94
+ * token out of any upstream error text before it is even logged, belt-and-braces alongside the
95
+ * generic-message substitution.
96
+ */
97
+ import { randomUUID } from 'node:crypto';
98
+ import { redactSecrets, validateBaseUrl } from '@jini-ai/agent-runtime';
99
+ import { createApiError } from '@jini-ai/protocol';
100
+ import { defineJsonRoute, mountJsonRoute } from './adapter.js';
101
+ import { validationError } from './request.js';
102
+ import { err, ok } from './types.js';
103
+ const DEFAULT_TAVILY_BASE_URL = 'https://api.tavily.com';
104
+ /** Tavily's own documented cap on `max_results` (see OD's `tavily.ts#TAVILY_MAX_RESULTS_LIMIT`). */
105
+ const TAVILY_MAX_RESULTS_LIMIT = 20;
106
+ const DEFAULT_MAX_SOURCES = 5;
107
+ /** Matches OD's own `tavily.ts` timeout — see module doc. */
108
+ const DEFAULT_TAVILY_TIMEOUT_MS = 30_000;
109
+ /** Matches OD's real `research/index.ts#searchResearch` query cap — see module doc. */
110
+ const MAX_QUERY_LENGTH = 1000;
111
+ function defaultInternalErrorSink(context) {
112
+ // eslint-disable-next-line no-console
113
+ console.error(`[@jini-ai/http-kit] internal error (research/search, correlationId=${context.correlationId})`, context.error);
114
+ }
115
+ /**
116
+ * Default `ResearchHttpDeps.resolveCredentials`: reads `TAVILY_API_KEY` from the environment.
117
+ *
118
+ * Takes no `providerId` even though the `resolveCredentials` dep type supplies one (a function of
119
+ * fewer parameters is assignable, so this still satisfies the interface). It used to accept one and
120
+ * return `{}` for anything other than `'tavily'`, but that arm was unreachable twice over: the only
121
+ * call site below passes the literal `'tavily'`, and `validateRequestedProvider` already rejects any
122
+ * other provider name at parse time. A host that supports a second provider injects its own
123
+ * `resolveCredentials` rather than reaching this function with a different id.
124
+ */
125
+ async function defaultResolveCredentials() {
126
+ const apiKey = process.env.TAVILY_API_KEY?.trim();
127
+ return apiKey ? { apiKey } : {};
128
+ }
129
+ function isRecord(value) {
130
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
131
+ }
132
+ function nonEmptyString(value) {
133
+ return typeof value === 'string' && value.trim().length > 0 ? value : null;
134
+ }
135
+ /**
136
+ * Only `'tavily'` is a supported provider (matches OD's real Phase-1 `searchResearch` — see module
137
+ * doc). OD only inspects `providers[0]` and treats a non-array `providers` value as absent rather
138
+ * than rejecting it; this mirrors both.
139
+ */
140
+ function validateRequestedProvider(rawProviders) {
141
+ if (!Array.isArray(rawProviders))
142
+ return ok(null);
143
+ const filtered = rawProviders.filter((p) => typeof p === 'string' && p.length > 0);
144
+ const requested = filtered[0];
145
+ if (requested !== undefined && requested !== 'tavily') {
146
+ return err(validationError(`provider "${requested}" is not supported — only "tavily" is available`));
147
+ }
148
+ return ok(null);
149
+ }
150
+ function parseResearchSearch(input) {
151
+ if (!isRecord(input.body))
152
+ return err(validationError('body must be a JSON object'));
153
+ const rawQuery = nonEmptyString(input.body.query);
154
+ if (!rawQuery)
155
+ return err(validationError('query must be a non-empty string', [{ path: 'query', message: 'required non-empty string' }]));
156
+ const query = rawQuery.trim().slice(0, MAX_QUERY_LENGTH);
157
+ const maxSources = input.body.maxSources;
158
+ if (maxSources !== undefined && (typeof maxSources !== 'number' || !Number.isFinite(maxSources) || maxSources <= 0)) {
159
+ return err(validationError('maxSources must be a positive number when provided'));
160
+ }
161
+ const providerCheck = validateRequestedProvider(input.body.providers);
162
+ if (!providerCheck.ok)
163
+ return providerCheck;
164
+ return ok({ query, ...(maxSources !== undefined ? { maxSources: maxSources } : {}) });
165
+ }
166
+ /**
167
+ * Calls Tavily's real `POST /search` endpoint directly — see module doc for the port's exact
168
+ * scope/provenance. Throws (never a `Result`) on any failure; the route handler below is the one
169
+ * place that converts a thrown error into the package's SEC-005 response shape.
170
+ *
171
+ * `apiKey` is a separate, non-optional parameter rather than being read off `credentials`, so the
172
+ * "credential is present" invariant is carried by the type system instead of by a runtime guard.
173
+ * This function used to take only `credentials` and re-check `credentials.apiKey` itself, but the
174
+ * sole caller below already returns `NOT_CONFIGURED` before it ever gets here — the check was
175
+ * unreachable by construction, so it is now expressed as a required argument instead of dead code.
176
+ * `credentials` is still passed for its optional transport config (`baseUrl`).
177
+ */
178
+ async function tavilySearch(apiKey, credentials, request) {
179
+ const baseUrl = credentials.baseUrl ?? DEFAULT_TAVILY_BASE_URL;
180
+ const baseUrlCheck = validateBaseUrl(baseUrl);
181
+ if (baseUrlCheck.error) {
182
+ throw new Error(baseUrlCheck.error);
183
+ }
184
+ const base = baseUrl.replace(/\/+$/, '');
185
+ // Floor before clamping, matching OD's real `clampMaxSources` — see module doc. Without the
186
+ // floor, a fractional `maxSources` (e.g. `2.7`, or `0.5` before the `Math.max(1, ...)` floor
187
+ // below) would forward a non-integer `max_results` straight to Tavily's real API.
188
+ const maxResults = Math.max(1, Math.min(Math.floor(request.maxSources ?? DEFAULT_MAX_SOURCES), TAVILY_MAX_RESULTS_LIMIT));
189
+ const body = {
190
+ query: request.query,
191
+ search_depth: 'basic',
192
+ max_results: maxResults,
193
+ include_answer: true,
194
+ include_raw_content: false,
195
+ };
196
+ // The timeout must stay armed for the full operation, not just until `fetch` resolves —
197
+ // `fetch` resolves as soon as response headers arrive, before the body is read. A round-2
198
+ // external audit caught (and locally reproduced) an earlier version of this fix that cleared
199
+ // the timer right after `fetch` resolved, leaving `response.text()`/`response.json()` below
200
+ // completely unbounded against a server that sends headers promptly and then stalls the body.
201
+ //
202
+ // `TavilyHttpError` marks an error already redacted and formatted by the `!response.ok` branch
203
+ // below, so the outer catch can rethrow it verbatim instead of wrapping it a second time (which
204
+ // would otherwise produce a "Tavily request failed: Tavily 429: ..." double-prefixed message).
205
+ class TavilyHttpError extends Error {
206
+ }
207
+ const timeoutController = new AbortController();
208
+ const timeoutHandle = setTimeout(() => timeoutController.abort(), DEFAULT_TAVILY_TIMEOUT_MS);
209
+ // Assigned in the `try` and returned *after* the `finally`, rather than returned from inside the
210
+ // `try`. Both forms behave identically, but a return-through-`finally` compiles to three copies of
211
+ // the `finally` body (normal completion, return, throw) and one of those is unreachable here — the
212
+ // `try` cannot fall off its end and the `catch` always throws. Falling through to a single return
213
+ // keeps the cleanup path honestly measurable instead of permanently one-third uncovered.
214
+ let output;
215
+ try {
216
+ const response = await fetch(`${base}/search`, {
217
+ method: 'POST',
218
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
219
+ body: JSON.stringify(body),
220
+ signal: timeoutController.signal,
221
+ });
222
+ if (!response.ok) {
223
+ const text = await response.text().catch(() => '');
224
+ throw new TavilyHttpError(redactSecrets(`Tavily ${response.status}: ${text.slice(0, 200) || 'no body'}`, [apiKey]));
225
+ }
226
+ const json = (await response.json());
227
+ const answer = typeof json.answer === 'string' ? json.answer : '';
228
+ const rawResults = Array.isArray(json.results) ? json.results : [];
229
+ const sources = [];
230
+ for (const raw of rawResults) {
231
+ const url = typeof raw.url === 'string' ? raw.url : '';
232
+ if (!url)
233
+ continue;
234
+ const publishedAt = typeof raw.published_date === 'string' && raw.published_date.trim() ? raw.published_date.trim() : undefined;
235
+ sources.push({
236
+ title: typeof raw.title === 'string' && raw.title.trim() ? raw.title.trim() : url,
237
+ url,
238
+ snippet: typeof raw.content === 'string' ? raw.content.trim().slice(0, 800) : '',
239
+ provider: 'tavily',
240
+ ...(publishedAt ? { publishedAt } : {}),
241
+ });
242
+ }
243
+ output = { answer, sources };
244
+ }
245
+ catch (error) {
246
+ if (!timeoutController.signal.aborted && error instanceof TavilyHttpError) {
247
+ throw error;
248
+ }
249
+ const message = error instanceof Error ? error.message : String(error);
250
+ throw new Error(redactSecrets(timeoutController.signal.aborted
251
+ ? `Tavily request timed out after ${DEFAULT_TAVILY_TIMEOUT_MS}ms`
252
+ : `Tavily request failed: ${message}`, [apiKey]));
253
+ }
254
+ finally {
255
+ clearTimeout(timeoutHandle);
256
+ }
257
+ return output;
258
+ }
259
+ /** Ported verbatim from OD's real `research/index.ts#synthesizeFallbackSummary` — see module doc. Used when Tavily returns no `answer` text so the caller still gets a usable `summary`. */
260
+ function synthesizeFallbackSummary(sources) {
261
+ const lead = sources
262
+ .slice(0, 5)
263
+ .map((s, i) => `- [${i + 1}] ${s.title}: ${s.snippet.slice(0, 200)}`)
264
+ .join('\n');
265
+ return `(No provider summary; top snippets follow.)\n${lead}`;
266
+ }
267
+ export const researchSearchRoute = defineJsonRoute({
268
+ method: 'post',
269
+ path: '/api/research/search',
270
+ requireSameOrigin: true,
271
+ parse: parseResearchSearch,
272
+ handle: async (request, deps) => {
273
+ const resolveCredentials = deps.resolveCredentials ?? defaultResolveCredentials;
274
+ const onInternalError = deps.onInternalError ?? defaultInternalErrorSink;
275
+ let credentials;
276
+ try {
277
+ credentials = await resolveCredentials('tavily');
278
+ }
279
+ catch (error) {
280
+ const correlationId = randomUUID();
281
+ onInternalError({ correlationId, error });
282
+ return err(createApiError('INTERNAL_ERROR', 'an internal error occurred', { requestId: correlationId }));
283
+ }
284
+ const apiKey = credentials.apiKey;
285
+ if (!apiKey) {
286
+ return err(createApiError('NOT_CONFIGURED', 'tavily provider not configured'));
287
+ }
288
+ try {
289
+ const raw = await tavilySearch(apiKey, credentials, request);
290
+ // Matches OD's real `NO_RESEARCH_SOURCES` (404) — see module doc. Zero usable sources is a
291
+ // failure, not an empty success, even when the upstream Tavily call itself succeeded.
292
+ if (raw.sources.length === 0) {
293
+ return err(createApiError('NOT_FOUND', 'no sources found'));
294
+ }
295
+ const result = {
296
+ query: request.query,
297
+ summary: raw.answer || synthesizeFallbackSummary(raw.sources),
298
+ sources: raw.sources,
299
+ provider: 'tavily',
300
+ depth: 'shallow',
301
+ fetchedAt: Date.now(),
302
+ };
303
+ return ok(result);
304
+ }
305
+ catch (error) {
306
+ const correlationId = randomUUID();
307
+ onInternalError({ correlationId, error });
308
+ return err(createApiError('INTERNAL_ERROR', 'an internal error occurred', { requestId: correlationId }));
309
+ }
310
+ },
311
+ });
312
+ /** Mounts `POST /api/research/search` on `app`. A pack's `http(app, services)` calls this directly. */
313
+ export function registerResearchRoutes(app, deps, adapter) {
314
+ mountJsonRoute(app, researchSearchRoute, deps, adapter);
315
+ }
316
+ //# sourceMappingURL=research.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"research.js","sourceRoot":"","sources":["../src/research.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Response-side serialization: writes a plain JSON body or a standard `ApiError` envelope onto
3
+ * an Express `Response`, and maps an `ApiErrorCode` to its default HTTP status. No internal
4
+ * dependencies — this is the terminal step of the Adapter's error-handling pipeline.
5
+ */
6
+ import type { Response } from 'express';
7
+ import { type ApiError } from '@jini-ai/protocol';
8
+ /** Writes `body` as JSON with the given status code. */
9
+ export declare function sendJson(res: Response, status: number, body: unknown): void;
10
+ /** Writes an `ApiError`, wrapped in the standard `{ error }` envelope, with the given status code. */
11
+ export declare function sendApiError(res: Response, status: number, error: ApiError): void;
12
+ /** Resolves the HTTP status to send for an `ApiError`, defaulting to 500 for unmapped codes. */
13
+ export declare function statusForError(error: ApiError): number;
14
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../src/response.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAA0B,KAAK,QAAQ,EAAqB,MAAM,mBAAmB,CAAC;AAE7F,wDAAwD;AACxD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAE3E;AAED,sGAAsG;AACtG,wBAAgB,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI,CAEjF;AAkCD,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAEtD"}
@@ -0,0 +1,45 @@
1
+ import { createApiErrorResponse } from '@jini-ai/protocol';
2
+ /** Writes `body` as JSON with the given status code. */
3
+ export function sendJson(res, status, body) {
4
+ res.status(status).json(body);
5
+ }
6
+ /** Writes an `ApiError`, wrapped in the standard `{ error }` envelope, with the given status code. */
7
+ export function sendApiError(res, status, error) {
8
+ res.status(status).json(createApiErrorResponse(error));
9
+ }
10
+ /**
11
+ * @internal
12
+ * Default HTTP status per generic `ApiErrorCode`. Codes not listed here fall back to 500 in
13
+ * `statusForError` — deliberately conservative, since an unmapped code (including any
14
+ * product-specific code a consuming pack defines) is more likely a new server-side failure mode
15
+ * than a client error.
16
+ */
17
+ const ERROR_STATUS_BY_CODE = {
18
+ BAD_REQUEST: 400,
19
+ UNAUTHORIZED: 401,
20
+ FORBIDDEN: 403,
21
+ NOT_FOUND: 404,
22
+ CONFLICT: 409,
23
+ PAYLOAD_TOO_LARGE: 413,
24
+ UNSUPPORTED_MEDIA_TYPE: 415,
25
+ VALIDATION_FAILED: 422,
26
+ RATE_LIMITED: 429,
27
+ INTERNAL_ERROR: 500,
28
+ UPSTREAM_UNAVAILABLE: 502,
29
+ SERVICE_UNAVAILABLE: 503,
30
+ NOT_CONFIGURED: 503,
31
+ TOOL_TOKEN_MISSING: 401,
32
+ TOOL_TOKEN_INVALID: 401,
33
+ TOOL_TOKEN_EXPIRED: 401,
34
+ TOOL_ENDPOINT_DENIED: 403,
35
+ TOOL_OPERATION_DENIED: 403,
36
+ TOOL_NOT_AVAILABLE: 503,
37
+ REMOTE_TOOL_BRIDGE_NOT_CONFIGURED: 503,
38
+ REMOTE_TOOL_BRIDGE_TOKEN_REQUIRED: 401,
39
+ OAUTH_FLOW_IN_PROGRESS: 409,
40
+ };
41
+ /** Resolves the HTTP status to send for an `ApiError`, defaulting to 500 for unmapped codes. */
42
+ export function statusForError(error) {
43
+ return ERROR_STATUS_BY_CODE[error.code] ?? 500;
44
+ }
45
+ //# sourceMappingURL=response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.js","sourceRoot":"","sources":["../src/response.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,sBAAsB,EAAoC,MAAM,mBAAmB,CAAC;AAE7F,wDAAwD;AACxD,MAAM,UAAU,QAAQ,CAAC,GAAa,EAAE,MAAc,EAAE,IAAa;IACnE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,YAAY,CAAC,GAAa,EAAE,MAAc,EAAE,KAAe;IACzE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,oBAAoB,GAA0C;IAClE,WAAW,EAAE,GAAG;IAChB,YAAY,EAAE,GAAG;IACjB,SAAS,EAAE,GAAG;IACd,SAAS,EAAE,GAAG;IACd,QAAQ,EAAE,GAAG;IACb,iBAAiB,EAAE,GAAG;IACtB,sBAAsB,EAAE,GAAG;IAC3B,iBAAiB,EAAE,GAAG;IACtB,YAAY,EAAE,GAAG;IACjB,cAAc,EAAE,GAAG;IACnB,oBAAoB,EAAE,GAAG;IACzB,mBAAmB,EAAE,GAAG;IACxB,cAAc,EAAE,GAAG;IACnB,kBAAkB,EAAE,GAAG;IACvB,kBAAkB,EAAE,GAAG;IACvB,kBAAkB,EAAE,GAAG;IACvB,oBAAoB,EAAE,GAAG;IACzB,qBAAqB,EAAE,GAAG;IAC1B,kBAAkB,EAAE,GAAG;IACvB,iCAAiC,EAAE,GAAG;IACtC,iCAAiC,EAAE,GAAG;IACtC,sBAAsB,EAAE,GAAG;CAC5B,CAAC;AAEF,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,OAAO,oBAAoB,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;AACjD,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { RouteRegistration } from './route-registration-guard.js';
2
+ /**
3
+ * Route families described by this manifest, keyed by the same feature id `@jini-ai/server`'s built-in
4
+ * feature catalog uses — so a host that enables features by name can look routes up by that same name.
5
+ */
6
+ export declare const JINI_ROUTE_MANIFEST: Readonly<Record<string, readonly RouteRegistration[]>>;
7
+ /**
8
+ * The routes one family mounts.
9
+ *
10
+ * @param family - A feature id, e.g. `'runs'`.
11
+ * @returns That family's registrations, or `undefined` when the family is not described by this
12
+ * manifest. The distinction matters: a caller building a proxy allow-list must treat `undefined` as
13
+ * "look it up yourself", never as "this family has no routes" — see this module's scope note.
14
+ * @complexity O(1).
15
+ * @overallScore 100/100
16
+ */
17
+ export declare function routeFamilyManifest(family: string): readonly RouteRegistration[] | undefined;
18
+ /**
19
+ * Every route across every family this manifest describes, de-duplicated.
20
+ *
21
+ * De-duplication is real rather than defensive: `health` intentionally mounts both `/health` and
22
+ * `/api/health` as separate routes, and a future family could legitimately share a path with another.
23
+ *
24
+ * @param families - Family ids to include. Unknown ids contribute nothing (rather than throwing), so a
25
+ * caller can pass its enabled-feature list verbatim without pre-filtering it.
26
+ * @returns One entry per distinct `METHOD PATH`, in first-seen order.
27
+ * @complexity O(n) in the total routes across the named families.
28
+ * @overallScore 100/100
29
+ */
30
+ export declare function manifestRoutesForFamilies(families: readonly string[]): readonly RouteRegistration[];
31
+ //# sourceMappingURL=route-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-manifest.d.ts","sourceRoot":"","sources":["../src/route-manifest.ts"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAqBvE;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC,CAkBtF,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,iBAAiB,EAAE,GAAG,SAAS,CAE5F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,iBAAiB,EAAE,CAYnG"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @module route-manifest
3
+ *
4
+ * Which `{method, path}` pairs each route family mounts, as inert data a caller can read *without*
5
+ * mounting anything.
6
+ *
7
+ * **The problem this solves.** A host that puts a reverse proxy in front of a Jini daemon in another
8
+ * process has to know that daemon's route list to forward anything. With no published inventory, the
9
+ * only way to build one is to copy path strings by hand — and a hand-copied list silently falls behind
10
+ * the moment a family gains a route. That has already happened at least once in a real consumer: a
11
+ * proxy shipped without `GET /api/runs` and the daemon's list endpoint was simply unreachable, 404-ing
12
+ * at the host's own router with nothing to indicate the host was the cause.
13
+ *
14
+ * **Why this file cannot itself drift.** It declares no method or path literals. Every entry is derived
15
+ * from the very `JsonRouteSpec` constant the family's `register*Routes` function mounts, so a spec's
16
+ * path change moves the manifest with it, automatically. The single exception is the SSE run-event
17
+ * route, which is registered with a bare `app.get` rather than a spec — and it contributes the
18
+ * already-exported `RUN_EVENTS_ROUTE_PATH` constant rather than a copy of it.
19
+ *
20
+ * That leaves exactly one failure mode: a family gains a route and nobody adds it *here*. The paired
21
+ * test (`__tests__/route-manifest.test.ts`) closes that by mounting each declared family's real
22
+ * registrar onto a recording app and asserting the manifest matches what actually registered — so a
23
+ * missing entry fails a test rather than reaching a consumer.
24
+ *
25
+ * **Scope, stated honestly: this covers the families a sidecar consumer proxies**, not all 19 the
26
+ * package can mount. {@link routeFamilyManifest} returns `undefined` for anything undeclared rather
27
+ * than an empty list, so "not described here" is distinguishable from "has no routes" — a proxy must
28
+ * never conclude a family is empty when it is merely undocumented. Adding a family is a matter of
29
+ * listing its exported specs below; the test then holds it correct.
30
+ */
31
+ import { agentListRoute, agentRescanRoute } from './agents.js';
32
+ import { delegatedToolExecuteRoute } from './delegated-tools.js';
33
+ import { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyRoute, versionInfoRoute, } from './health.js';
34
+ import { RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
35
+ import { toolCatalogDescribeRoute, toolCatalogSearchRoute } from './tool-catalog.js';
36
+ /**
37
+ * Projects route specs onto `RouteRegistration`s, upper-casing the method so entries compare directly
38
+ * against {@link getRouteRegistrationInventory}'s output.
39
+ *
40
+ * @param specs - Route specs, passed by reference so no path literal is ever restated.
41
+ * @returns One registration per spec, in argument order.
42
+ * @complexity O(n) in the number of specs.
43
+ * @overallScore 100/100
44
+ */
45
+ function fromSpecs(...specs) {
46
+ return specs.map((spec) => ({ method: spec.method.toUpperCase(), path: spec.path }));
47
+ }
48
+ /**
49
+ * Route families described by this manifest, keyed by the same feature id `@jini-ai/server`'s built-in
50
+ * feature catalog uses — so a host that enables features by name can look routes up by that same name.
51
+ */
52
+ export const JINI_ROUTE_MANIFEST = {
53
+ health: fromSpecs(healthRoute, apiHealthRoute, readyRoute, apiReadyRoute, versionInfoRoute, apiVersionInfoRoute),
54
+ runs: [
55
+ ...fromSpecs(runStartRoute, runListRoute, runStatusRoute, runCancelRoute),
56
+ // Registered with a bare `app.get` rather than a route spec (by `registerRunRoutes` itself), so
57
+ // it contributes its exported path constant instead of a copied literal.
58
+ { method: 'GET', path: RUN_EVENTS_ROUTE_PATH },
59
+ ],
60
+ agents: fromSpecs(agentListRoute, agentRescanRoute),
61
+ toolCatalog: fromSpecs(toolCatalogSearchRoute, toolCatalogDescribeRoute),
62
+ delegatedToolCalls: fromSpecs(delegatedToolExecuteRoute),
63
+ };
64
+ /**
65
+ * The routes one family mounts.
66
+ *
67
+ * @param family - A feature id, e.g. `'runs'`.
68
+ * @returns That family's registrations, or `undefined` when the family is not described by this
69
+ * manifest. The distinction matters: a caller building a proxy allow-list must treat `undefined` as
70
+ * "look it up yourself", never as "this family has no routes" — see this module's scope note.
71
+ * @complexity O(1).
72
+ * @overallScore 100/100
73
+ */
74
+ export function routeFamilyManifest(family) {
75
+ return JINI_ROUTE_MANIFEST[family];
76
+ }
77
+ /**
78
+ * Every route across every family this manifest describes, de-duplicated.
79
+ *
80
+ * De-duplication is real rather than defensive: `health` intentionally mounts both `/health` and
81
+ * `/api/health` as separate routes, and a future family could legitimately share a path with another.
82
+ *
83
+ * @param families - Family ids to include. Unknown ids contribute nothing (rather than throwing), so a
84
+ * caller can pass its enabled-feature list verbatim without pre-filtering it.
85
+ * @returns One entry per distinct `METHOD PATH`, in first-seen order.
86
+ * @complexity O(n) in the total routes across the named families.
87
+ * @overallScore 100/100
88
+ */
89
+ export function manifestRoutesForFamilies(families) {
90
+ const seen = new Set();
91
+ const routes = [];
92
+ for (const family of families) {
93
+ for (const route of JINI_ROUTE_MANIFEST[family] ?? []) {
94
+ const key = `${route.method} ${route.path}`;
95
+ if (seen.has(key))
96
+ continue;
97
+ seen.add(key);
98
+ routes.push(route);
99
+ }
100
+ }
101
+ return routes;
102
+ }
103
+ //# sourceMappingURL=route-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-manifest.js","sourceRoot":"","sources":["../src/route-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EACL,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AASrF;;;;;;;;GAQG;AACH,SAAS,SAAS,CAAC,GAAG,KAA+B;IACnD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACvF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAA2D;IACzF,MAAM,EAAE,SAAS,CACf,WAAW,EACX,cAAc,EACd,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,mBAAmB,CACpB;IACD,IAAI,EAAE;QACJ,GAAG,SAAS,CAAC,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,CAAC;QACzE,gGAAgG;QAChG,yEAAyE;QACzE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,qBAAqB,EAAE;KAC/C;IACD,MAAM,EAAE,SAAS,CAAC,cAAc,EAAE,gBAAgB,CAAC;IACnD,WAAW,EAAE,SAAS,CAAC,sBAAsB,EAAE,wBAAwB,CAAC;IACxE,kBAAkB,EAAE,SAAS,CAAC,yBAAyB,CAAC;CACzD,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,yBAAyB,CAAC,QAA2B;IACnE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,MAAM,GAAwB,EAAE,CAAC;IACvC,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,KAAK,MAAM,KAAK,IAAI,mBAAmB,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YACtD,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @module route-registration-guard
3
+ *
4
+ * Wraps an Express app's route-registration methods (`get`/`post`/`put`/`patch`/`delete`/
5
+ * `options`/`all`/`use`) to build a queryable inventory of every mounted route, and to guard a
6
+ * caller-supplied set of "must register at most once" route keys against accidental duplicate
7
+ * registration — e.g. two packs each mounting `POST /api/runs` would otherwise silently shadow
8
+ * one another instead of failing loudly at composition time.
9
+ *
10
+ * Genericized from an origin daemon's route-registration guard — see `source-map.md`. The
11
+ * origin hardcoded a fixed two-route guarded set (product routes with no meaning in the generic
12
+ * engine); here the guarded set is an injectable `ReadonlySet<string>` that defaults to empty, so
13
+ * installing this guard with no options is a pure inventory tap with no enforcement.
14
+ */
15
+ import type { Express } from 'express';
16
+ /** One recorded route registration: the HTTP verb and the literal string path it was mounted on. */
17
+ export interface RouteRegistration {
18
+ method: string;
19
+ path: string;
20
+ }
21
+ /**
22
+ * Builds the `METHOD PATH` key used to check `guardedRouteKeys` membership.
23
+ *
24
+ * @param method - The Express registration method name (any case; normalized to upper-case in the key).
25
+ * @param path - The first argument passed to that method. Only a string path can collide — a
26
+ * RegExp or array-of-paths route is never tracked, matching the inventory's own string-only
27
+ * recording below.
28
+ * @param guardedRouteKeys - The set of `METHOD PATH` keys this call installed a duplicate-registration
29
+ * guard for.
30
+ * @returns The matching key if `path` is a string and the key is in `guardedRouteKeys`, otherwise `null`.
31
+ * @complexity O(1) — a single `Set` lookup.
32
+ * @overallScore 100/100
33
+ */
34
+ export declare function guardedRouteKey(method: string, path: unknown, guardedRouteKeys: ReadonlySet<string>): string | null;
35
+ export interface InstallRouteRegistrationGuardOptions {
36
+ /** `METHOD PATH` keys (e.g. `'POST /api/runs'`) that must be registered at most once on `app`. Defaults to none — installing the guard with no options only builds the inventory, it enforces nothing. */
37
+ guardedRouteKeys?: ReadonlySet<string>;
38
+ }
39
+ /**
40
+ * Monkey-patches `app`'s route-registration methods to record every string-path registration into
41
+ * a per-app inventory (retrievable via {@link getRouteRegistrationInventory}) and to throw if any
42
+ * `options.guardedRouteKeys` entry is registered a second time.
43
+ *
44
+ * @param app - The Express app to instrument. Must not already have this guard installed (installing
45
+ * twice would double-wrap every method and double-count the inventory — not guarded against here
46
+ * since `createLocalNodeDaemon` and every other caller in this codebase calls this exactly once
47
+ * per app at composition time).
48
+ * @param options.guardedRouteKeys - See {@link InstallRouteRegistrationGuardOptions}.
49
+ * @returns Nothing — mutates `app` in place and stashes its inventory under a well-known `Symbol.for` key.
50
+ * @throws If a `guardedRouteKeys` entry is registered more than once, at the moment of the second registration.
51
+ * @complexity O(1) setup (patches 8 fixed methods); each wrapped call is O(1) beyond the wrapped original.
52
+ * @overallScore 100/100
53
+ */
54
+ export declare function installRouteRegistrationGuard(app: Express, options?: InstallRouteRegistrationGuardOptions): void;
55
+ /**
56
+ * Reads back every route {@link installRouteRegistrationGuard} recorded for `app`.
57
+ *
58
+ * @param app - An app the guard was installed on. Safe to call on an app with no guard installed
59
+ * (returns an empty array rather than throwing).
60
+ * @returns A fresh array copy each call — the caller can never mutate the guard's own internal inventory through the returned reference.
61
+ * @complexity O(n) in the number of routes registered so far.
62
+ * @overallScore 100/100
63
+ */
64
+ export declare function getRouteRegistrationInventory(app: Express): RouteRegistration[];
65
+ //# sourceMappingURL=route-registration-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-registration-guard.d.ts","sourceRoot":"","sources":["../src/route-registration-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,oGAAoG;AACpG,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAC,GACpC,MAAM,GAAG,IAAI,CAIf;AAED,MAAM,WAAW,oCAAoC;IACnD,0MAA0M;IAC1M,gBAAgB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACxC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,oCAAyC,GACjD,IAAI,CAyBN;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,EAAE,CAE/E"}