@jini-ai/agent-runtime 0.3.10 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +34 -17
  3. package/dist/acp-model-probe.d.ts +6 -4
  4. package/dist/acp-model-probe.js +3 -3
  5. package/dist/agent-protocol/acp/account-failure.d.ts +4 -2
  6. package/dist/agent-protocol/acp/account-failure.js +1 -1
  7. package/dist/agent-protocol/acp/json.d.ts +6 -6
  8. package/dist/agent-protocol/acp/json.js +3 -3
  9. package/dist/agent-protocol/acp/models.d.ts +10 -5
  10. package/dist/agent-protocol/acp/models.js +32 -31
  11. package/dist/agent-protocol/acp/session-params.d.ts +3 -1
  12. package/dist/agent-protocol/acp/session-params.js +2 -1
  13. package/dist/agent-protocol/acp/session.d.ts +28 -22
  14. package/dist/agent-protocol/acp/session.js +181 -147
  15. package/dist/agent-protocol/acp/text-suppression.d.ts +1 -1
  16. package/dist/agent-protocol/acp/text-suppression.js +1 -1
  17. package/dist/agent-protocol/acp/types.d.ts +3 -2
  18. package/dist/agent-protocol/acp/updates.d.ts +11 -11
  19. package/dist/agent-protocol/acp/updates.js +2 -2
  20. package/dist/agent-protocol/core/json-line-stream.d.ts +24 -8
  21. package/dist/agent-protocol/core/json-line-stream.js +8 -8
  22. package/dist/agent-protocol/pi-rpc/events.d.ts +5 -1
  23. package/dist/agent-protocol/pi-rpc/events.js +36 -36
  24. package/dist/agent-protocol/pi-rpc/internal.d.ts +4 -1
  25. package/dist/agent-protocol/pi-rpc/models.d.ts +3 -1
  26. package/dist/agent-protocol/pi-rpc/models.js +1 -1
  27. package/dist/agent-protocol/pi-rpc/session.d.ts +1 -1
  28. package/dist/agent-protocol/pi-rpc/session.js +33 -32
  29. package/dist/amr-model-cache.d.ts +12 -4
  30. package/dist/amr-model-cache.js +17 -21
  31. package/dist/amr-profile-resolver.d.ts +3 -1
  32. package/dist/anthropic-live-models.js +0 -0
  33. package/dist/artifact-taxonomy.d.ts +6 -2
  34. package/dist/auth.d.ts +52 -15
  35. package/dist/auth.js +36 -36
  36. package/dist/claude-code-models.js +2 -2
  37. package/dist/claude-stream.d.ts +22 -3
  38. package/dist/claude-stream.js +24 -8
  39. package/dist/copilot-stream.d.ts +7 -3
  40. package/dist/copilot-stream.js +3 -3
  41. package/dist/defs/aider.d.ts +8 -1
  42. package/dist/defs/aider.js +2 -2
  43. package/dist/defs/amp.d.ts +8 -1
  44. package/dist/defs/amp.js +2 -2
  45. package/dist/defs/amr.d.ts +22 -6
  46. package/dist/defs/amr.js +23 -23
  47. package/dist/defs/antigravity.d.ts +15 -4
  48. package/dist/defs/antigravity.js +9 -8
  49. package/dist/defs/auggie.d.ts +4 -1
  50. package/dist/defs/auggie.js +1 -1
  51. package/dist/defs/claude.d.ts +12 -2
  52. package/dist/defs/claude.js +5 -5
  53. package/dist/defs/cline.d.ts +4 -1
  54. package/dist/defs/cline.js +1 -1
  55. package/dist/defs/codebuddy.d.ts +8 -1
  56. package/dist/defs/codebuddy.js +2 -2
  57. package/dist/defs/codex.d.ts +18 -4
  58. package/dist/defs/codex.js +7 -7
  59. package/dist/defs/copilot.d.ts +8 -1
  60. package/dist/defs/copilot.js +2 -2
  61. package/dist/defs/crush.d.ts +8 -1
  62. package/dist/defs/crush.js +1 -1
  63. package/dist/defs/cursor-agent.d.ts +14 -3
  64. package/dist/defs/cursor-agent.js +5 -5
  65. package/dist/defs/deepseek.d.ts +8 -1
  66. package/dist/defs/deepseek.js +2 -2
  67. package/dist/defs/devin.d.ts +12 -2
  68. package/dist/defs/devin.js +3 -3
  69. package/dist/defs/droid.d.ts +8 -1
  70. package/dist/defs/droid.js +1 -1
  71. package/dist/defs/gemini.d.ts +8 -1
  72. package/dist/defs/gemini.js +1 -1
  73. package/dist/defs/goose.d.ts +4 -1
  74. package/dist/defs/goose.js +1 -1
  75. package/dist/defs/grok-build.d.ts +11 -2
  76. package/dist/defs/grok-build.js +3 -3
  77. package/dist/defs/hermes.d.ts +4 -1
  78. package/dist/defs/hermes.js +2 -2
  79. package/dist/defs/kilo.d.ts +4 -1
  80. package/dist/defs/kilo.js +2 -2
  81. package/dist/defs/kimi.d.ts +4 -1
  82. package/dist/defs/kimi.js +2 -2
  83. package/dist/defs/kiro.d.ts +4 -1
  84. package/dist/defs/kiro.js +2 -2
  85. package/dist/defs/mimo.d.ts +8 -1
  86. package/dist/defs/mimo.js +2 -2
  87. package/dist/defs/opencode.d.ts +9 -2
  88. package/dist/defs/opencode.js +2 -2
  89. package/dist/defs/pi.d.ts +12 -2
  90. package/dist/defs/pi.js +9 -9
  91. package/dist/defs/qoder.d.ts +8 -1
  92. package/dist/defs/qoder.js +2 -2
  93. package/dist/defs/qwen.d.ts +8 -1
  94. package/dist/defs/qwen.js +2 -2
  95. package/dist/defs/reasonix.d.ts +4 -1
  96. package/dist/defs/reasonix.js +3 -3
  97. package/dist/defs/shared.d.ts +4 -2
  98. package/dist/defs/shared.js +2 -2
  99. package/dist/defs/trae-cli.d.ts +12 -2
  100. package/dist/defs/trae-cli.js +3 -3
  101. package/dist/defs/vibe.d.ts +4 -1
  102. package/dist/defs/vibe.js +2 -2
  103. package/dist/detection.d.ts +19 -4
  104. package/dist/detection.js +33 -33
  105. package/dist/diagnostics.d.ts +14 -3
  106. package/dist/diagnostics.js +5 -5
  107. package/dist/env.d.ts +16 -4
  108. package/dist/env.js +7 -7
  109. package/dist/executables.d.ts +24 -8
  110. package/dist/executables.js +13 -13
  111. package/dist/index.d.ts +228 -4
  112. package/dist/index.js +230 -6
  113. package/dist/invocation.d.ts +6 -1
  114. package/dist/invocation.js +2 -2
  115. package/dist/json-event-stream.d.ts +7 -2
  116. package/dist/json-event-stream.js +3 -3
  117. package/dist/launch.d.ts +12 -2
  118. package/dist/launch.js +3 -3
  119. package/dist/log-file.d.ts +4 -1
  120. package/dist/log-file.js +2 -2
  121. package/dist/mcp.d.ts +6 -3
  122. package/dist/mcp.js +1 -1
  123. package/dist/metadata.d.ts +7 -3
  124. package/dist/metadata.js +1 -1
  125. package/dist/mmd-routes.d.ts +22 -6
  126. package/dist/mmd-routes.js +14 -14
  127. package/dist/model-catalog-cache.d.ts +50 -0
  128. package/dist/model-catalog-cache.js +61 -0
  129. package/dist/model-loading-state.d.ts +10 -0
  130. package/dist/model-loading-state.js +23 -0
  131. package/dist/model-registry.d.ts +28 -7
  132. package/dist/model-registry.js +11 -8
  133. package/dist/models.d.ts +31 -6
  134. package/dist/models.js +6 -6
  135. package/dist/opencode-log.d.ts +12 -4
  136. package/dist/opencode-log.js +8 -8
  137. package/dist/paths.d.ts +6 -2
  138. package/dist/paths.js +3 -3
  139. package/dist/pi-models.d.ts +3 -1
  140. package/dist/pi-models.js +2 -2
  141. package/dist/prompt-budget.d.ts +17 -4
  142. package/dist/prompt-budget.js +3 -3
  143. package/dist/prompt-file.d.ts +5 -1
  144. package/dist/prompt-file.js +1 -1
  145. package/dist/providers/aihubmix.d.ts +34 -9
  146. package/dist/providers/aihubmix.js +26 -26
  147. package/dist/providers/anthropic-messages.d.ts +77 -20
  148. package/dist/providers/anthropic-messages.js +53 -50
  149. package/dist/providers/azure-chat.d.ts +21 -7
  150. package/dist/providers/azure-chat.js +18 -16
  151. package/dist/providers/connection-guard.d.ts +45 -31
  152. package/dist/providers/connection-guard.js +40 -143
  153. package/dist/providers/connection-test.d.ts +1 -1
  154. package/dist/providers/connection-test.js +13 -30
  155. package/dist/providers/elevenlabs.d.ts +7 -2
  156. package/dist/providers/elevenlabs.js +4 -4
  157. package/dist/providers/google-messages.d.ts +44 -10
  158. package/dist/providers/google-messages.js +36 -34
  159. package/dist/providers/google-schema.d.ts +26 -0
  160. package/dist/providers/google-schema.js +253 -0
  161. package/dist/providers/google.d.ts +21 -6
  162. package/dist/providers/google.js +10 -10
  163. package/dist/providers/index.d.ts +3 -2
  164. package/dist/providers/index.js +3 -2
  165. package/dist/providers/model-catalog.d.ts +1 -1
  166. package/dist/providers/model-catalog.js +14 -27
  167. package/dist/providers/oauth-callback-server.d.ts +1 -1
  168. package/dist/providers/oauth-callback-server.js +3 -2
  169. package/dist/providers/oauth-credentials.d.ts +7 -1
  170. package/dist/providers/oauth-credentials.js +5 -5
  171. package/dist/providers/oauth-provider.d.ts +2 -2
  172. package/dist/providers/oauth-provider.js +20 -25
  173. package/dist/providers/oauth-tokens.d.ts +26 -6
  174. package/dist/providers/oauth-tokens.js +9 -9
  175. package/dist/providers/ollama-chat.d.ts +66 -31
  176. package/dist/providers/ollama-chat.js +64 -61
  177. package/dist/providers/openai-chat.d.ts +77 -22
  178. package/dist/providers/openai-chat.js +71 -67
  179. package/dist/providers/pkce.d.ts +33 -20
  180. package/dist/providers/pkce.js +75 -146
  181. package/dist/providers/sse-decode.d.ts +3 -1
  182. package/dist/providers/sse-decode.js +1 -1
  183. package/dist/providers/token-params.d.ts +16 -5
  184. package/dist/providers/token-params.js +6 -6
  185. package/dist/providers/tool-turn-types.d.ts +89 -0
  186. package/dist/providers/tool-turn-types.js +2 -0
  187. package/dist/providers/tool-turn.d.ts +10 -0
  188. package/dist/providers/tool-turn.js +252 -0
  189. package/dist/qoder-stream.d.ts +7 -3
  190. package/dist/qoder-stream.js +3 -3
  191. package/dist/registry.d.ts +6 -2
  192. package/dist/registry.js +4 -4
  193. package/dist/resolution.d.ts +5 -1
  194. package/dist/resolution.js +3 -3
  195. package/dist/role-marker-guard.d.ts +9 -5
  196. package/dist/role-marker-guard.js +5 -5
  197. package/dist/terminal-launch.d.ts +6 -1
  198. package/dist/terminal-launch.js +2 -2
  199. package/dist/types.d.ts +22 -6
  200. package/package.json +43 -4
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module providers/connection-guard
3
3
  *
4
- * Minimal, self-contained SSRF-guard + secret-redaction utilities needed by
4
+ * SSRF validation and pinned transport needed by
5
5
  * this package's own `model-catalog.ts`. Vendored from the small generic
6
6
  * subset of OD's `packages/contracts/src/api/connectionTest.ts` (the
7
7
  * `isLoopbackApiHost`/`isBlockedExternalApiHostname`/`validateBaseUrl` triad)
@@ -10,17 +10,25 @@
10
10
  * 2,600-line file, which is almost entirely OD's own agent-CLI
11
11
  * connection-test orchestration (proxy dispatchers, product-specific
12
12
  * executable-fallback copy and env-var names) and out of this task's
13
- * scope; see `source-map.md` for the exact origin details. These four
14
- * functions are pure security/text utilities with
13
+ * scope; see `archived provenance ledger` for the exact origin details. The shared classifiers now live in `@jini-ai/platform/net`, and redaction
14
+ * lives in core. Validation and transport here retain
15
15
  * no product coupling in the origin — reject requests to loopback-disguised
16
16
  * or RFC1918/link-local/CGNAT/metadata-service addresses, and strip bearer
17
17
  * tokens / API-key headers / `?key=` query values out of free-form text
18
- * before it is logged or surfaced to a caller.
18
+ * before it is logged or surfaced to a caller. Provider modules call core with
19
+ * explicit exactSecrets because upstream 401 bodies can echo configured credentials.
19
20
  *
20
- * Also exports `pinnedFetch` — a `node:https`/`node:http`-based POST that dials the exact address
21
+ * The provider redaction contract removes bearer tokens,
22
+ * `x-api-key`/`api-key`/`x-goog-api-key` headers, `?key=` query values,
23
+ * and exact secret strings supplied by the caller from free-form text.
24
+ * It is used for logging or surfacing upstream failures without leaking
25
+ * credentials embedded in them (some providers echo the key in a 401 body).
26
+ * Core owns this union now; provider model/request IDs remain readable.
27
+ *
28
+ * Exports `pinnedFetch` — a `node:https`/`node:http`-based POST that dials the exact address
21
29
  * `validateBaseUrlResolved` already validated, instead of leaving the transport to re-resolve DNS
22
30
  * independently when it connects. Added later than the four functions above, for the same
23
- * dependency-free package; see `pinnedFetch`'s own doc for why `fetch()` itself cannot do this.
31
+ * explicit network boundary; see `pinnedFetch`'s own doc for why `fetch()` itself cannot do this.
24
32
  *
25
33
  * ## Paired with `@jini-ai/ui`'s `utils/endpoint-policy.ts`
26
34
  *
@@ -31,7 +39,7 @@
31
39
  * imports by design and this package is Node-only (`validateBaseUrlResolved`
32
40
  * needs `node:dns`); see that file's header for the full reasoning.
33
41
  *
34
- * **If the block-list here changes, change it there too.** A divergence means
42
+ * **If the platform/net block-list changes, change the browser copy too.** A divergence means
35
43
  * the UI accepts an endpoint this guard then refuses — or, in the direction
36
44
  * that actually matters, the UI accepts one this guard would have refused.
37
45
  */
@@ -49,10 +57,6 @@ export interface BaseUrlValidationResult {
49
57
  */
50
58
  pinnedAddress?: DnsLookupAddress;
51
59
  }
52
- /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
53
- export declare function isLoopbackApiHost(hostname: string): boolean;
54
- /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
55
- export declare function isBlockedExternalApiHostname(hostname: string): boolean;
56
60
  /**
57
61
  * Synchronous base-URL check: scheme allow-list + literal-hostname block-list.
58
62
  * Does not resolve DNS — see {@link validateBaseUrlResolved}.
@@ -73,12 +77,16 @@ export declare function isBlockedExternalApiHostname(hostname: string): boolean;
73
77
  * is the only shape that would let the block-list inspect one string while the
74
78
  * network used another.
75
79
  */
76
- export declare function validateBaseUrl(baseUrl: string): BaseUrlValidationResult;
80
+ export declare function validateBaseUrl({ baseUrl }: {
81
+ baseUrl: string;
82
+ }): BaseUrlValidationResult;
77
83
  export type DnsLookupAddress = {
78
84
  address: string;
79
85
  family: number;
80
86
  };
81
- export type DnsLookupFn = (hostname: string) => Promise<DnsLookupAddress[]>;
87
+ export type DnsLookupFn = (requiredArgs: {
88
+ hostname: string;
89
+ }) => Promise<DnsLookupAddress[]>;
82
90
  /**
83
91
  * DNS-aware companion to {@link validateBaseUrl}. The synchronous check only
84
92
  * inspects the literal hostname string, so a public DNS name pointing at
@@ -110,15 +118,19 @@ export type DnsLookupFn = (hostname: string) => Promise<DnsLookupAddress[]>;
110
118
  * unvalidated address; `pinnedFetch` never follows one regardless (`node:https`/`node:http` don't,
111
119
  * unlike `fetch`'s default), but callers still pass `redirect: 'error'` for self-documentation.
112
120
  *
113
- * DNS lookup failures are not treated as a security signal — the caller is
114
- * going to surface a connection error from `fetch` anyway, and turning a
115
- * transient resolver hiccup into a rejection would just confuse callers. The
121
+ * DNS lookup failures and empty answers fail closed: an unpinned fallback
122
+ * would let the transport resolve the host again without checking that answer.
123
+ * Earlier behavior deferred resolver hiccups to the transport; that bypassed
124
+ * validation if its independent lookup succeeded, so it is no longer allowed. The
116
125
  * synchronous hostname check still rejects the obvious literal-IP cases
117
- * before DNS is ever consulted. `lookup` defaults to `node:dns`'s promise
118
- * `lookup(hostname, { all: true, family: 0 })`, injectable for tests and for
119
- * hosts that already own a resolver.
126
+ * before DNS is ever consulted. `lookup` is a required injected resolver;
127
+ * a Node host can wrap `node:dns`'s promise
128
+ * `lookup(hostname, { all: true, family: 0 })` in this port's object signature.
120
129
  */
121
- export declare function validateBaseUrlResolved(baseUrl: string, lookup: DnsLookupFn): Promise<BaseUrlValidationResult>;
130
+ export declare function validateBaseUrlResolved({ baseUrl, lookup }: {
131
+ baseUrl: string;
132
+ lookup: DnsLookupFn;
133
+ }): Promise<BaseUrlValidationResult>;
122
134
  /** Request options accepted by {@link pinnedFetch} — the subset of `fetch`'s `init` this package's provider adapters actually pass. `redirect` is accepted only as documentation of intent: `pinnedFetch` never follows a redirect regardless of this field, so a caller cannot opt back into `fetch`'s default follow-redirects behavior. */
123
135
  export interface PinnedFetchInit {
124
136
  readonly method: string;
@@ -144,7 +156,11 @@ export interface PinnedFetchResponse {
144
156
  * behavior, including the SSRF guard and DNS pinning, never changes unless a caller explicitly
145
157
  * overrides it.
146
158
  */
147
- export type PinnedFetch = (url: string, init: PinnedFetchInit, pinnedAddress: DnsLookupAddress | undefined) => Promise<PinnedFetchResponse>;
159
+ export type PinnedFetch = (requiredArgs: {
160
+ url: string;
161
+ init: PinnedFetchInit;
162
+ pinnedAddress: DnsLookupAddress | undefined;
163
+ }) => Promise<PinnedFetchResponse>;
148
164
  /**
149
165
  * `fetch()`-shaped POST that dials `pinnedAddress` directly instead of letting the transport
150
166
  * re-resolve DNS when it connects — see {@link validateBaseUrlResolved}'s doc for the TOCTOU this
@@ -173,15 +189,13 @@ export type PinnedFetch = (url: string, init: PinnedFetchInit, pinnedAddress: Dn
173
189
  * (`lookup` option simply omitted). That is intentional, not a gap: an IP literal has nothing left
174
190
  * to re-resolve, and loopback literals are the documented carve-out, outside this threat model.
175
191
  */
176
- export declare function pinnedFetch(url: string, init: PinnedFetchInit, pinnedAddress: DnsLookupAddress | undefined): Promise<PinnedFetchResponse>;
177
- /**
178
- * Redacts bearer tokens, `x-api-key`/`api-key`/`x-goog-api-key` headers,
179
- * `?key=` query values, and any exact secret strings supplied via
180
- * `exactSecrets` out of free-form text — for logging or surfacing an
181
- * upstream error message to a caller without leaking credentials embedded in
182
- * it (some providers echo the key back in a 401 body).
183
- */
184
- export declare function redactSecrets(text: string, exactSecrets?: ReadonlyArray<string | undefined | null>): string;
192
+ export declare function pinnedFetch({ url, init, pinnedAddress }: {
193
+ url: string;
194
+ init: PinnedFetchInit;
195
+ pinnedAddress: DnsLookupAddress | undefined;
196
+ }): Promise<PinnedFetchResponse>;
185
197
  /** Default DNS lookup for {@link validateBaseUrlResolved} — `node:dns/promises`' `lookup(hostname, { all: true, family: 0 })`. */
186
- export declare function defaultDnsLookup(hostname: string): Promise<DnsLookupAddress[]>;
198
+ export declare function defaultDnsLookup({ hostname }: {
199
+ hostname: string;
200
+ }): Promise<DnsLookupAddress[]>;
187
201
  //# sourceMappingURL=connection-guard.d.ts.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module providers/connection-guard
3
3
  *
4
- * Minimal, self-contained SSRF-guard + secret-redaction utilities needed by
4
+ * SSRF validation and pinned transport needed by
5
5
  * this package's own `model-catalog.ts`. Vendored from the small generic
6
6
  * subset of OD's `packages/contracts/src/api/connectionTest.ts` (the
7
7
  * `isLoopbackApiHost`/`isBlockedExternalApiHostname`/`validateBaseUrl` triad)
@@ -10,17 +10,25 @@
10
10
  * 2,600-line file, which is almost entirely OD's own agent-CLI
11
11
  * connection-test orchestration (proxy dispatchers, product-specific
12
12
  * executable-fallback copy and env-var names) and out of this task's
13
- * scope; see `source-map.md` for the exact origin details. These four
14
- * functions are pure security/text utilities with
13
+ * scope; see `archived provenance ledger` for the exact origin details. The shared classifiers now live in `@jini-ai/platform/net`, and redaction
14
+ * lives in core. Validation and transport here retain
15
15
  * no product coupling in the origin — reject requests to loopback-disguised
16
16
  * or RFC1918/link-local/CGNAT/metadata-service addresses, and strip bearer
17
17
  * tokens / API-key headers / `?key=` query values out of free-form text
18
- * before it is logged or surfaced to a caller.
18
+ * before it is logged or surfaced to a caller. Provider modules call core with
19
+ * explicit exactSecrets because upstream 401 bodies can echo configured credentials.
19
20
  *
20
- * Also exports `pinnedFetch` — a `node:https`/`node:http`-based POST that dials the exact address
21
+ * The provider redaction contract removes bearer tokens,
22
+ * `x-api-key`/`api-key`/`x-goog-api-key` headers, `?key=` query values,
23
+ * and exact secret strings supplied by the caller from free-form text.
24
+ * It is used for logging or surfacing upstream failures without leaking
25
+ * credentials embedded in them (some providers echo the key in a 401 body).
26
+ * Core owns this union now; provider model/request IDs remain readable.
27
+ *
28
+ * Exports `pinnedFetch` — a `node:https`/`node:http`-based POST that dials the exact address
21
29
  * `validateBaseUrlResolved` already validated, instead of leaving the transport to re-resolve DNS
22
30
  * independently when it connects. Added later than the four functions above, for the same
23
- * dependency-free package; see `pinnedFetch`'s own doc for why `fetch()` itself cannot do this.
31
+ * explicit network boundary; see `pinnedFetch`'s own doc for why `fetch()` itself cannot do this.
24
32
  *
25
33
  * ## Paired with `@jini-ai/ui`'s `utils/endpoint-policy.ts`
26
34
  *
@@ -31,99 +39,11 @@
31
39
  * imports by design and this package is Node-only (`validateBaseUrlResolved`
32
40
  * needs `node:dns`); see that file's header for the full reasoning.
33
41
  *
34
- * **If the block-list here changes, change it there too.** A divergence means
42
+ * **If the platform/net block-list changes, change the browser copy too.** A divergence means
35
43
  * the UI accepts an endpoint this guard then refuses — or, in the direction
36
44
  * that actually matters, the UI accepts one this guard would have refused.
37
45
  */
38
- function normalizeBracketedIpv6(hostname) {
39
- const stripped = hostname.startsWith('[') && hostname.endsWith(']')
40
- ? hostname.slice(1, -1)
41
- : hostname;
42
- // FQDN trailing-dot form (RFC 1034) resolves identically to the dotless
43
- // form, so `localhost.` must normalize to `localhost` before the equality
44
- // check below — and `0.0.0.0.`, `10.0.0.1.`, etc. must normalize before
45
- // isBlockedIpv4 parses them. Strips one or more trailing dots.
46
- return stripped.toLowerCase().replace(/\.+$/, '');
47
- }
48
- function parseIpv4(hostname) {
49
- const parts = hostname.split('.');
50
- if (parts.length !== 4)
51
- return null;
52
- const parsed = parts.map((part) => {
53
- if (!/^\d{1,3}$/.test(part))
54
- return null;
55
- const value = Number(part);
56
- return value >= 0 && value <= 255 ? value : null;
57
- });
58
- if (parsed.some((part) => part === null))
59
- return null;
60
- return parsed;
61
- }
62
- function isLoopbackIpv4(hostname) {
63
- const parts = parseIpv4(hostname);
64
- return Boolean(parts && parts[0] === 127);
65
- }
66
- function isBlockedIpv4(hostname) {
67
- const parts = parseIpv4(hostname);
68
- if (!parts)
69
- return false;
70
- const [a, b] = parts;
71
- return (a === 0 ||
72
- (a === 100 && b >= 64 && b <= 127) ||
73
- (a === 169 && b === 254) ||
74
- a === 10 ||
75
- (a === 192 && b === 168) ||
76
- (a === 172 && b >= 16 && b <= 31) ||
77
- a >= 224);
78
- }
79
- function ipv4MappedToDotted(hostname) {
80
- const host = normalizeBracketedIpv6(hostname);
81
- const mapped = /^::ffff:(.+)$/i.exec(host)?.[1];
82
- if (!mapped)
83
- return null;
84
- if (parseIpv4(mapped.toLowerCase()))
85
- return mapped.toLowerCase();
86
- const hexParts = mapped.split(':');
87
- if (hexParts.length !== 2 ||
88
- !hexParts.every((part) => /^[0-9a-f]{1,4}$/i.test(part))) {
89
- return null;
90
- }
91
- // Non-null assertions, not a runtime guard: the length/regex checks above
92
- // already guarantee exactly two non-empty hex segments here.
93
- const hi = hexParts[0];
94
- const lo = hexParts[1];
95
- const value = (Number.parseInt(hi, 16) << 16) | Number.parseInt(lo, 16);
96
- return [
97
- (value >>> 24) & 255,
98
- (value >>> 16) & 255,
99
- (value >>> 8) & 255,
100
- value & 255,
101
- ].join('.');
102
- }
103
- /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
104
- export function isLoopbackApiHost(hostname) {
105
- const host = normalizeBracketedIpv6(hostname);
106
- if (host === 'localhost' || host === '::1')
107
- return true;
108
- if (isLoopbackIpv4(host))
109
- return true;
110
- const mapped = ipv4MappedToDotted(host);
111
- return Boolean(mapped && isLoopbackIpv4(mapped));
112
- }
113
- /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
114
- export function isBlockedExternalApiHostname(hostname) {
115
- const host = normalizeBracketedIpv6(hostname);
116
- if (host === '::')
117
- return true;
118
- if (isBlockedIpv4(host))
119
- return true;
120
- if (/^f[cd][0-9a-f]{2}:/i.test(host))
121
- return true;
122
- if (/^fe[89ab][0-9a-f]:/i.test(host))
123
- return true;
124
- const mapped = ipv4MappedToDotted(host);
125
- return Boolean(mapped && isBlockedIpv4(mapped));
126
- }
46
+ import { isLoopbackApiHost, isBlockedExternalApiHostname } from '@jini-ai/platform/net';
127
47
  /**
128
48
  * Synchronous base-URL check: scheme allow-list + literal-hostname block-list.
129
49
  * Does not resolve DNS — see {@link validateBaseUrlResolved}.
@@ -144,7 +64,7 @@ export function isBlockedExternalApiHostname(hostname) {
144
64
  * is the only shape that would let the block-list inspect one string while the
145
65
  * network used another.
146
66
  */
147
- export function validateBaseUrl(baseUrl) {
67
+ export function validateBaseUrl({ baseUrl }) {
148
68
  let parsed;
149
69
  try {
150
70
  // Trim BEFORE stripping trailing slashes: in `"https://x/ "` the trailing
@@ -158,7 +78,7 @@ export function validateBaseUrl(baseUrl) {
158
78
  return { error: 'Only http/https allowed' };
159
79
  }
160
80
  const hostname = parsed.hostname.toLowerCase();
161
- if (!isLoopbackApiHost(hostname) && isBlockedExternalApiHostname(hostname)) {
81
+ if (!isLoopbackApiHost({ hostname: hostname }) && isBlockedExternalApiHostname({ hostname: hostname })) {
162
82
  return { error: 'Internal IPs blocked', forbidden: true };
163
83
  }
164
84
  return { parsed };
@@ -202,46 +122,47 @@ function looksLikeIpLiteral(hostname) {
202
122
  * unvalidated address; `pinnedFetch` never follows one regardless (`node:https`/`node:http` don't,
203
123
  * unlike `fetch`'s default), but callers still pass `redirect: 'error'` for self-documentation.
204
124
  *
205
- * DNS lookup failures are not treated as a security signal — the caller is
206
- * going to surface a connection error from `fetch` anyway, and turning a
207
- * transient resolver hiccup into a rejection would just confuse callers. The
125
+ * DNS lookup failures and empty answers fail closed: an unpinned fallback
126
+ * would let the transport resolve the host again without checking that answer.
127
+ * Earlier behavior deferred resolver hiccups to the transport; that bypassed
128
+ * validation if its independent lookup succeeded, so it is no longer allowed. The
208
129
  * synchronous hostname check still rejects the obvious literal-IP cases
209
- * before DNS is ever consulted. `lookup` defaults to `node:dns`'s promise
210
- * `lookup(hostname, { all: true, family: 0 })`, injectable for tests and for
211
- * hosts that already own a resolver.
130
+ * before DNS is ever consulted. `lookup` is a required injected resolver;
131
+ * a Node host can wrap `node:dns`'s promise
132
+ * `lookup(hostname, { all: true, family: 0 })` in this port's object signature.
212
133
  */
213
- export async function validateBaseUrlResolved(baseUrl, lookup) {
214
- const sync = validateBaseUrl(baseUrl);
134
+ export async function validateBaseUrlResolved({ baseUrl, lookup }) {
135
+ const sync = validateBaseUrl({ baseUrl: baseUrl });
215
136
  if (sync.error || !sync.parsed)
216
137
  return sync;
217
138
  const hostname = sync.parsed.hostname.toLowerCase();
218
- if (isLoopbackApiHost(hostname))
139
+ if (isLoopbackApiHost({ hostname: hostname }))
219
140
  return sync;
220
141
  if (looksLikeIpLiteral(hostname))
221
142
  return sync;
222
143
  let addresses;
223
144
  try {
224
- addresses = await lookup(hostname);
145
+ addresses = await lookup({ hostname });
225
146
  }
226
147
  catch {
227
- return sync;
148
+ return { error: 'DNS lookup failed', forbidden: true };
228
149
  }
150
+ if (addresses.length === 0)
151
+ return { error: 'DNS lookup returned no addresses', forbidden: true };
229
152
  for (const addr of addresses) {
230
153
  const ip = String(addr.address).toLowerCase();
231
- if (isLoopbackApiHost(ip))
154
+ if (isLoopbackApiHost({ hostname: ip }))
232
155
  continue;
233
- if (isBlockedExternalApiHostname(ip)) {
156
+ if (isBlockedExternalApiHostname({ hostname: ip })) {
234
157
  return { error: 'Internal IPs blocked', forbidden: true };
235
158
  }
236
159
  }
237
160
  // Pin to the FIRST resolved address — the loop above already proved every address in this list
238
161
  // cleared the block-list, so this is "the address the guard actually approved", singular, ready
239
- // to hand to `pinnedFetch`. An empty `addresses` array (a `lookup` that resolves to nothing
240
- // without throwing) leaves `pinnedAddress` unset; a real `dns.lookup` throws ENOTFOUND rather
241
- // than resolving to `[]`, so this is not a realistic gap, just a graceful fallback to the
242
- // pre-pinning pass-through behavior.
243
- const pinnedAddress = addresses[0];
244
- return pinnedAddress ? { ...sync, pinnedAddress } : sync;
162
+ // to hand to `pinnedFetch`. A real `dns.lookup` throws ENOTFOUND rather than resolving to `[]`;
163
+ // injected resolvers can do either, and both fail closed above instead of reviving the earlier
164
+ // pre-pinning pass-through behavior. The nonempty check guarantees the first address exists.
165
+ return { ...sync, pinnedAddress: addresses[0] };
245
166
  }
246
167
  /**
247
168
  * Idle-socket safety net, applied to every `pinnedFetch` request regardless of `init.signal`.
@@ -280,7 +201,7 @@ const PINNED_FETCH_IDLE_TIMEOUT_MS = 300_000;
280
201
  * (`lookup` option simply omitted). That is intentional, not a gap: an IP literal has nothing left
281
202
  * to re-resolve, and loopback literals are the documented carve-out, outside this threat model.
282
203
  */
283
- export async function pinnedFetch(url, init, pinnedAddress) {
204
+ export async function pinnedFetch({ url, init, pinnedAddress }) {
284
205
  const parsed = new URL(url);
285
206
  const isHttps = parsed.protocol === 'https:';
286
207
  const { request: httpsRequest } = await import('node:https');
@@ -380,32 +301,8 @@ export async function pinnedFetch(url, init, pinnedAddress) {
380
301
  req.end(init.body);
381
302
  });
382
303
  }
383
- function escapeRegExp(value) {
384
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
385
- }
386
- /**
387
- * Redacts bearer tokens, `x-api-key`/`api-key`/`x-goog-api-key` headers,
388
- * `?key=` query values, and any exact secret strings supplied via
389
- * `exactSecrets` out of free-form text — for logging or surfacing an
390
- * upstream error message to a caller without leaking credentials embedded in
391
- * it (some providers echo the key back in a 401 body).
392
- */
393
- export function redactSecrets(text, exactSecrets = []) {
394
- if (typeof text !== 'string' || text.length === 0)
395
- return '';
396
- let redacted = text
397
- .replace(/Bearer\s+[A-Za-z0-9_\-.+/=]+/gi, 'Bearer [REDACTED]')
398
- .replace(/(x-api-key|api-key|x-goog-api-key)\s*[:=]\s*[^\s,;"']+/gi, '$1: [REDACTED]')
399
- .replace(/([?&]key=)[^&\s]+/gi, '$1[REDACTED]');
400
- for (const secret of exactSecrets) {
401
- if (typeof secret !== 'string' || secret.length === 0)
402
- continue;
403
- redacted = redacted.replace(new RegExp(escapeRegExp(secret), 'g'), '[REDACTED]');
404
- }
405
- return redacted;
406
- }
407
304
  /** Default DNS lookup for {@link validateBaseUrlResolved} — `node:dns/promises`' `lookup(hostname, { all: true, family: 0 })`. */
408
- export async function defaultDnsLookup(hostname) {
305
+ export async function defaultDnsLookup({ hostname }) {
409
306
  const { promises: dnsPromises } = await import('node:dns');
410
307
  const result = await dnsPromises.lookup(hostname, { all: true, family: 0 });
411
308
  return result.map(({ address, family }) => ({ address, family }));
@@ -58,5 +58,5 @@ export interface ConnectionTestResponse {
58
58
  * @complexity O(1) — one bounded-size HTTP request with a hard timeout.
59
59
  * @overallScore 100
60
60
  */
61
- export declare function testProviderConnection(input: ProviderConnectionTestInput): Promise<ConnectionTestResponse>;
61
+ export declare function testProviderConnection(requiredArgs: Pick<ProviderConnectionTestInput, "protocol" | "baseUrl" | "apiKey" | "model">, optionalArgs?: Omit<ProviderConnectionTestInput, "protocol" | "baseUrl" | "apiKey" | "model">): Promise<ConnectionTestResponse>;
62
62
  //# sourceMappingURL=connection-test.d.ts.map
@@ -1,23 +1,5 @@
1
- /**
2
- * @module providers/connection-test
3
- *
4
- * BYOK connection smoke test — sends a tiny "Reply with only: ok" completion
5
- * request to the configured provider endpoint and classifies the outcome, so
6
- * a settings UI can tell the operator "this endpoint works" without spending
7
- * a real chat turn to find out. Ported from OD's `apps/daemon/src/connectionTest.ts`
8
- * (`testProviderConnection` + `buildProviderCall` + `inspectProviderCompletion`),
9
- * narrowed to the four protocols `@jini-ai/ui`'s execution-tab canary exposes
10
- * (`anthropic` / `openai` / `azure` / `google`) — the origin also covers
11
- * `ollama` / `senseaudio` / `aihubmix` / `bedrock`, which stay out of scope
12
- * here the same way `model-catalog.ts` keeps Bedrock to a static seed and
13
- * Azure to an explicit "unsupported" response for model discovery.
14
- *
15
- * Shares this package's own vendored SSRF guard / secret redaction
16
- * (`connection-guard.ts`) and vendored connectivity types (`types.ts`) —
17
- * no OD workspace-package imports, matching every other file in this
18
- * directory.
19
- */
20
- import { defaultDnsLookup, redactSecrets, validateBaseUrlResolved } from './connection-guard.js';
1
+ import { redactSecrets } from '@jini-ai/core';
2
+ import { defaultDnsLookup, validateBaseUrlResolved } from './connection-guard.js';
21
3
  import { googleGenerateContentUrl } from './google.js';
22
4
  import { buildOpenAIChatTokenParam, buildLegacyMaxTokensParam, buildMaxCompletionTokensParam } from './token-params.js';
23
5
  const SMOKE_PROMPT = 'Reply with only: ok';
@@ -185,7 +167,7 @@ function buildProviderCall(input) {
185
167
  },
186
168
  body: {
187
169
  model,
188
- ...buildOpenAIChatTokenParam(model, CONNECTION_TEST_MAX_TOKENS),
170
+ ...buildOpenAIChatTokenParam({ model: model, maxTokens: CONNECTION_TEST_MAX_TOKENS }),
189
171
  messages: [{ role: 'user', content: SMOKE_PROMPT }],
190
172
  stream: false,
191
173
  },
@@ -206,7 +188,7 @@ function buildProviderCall(input) {
206
188
  headers: { 'content-type': 'application/json', 'api-key': apiKey },
207
189
  body: {
208
190
  ...(usesVersionedOpenAIPath ? { model } : {}),
209
- ...buildLegacyMaxTokensParam(CONNECTION_TEST_MAX_TOKENS),
191
+ ...buildLegacyMaxTokensParam({ maxTokens: CONNECTION_TEST_MAX_TOKENS }),
210
192
  messages: [{ role: 'user', content: SMOKE_PROMPT }],
211
193
  stream: false,
212
194
  },
@@ -216,7 +198,7 @@ function buildProviderCall(input) {
216
198
  case 'google': {
217
199
  const effectiveBaseUrl = baseUrl.trim() || 'https://generativelanguage.googleapis.com';
218
200
  return {
219
- url: googleGenerateContentUrl(effectiveBaseUrl, model),
201
+ url: googleGenerateContentUrl({ baseUrl: effectiveBaseUrl, model: model }),
220
202
  headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
221
203
  body: {
222
204
  contents: [{ role: 'user', parts: [{ text: SMOKE_PROMPT }] }],
@@ -247,7 +229,7 @@ function buildProviderCall(input) {
247
229
  // rejected `max_tokens` param; this smoke test keeps the simpler single-shot
248
230
  // legacy param (a connection test failing on a param-name mismatch on a
249
231
  // reasoning-tier Azure deployment is a known, narrow gap — see this
250
- // package's `source-map.md`) rather than porting OD's full retry-on-400 loop.
232
+ // package's `archived provenance ledger`) rather than porting OD's full retry-on-400 loop.
251
233
  void buildMaxCompletionTokensParam;
252
234
  /**
253
235
  * Probes a BYOK provider endpoint with the supplied credentials by sending a
@@ -262,7 +244,8 @@ void buildMaxCompletionTokensParam;
262
244
  * @complexity O(1) — one bounded-size HTTP request with a hard timeout.
263
245
  * @overallScore 100
264
246
  */
265
- export async function testProviderConnection(input) {
247
+ export async function testProviderConnection(requiredArgs, optionalArgs = {}) {
248
+ const input = { ...optionalArgs, ...requiredArgs };
266
249
  const start = Date.now();
267
250
  const model = input.model.trim();
268
251
  if (!isSupportedProtocol(input.protocol)) {
@@ -290,7 +273,7 @@ export async function testProviderConnection(input) {
290
273
  detail: 'No API key saved. Save one to test the connection.',
291
274
  };
292
275
  }
293
- const validated = await validateBaseUrlResolved(input.baseUrl, input.dnsLookup ?? defaultDnsLookup);
276
+ const validated = await validateBaseUrlResolved({ baseUrl: input.baseUrl, lookup: input.dnsLookup ?? defaultDnsLookup });
294
277
  if (validated.error || !validated.parsed) {
295
278
  return {
296
279
  ok: false,
@@ -310,7 +293,7 @@ export async function testProviderConnection(input) {
310
293
  kind: 'unknown',
311
294
  latencyMs: Date.now() - start,
312
295
  model,
313
- detail: redactSecrets(err instanceof Error ? err.message : String(err), [input.apiKey]),
296
+ detail: redactSecrets({ input: err instanceof Error ? err.message : String(err) }, { exactSecrets: [input.apiKey] }),
314
297
  };
315
298
  }
316
299
  const controller = new AbortController();
@@ -342,7 +325,7 @@ export async function testProviderConnection(input) {
342
325
  // paths below, both of which treat an empty `data` as "no usable text".
343
326
  }
344
327
  if (!response.ok) {
345
- const redactedDetail = redactSecrets(extractProviderErrorDetail(data, rawText).slice(0, 240), [input.apiKey]);
328
+ const redactedDetail = redactSecrets({ input: extractProviderErrorDetail(data, rawText).slice(0, 240) }, { exactSecrets: [input.apiKey] });
346
329
  result = {
347
330
  ok: false,
348
331
  kind: statusToKind(response.status, redactedDetail),
@@ -366,7 +349,7 @@ export async function testProviderConnection(input) {
366
349
  // header puts the operator's key in `detail` verbatim. Two independent
367
350
  // audits reproduced that, each from a different call boundary; the
368
351
  // redaction was simply missing on this one branch.
369
- const sample = truncateSample(redactSecrets(text, [input.apiKey]));
352
+ const sample = truncateSample(redactSecrets({ input: text }, { exactSecrets: [input.apiKey] }));
370
353
  // Naming the stop reason turns an unactionable "it returned nothing" into a diagnosis:
371
354
  // `MAX_TOKENS` means the smoke budget was too small (usually a thinking model), a safety or
372
355
  // content-filter reason means the probe was blocked, not that the endpoint is broken.
@@ -392,7 +375,7 @@ export async function testProviderConnection(input) {
392
375
  kind: networkErrorToKind(err),
393
376
  latencyMs: Date.now() - start,
394
377
  model,
395
- detail: redactSecrets(err instanceof Error ? err.message : String(err), [input.apiKey]),
378
+ detail: redactSecrets({ input: err instanceof Error ? err.message : String(err) }, { exactSecrets: [input.apiKey] }),
396
379
  };
397
380
  }
398
381
  finally {
@@ -10,7 +10,9 @@ export interface ElevenLabsCredentials {
10
10
  baseUrl?: string;
11
11
  }
12
12
  /** Host-supplied credential lookup — replaces OD's product-owned settings-store reader. */
13
- export type ElevenLabsCredentialResolver = (workspaceKey: string) => Promise<ElevenLabsCredentials>;
13
+ export type ElevenLabsCredentialResolver = (requiredArgs: {
14
+ workspaceKey: string;
15
+ }) => Promise<ElevenLabsCredentials>;
14
16
  /** Thrown when {@link ElevenLabsCredentialResolver} resolves no API key. */
15
17
  export declare class ElevenLabsCredentialMissingError extends Error {
16
18
  constructor();
@@ -21,7 +23,10 @@ export declare class ElevenLabsCredentialMissingError extends Error {
21
23
  * result for {@link ELEVENLABS_VOICE_CACHE_TTL_MS}. Throws
22
24
  * {@link ElevenLabsCredentialMissingError} when no API key is configured.
23
25
  */
24
- export declare function listElevenLabsVoiceOptions(workspaceKey: string, resolveCredentials: ElevenLabsCredentialResolver, options?: {
26
+ export declare function listElevenLabsVoiceOptions({ workspaceKey, resolveCredentials }: {
27
+ workspaceKey: string;
28
+ resolveCredentials: ElevenLabsCredentialResolver;
29
+ }, options?: {
25
30
  limit?: number;
26
31
  requestInit?: Pick<RequestInit, 'dispatcher'>;
27
32
  }): Promise<ElevenLabsVoiceOption[]>;
@@ -9,14 +9,14 @@
9
9
  * De-branded: the origin resolved the API key/base URL via OD's own
10
10
  * `resolveProviderConfig` (`../media/config.js`, a product-owned
11
11
  * settings-store reader entirely out of this package's scope) and threw a
12
- * hardcoded product-prefixed env-var hint (see `source-map.md` for the
12
+ * hardcoded product-prefixed env-var hint (see `archived provenance ledger` for the
13
13
  * exact original string). Both are replaced by an
14
14
  * injected {@link ElevenLabsCredentialResolver} port — a host supplies its
15
15
  * own settings-store/env lookup; this package only knows how to call the
16
16
  * ElevenLabs wire API once it has a key.
17
17
  */
18
18
  import { createHash } from 'node:crypto';
19
- import { FETCH_TIMEOUT_MS, fetchWithTimeout } from '@jini-ai/platform';
19
+ import { FETCH_TIMEOUT_MS, fetchWithTimeout } from '@jini-ai/platform/fetch-with-timeout';
20
20
  const ELEVENLABS_DEFAULT_BASE_URL = 'https://api.elevenlabs.io';
21
21
  const ELEVENLABS_DEFAULT_VOICE_LIMIT = 100;
22
22
  const ELEVENLABS_MAX_VOICE_LIMIT = 100;
@@ -93,8 +93,8 @@ function cloneVoiceOptions(voices) {
93
93
  * result for {@link ELEVENLABS_VOICE_CACHE_TTL_MS}. Throws
94
94
  * {@link ElevenLabsCredentialMissingError} when no API key is configured.
95
95
  */
96
- export async function listElevenLabsVoiceOptions(workspaceKey, resolveCredentials, options = {}) {
97
- const credentials = await resolveCredentials(workspaceKey);
96
+ export async function listElevenLabsVoiceOptions({ workspaceKey, resolveCredentials }, options = {}) {
97
+ const credentials = await resolveCredentials({ workspaceKey });
98
98
  if (!credentials.apiKey) {
99
99
  throw new ElevenLabsCredentialMissingError();
100
100
  }