@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.
- package/LICENSE +201 -0
- package/README.md +107 -0
- package/dist/active-context.d.ts +99 -0
- package/dist/active-context.d.ts.map +1 -0
- package/dist/active-context.js +77 -0
- package/dist/active-context.js.map +1 -0
- package/dist/adapter.d.ts +36 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +92 -0
- package/dist/adapter.js.map +1 -0
- package/dist/agents.d.ts +55 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +23 -0
- package/dist/agents.js.map +1 -0
- package/dist/api-security-middleware.d.ts +146 -0
- package/dist/api-security-middleware.d.ts.map +1 -0
- package/dist/api-security-middleware.js +276 -0
- package/dist/api-security-middleware.js.map +1 -0
- package/dist/attachments.d.ts +317 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +664 -0
- package/dist/attachments.js.map +1 -0
- package/dist/cancel-owned-runs.d.ts +44 -0
- package/dist/cancel-owned-runs.d.ts.map +1 -0
- package/dist/cancel-owned-runs.js +35 -0
- package/dist/cancel-owned-runs.js.map +1 -0
- package/dist/compat.d.ts +22 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +17 -0
- package/dist/compat.js.map +1 -0
- package/dist/component-catalog.d.ts +74 -0
- package/dist/component-catalog.d.ts.map +1 -0
- package/dist/component-catalog.js +74 -0
- package/dist/component-catalog.js.map +1 -0
- package/dist/connectors.d.ts +165 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +474 -0
- package/dist/connectors.js.map +1 -0
- package/dist/daemon-status.d.ts +72 -0
- package/dist/daemon-status.d.ts.map +1 -0
- package/dist/daemon-status.js +60 -0
- package/dist/daemon-status.js.map +1 -0
- package/dist/db-ops.d.ts +116 -0
- package/dist/db-ops.d.ts.map +1 -0
- package/dist/db-ops.js +191 -0
- package/dist/db-ops.js.map +1 -0
- package/dist/delegated-tools.d.ts +46 -0
- package/dist/delegated-tools.d.ts.map +1 -0
- package/dist/delegated-tools.js +155 -0
- package/dist/delegated-tools.js.map +1 -0
- package/dist/express/run-stream.d.ts +15 -0
- package/dist/express/run-stream.d.ts.map +1 -0
- package/dist/express/run-stream.js +11 -0
- package/dist/express/run-stream.js.map +1 -0
- package/dist/express-index.d.ts +32 -0
- package/dist/express-index.d.ts.map +1 -0
- package/dist/express-index.js +14 -0
- package/dist/express-index.js.map +1 -0
- package/dist/fastify/adapter.d.ts +31 -0
- package/dist/fastify/adapter.d.ts.map +1 -0
- package/dist/fastify/adapter.js +64 -0
- package/dist/fastify/adapter.js.map +1 -0
- package/dist/fastify/agents.d.ts +13 -0
- package/dist/fastify/agents.d.ts.map +1 -0
- package/dist/fastify/agents.js +7 -0
- package/dist/fastify/agents.js.map +1 -0
- package/dist/fastify/api-security-middleware.d.ts +64 -0
- package/dist/fastify/api-security-middleware.d.ts.map +1 -0
- package/dist/fastify/api-security-middleware.js +139 -0
- package/dist/fastify/api-security-middleware.js.map +1 -0
- package/dist/fastify/compat.d.ts +22 -0
- package/dist/fastify/compat.d.ts.map +1 -0
- package/dist/fastify/compat.js +16 -0
- package/dist/fastify/compat.js.map +1 -0
- package/dist/fastify/daemon-status.d.ts +22 -0
- package/dist/fastify/daemon-status.d.ts.map +1 -0
- package/dist/fastify/daemon-status.js +9 -0
- package/dist/fastify/daemon-status.js.map +1 -0
- package/dist/fastify/host-tools.d.ts +13 -0
- package/dist/fastify/host-tools.d.ts.map +1 -0
- package/dist/fastify/host-tools.js +8 -0
- package/dist/fastify/host-tools.js.map +1 -0
- package/dist/fastify/index.d.ts +36 -0
- package/dist/fastify/index.d.ts.map +1 -0
- package/dist/fastify/index.js +18 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/fastify/local-daemon-request.d.ts +43 -0
- package/dist/fastify/local-daemon-request.d.ts.map +1 -0
- package/dist/fastify/local-daemon-request.js +155 -0
- package/dist/fastify/local-daemon-request.js.map +1 -0
- package/dist/fastify/origin.d.ts +21 -0
- package/dist/fastify/origin.d.ts.map +1 -0
- package/dist/fastify/origin.js +14 -0
- package/dist/fastify/origin.js.map +1 -0
- package/dist/fastify/request.d.ts +20 -0
- package/dist/fastify/request.d.ts.map +1 -0
- package/dist/fastify/request.js +25 -0
- package/dist/fastify/request.js.map +1 -0
- package/dist/fastify/response.d.ts +20 -0
- package/dist/fastify/response.d.ts.map +1 -0
- package/dist/fastify/response.js +41 -0
- package/dist/fastify/response.js.map +1 -0
- package/dist/fastify/route-registration-guard.d.ts +70 -0
- package/dist/fastify/route-registration-guard.d.ts.map +1 -0
- package/dist/fastify/route-registration-guard.js +69 -0
- package/dist/fastify/route-registration-guard.js.map +1 -0
- package/dist/fastify/run-stream.d.ts +18 -0
- package/dist/fastify/run-stream.d.ts.map +1 -0
- package/dist/fastify/run-stream.js +10 -0
- package/dist/fastify/run-stream.js.map +1 -0
- package/dist/fastify/runs.d.ts +17 -0
- package/dist/fastify/runs.d.ts.map +1 -0
- package/dist/fastify/runs.js +33 -0
- package/dist/fastify/runs.js.map +1 -0
- package/dist/frontend-control.d.ts +131 -0
- package/dist/frontend-control.d.ts.map +1 -0
- package/dist/frontend-control.js +77 -0
- package/dist/frontend-control.js.map +1 -0
- package/dist/frontend-sessions.d.ts +100 -0
- package/dist/frontend-sessions.d.ts.map +1 -0
- package/dist/frontend-sessions.js +180 -0
- package/dist/frontend-sessions.js.map +1 -0
- package/dist/health.d.ts +91 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +53 -0
- package/dist/health.js.map +1 -0
- package/dist/host-tools.d.ts +174 -0
- package/dist/host-tools.d.ts.map +1 -0
- package/dist/host-tools.js +338 -0
- package/dist/host-tools.js.map +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/local-daemon-request.d.ts +44 -0
- package/dist/local-daemon-request.d.ts.map +1 -0
- package/dist/local-daemon-request.js +145 -0
- package/dist/local-daemon-request.js.map +1 -0
- package/dist/media.d.ts +131 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +352 -0
- package/dist/media.js.map +1 -0
- package/dist/memory.d.ts +230 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +331 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-proxy.d.ts +45 -0
- package/dist/model-proxy.d.ts.map +1 -0
- package/dist/model-proxy.js +619 -0
- package/dist/model-proxy.js.map +1 -0
- package/dist/origin-validation.d.ts +64 -0
- package/dist/origin-validation.d.ts.map +1 -0
- package/dist/origin-validation.js +226 -0
- package/dist/origin-validation.js.map +1 -0
- package/dist/origin.d.ts +32 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +14 -0
- package/dist/origin.js.map +1 -0
- package/dist/pack-http.d.ts +30 -0
- package/dist/pack-http.d.ts.map +1 -0
- package/dist/pack-http.js +12 -0
- package/dist/pack-http.js.map +1 -0
- package/dist/raw-sse.d.ts +44 -0
- package/dist/raw-sse.d.ts.map +1 -0
- package/dist/raw-sse.js +93 -0
- package/dist/raw-sse.js.map +1 -0
- package/dist/remote-run-events.d.ts +66 -0
- package/dist/remote-run-events.d.ts.map +1 -0
- package/dist/remote-run-events.js +230 -0
- package/dist/remote-run-events.js.map +1 -0
- package/dist/request.d.ts +19 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +25 -0
- package/dist/request.js.map +1 -0
- package/dist/research.d.ts +42 -0
- package/dist/research.d.ts.map +1 -0
- package/dist/research.js +316 -0
- package/dist/research.js.map +1 -0
- package/dist/response.d.ts +14 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +45 -0
- package/dist/response.js.map +1 -0
- package/dist/route-manifest.d.ts +31 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +103 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/route-registration-guard.d.ts +65 -0
- package/dist/route-registration-guard.d.ts.map +1 -0
- package/dist/route-registration-guard.js +71 -0
- package/dist/route-registration-guard.js.map +1 -0
- package/dist/routines.d.ts +114 -0
- package/dist/routines.d.ts.map +1 -0
- package/dist/routines.js +320 -0
- package/dist/routines.js.map +1 -0
- package/dist/run-stream.d.ts +60 -0
- package/dist/run-stream.d.ts.map +1 -0
- package/dist/run-stream.js +108 -0
- package/dist/run-stream.js.map +1 -0
- package/dist/runs.d.ts +79 -0
- package/dist/runs.d.ts.map +1 -0
- package/dist/runs.js +232 -0
- package/dist/runs.js.map +1 -0
- package/dist/sse.d.ts +139 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +157 -0
- package/dist/sse.js.map +1 -0
- package/dist/terminals.d.ts +125 -0
- package/dist/terminals.d.ts.map +1 -0
- package/dist/terminals.js +349 -0
- package/dist/terminals.js.map +1 -0
- package/dist/tool-catalog.d.ts +68 -0
- package/dist/tool-catalog.d.ts.map +1 -0
- package/dist/tool-catalog.js +68 -0
- package/dist/tool-catalog.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-root.d.ts +69 -0
- package/dist/workspace-root.d.ts.map +1 -0
- package/dist/workspace-root.js +63 -0
- package/dist/workspace-root.js.map +1 -0
- package/dist/xai.d.ts +86 -0
- package/dist/xai.d.ts.map +1 -0
- package/dist/xai.js +668 -0
- package/dist/xai.js.map +1 -0
- package/package.json +58 -0
package/dist/research.js
ADDED
|
@@ -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"}
|
package/dist/response.js
ADDED
|
@@ -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"}
|