nexus-ai-pro 2.2.0 → 2.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 (298) hide show
  1. package/API_STABILITY.md +225 -0
  2. package/CHANGELOG.md +325 -1
  3. package/README.md +20 -6
  4. package/SECURITY.md +140 -0
  5. package/dist/adapter-kit/contracts.d.ts +87 -0
  6. package/dist/adapter-kit/contracts.js +237 -0
  7. package/dist/adapter-kit/errors.d.ts +39 -0
  8. package/dist/adapter-kit/errors.js +52 -0
  9. package/dist/adapter-kit/index.d.ts +144 -0
  10. package/dist/adapter-kit/index.js +178 -0
  11. package/dist/adapter-kit/semver.d.ts +12 -0
  12. package/dist/adapter-kit/semver.js +55 -0
  13. package/dist/agent/capabilities.d.ts +39 -0
  14. package/dist/agent/capabilities.js +36 -0
  15. package/dist/agent/create-agent.d.ts +61 -11
  16. package/dist/agent/create-agent.js +136 -22
  17. package/dist/agent/index.d.ts +3 -2
  18. package/dist/agent/index.js +1 -1
  19. package/dist/agent/middleware/approval.d.ts +26 -0
  20. package/dist/agent/middleware/approval.js +23 -0
  21. package/dist/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
  22. package/dist/agent/{middleware.js → middleware/basic.js} +3 -5
  23. package/dist/agent/middleware/context-editor.d.ts +52 -0
  24. package/dist/agent/middleware/context-editor.js +66 -0
  25. package/dist/agent/middleware/filesystem.d.ts +44 -0
  26. package/dist/agent/middleware/filesystem.js +83 -0
  27. package/dist/agent/middleware/index.d.ts +19 -0
  28. package/dist/agent/middleware/index.js +9 -0
  29. package/dist/agent/middleware/limits.d.ts +42 -0
  30. package/dist/agent/middleware/limits.js +52 -0
  31. package/dist/agent/middleware/models.d.ts +48 -0
  32. package/dist/agent/middleware/models.js +44 -0
  33. package/dist/agent/middleware/pii.d.ts +67 -0
  34. package/dist/agent/middleware/pii.js +142 -0
  35. package/dist/agent/middleware/retry.d.ts +66 -0
  36. package/dist/agent/middleware/retry.js +59 -0
  37. package/dist/agent/middleware/shared.d.ts +41 -0
  38. package/dist/agent/middleware/shared.js +73 -0
  39. package/dist/agent/middleware/tool-selector.d.ts +64 -0
  40. package/dist/agent/middleware/tool-selector.js +106 -0
  41. package/dist/agent/permissions.d.ts +113 -0
  42. package/dist/agent/permissions.js +180 -0
  43. package/dist/agent/sandbox.d.ts +151 -0
  44. package/dist/agent/sandbox.js +257 -0
  45. package/dist/agent/tool.d.ts +8 -3
  46. package/dist/agent/tool.js +14 -3
  47. package/dist/core/secure-stream.js +4 -3
  48. package/dist/deep-agent/index.d.ts +109 -0
  49. package/dist/deep-agent/index.js +216 -0
  50. package/dist/embeddings/adapters.js +2 -1
  51. package/dist/evals/judge.js +2 -5
  52. package/dist/evaluate/cache.d.ts +0 -14
  53. package/dist/evaluate/cache.js +0 -30
  54. package/dist/evaluate/file-cache.d.ts +15 -0
  55. package/dist/evaluate/file-cache.js +30 -0
  56. package/dist/evaluate/index.d.ts +2 -1
  57. package/dist/evaluate/index.js +2 -1
  58. package/dist/evaluate/review.js +2 -2
  59. package/dist/evaluate/run.js +2 -2
  60. package/dist/evaluate/version.js +3 -3
  61. package/dist/graph/functional.d.ts +9 -0
  62. package/dist/graph/functional.js +20 -3
  63. package/dist/graph/graph.js +27 -6
  64. package/dist/graph/lint.d.ts +4 -1
  65. package/dist/graph/lint.js +35 -0
  66. package/dist/graph/node-cache.js +3 -3
  67. package/dist/hallucination/rag.d.ts +4 -1
  68. package/dist/hallucination/rag.js +6 -1
  69. package/dist/images/asset-stores.js +2 -3
  70. package/dist/images/asset-support.js +2 -2
  71. package/dist/images/assets.js +1 -2
  72. package/dist/images/evals.js +4 -4
  73. package/dist/insights/proposals.js +1 -2
  74. package/dist/insights/regressions.d.ts +26 -2
  75. package/dist/insights/regressions.js +129 -3
  76. package/dist/loaders/git.js +8 -1
  77. package/dist/loaders/web.js +7 -1
  78. package/dist/mcp/client.d.ts +14 -0
  79. package/dist/mcp/client.js +59 -1
  80. package/dist/mcp/registry.d.ts +6 -0
  81. package/dist/mcp/registry.js +7 -0
  82. package/dist/postgres/fulltext.d.ts +67 -0
  83. package/dist/postgres/fulltext.js +91 -0
  84. package/dist/prompts/file.js +28 -11
  85. package/dist/protocols/a2a.d.ts +235 -0
  86. package/dist/protocols/a2a.js +386 -0
  87. package/dist/protocols/acp.d.ts +87 -0
  88. package/dist/protocols/acp.js +322 -0
  89. package/dist/protocols/ag-ui.d.ts +120 -0
  90. package/dist/protocols/ag-ui.js +180 -0
  91. package/dist/protocols/shared.d.ts +80 -0
  92. package/dist/protocols/shared.js +49 -0
  93. package/dist/providers/anthropic.js +1 -0
  94. package/dist/providers/cohere.js +2 -2
  95. package/dist/providers/google.js +2 -11
  96. package/dist/providers/openai.js +1 -0
  97. package/dist/rag/elasticsearch.d.ts +71 -0
  98. package/dist/rag/elasticsearch.js +130 -0
  99. package/dist/rag/file-ingestion.js +15 -9
  100. package/dist/rag/pipeline.d.ts +140 -0
  101. package/dist/rag/pipeline.js +187 -0
  102. package/dist/rag/rerankers.d.ts +92 -0
  103. package/dist/rag/rerankers.js +97 -0
  104. package/dist/rag/retrievers.d.ts +12 -1
  105. package/dist/runtime/detect.d.ts +17 -0
  106. package/dist/runtime/detect.js +16 -0
  107. package/dist/runtime/index.d.ts +19 -0
  108. package/dist/runtime/index.js +7 -0
  109. package/dist/security/injection-detector.d.ts +2 -0
  110. package/dist/security/injection-detector.js +7 -1
  111. package/dist/security/input-guard.js +5 -2
  112. package/dist/security/output-guard.js +8 -4
  113. package/dist/security/pii-detector.js +22 -6
  114. package/dist/security/prompt-hardening.js +6 -2
  115. package/dist/security/semantic-injection-classifier.d.ts +2 -1
  116. package/dist/security/semantic-injection-classifier.js +31 -35
  117. package/dist/security/upload-scanner.d.ts +8 -0
  118. package/dist/security/upload-scanner.js +51 -9
  119. package/dist/server/assistant.d.ts +5 -1
  120. package/dist/server/assistant.js +4 -2
  121. package/dist/server/auth.d.ts +250 -0
  122. package/dist/server/auth.js +392 -0
  123. package/dist/server/deployments.d.ts +63 -1
  124. package/dist/server/deployments.js +75 -2
  125. package/dist/server/runs.d.ts +9 -0
  126. package/dist/server/runs.js +20 -0
  127. package/dist/server/server.js +5 -0
  128. package/dist/store/memory.d.ts +10 -0
  129. package/dist/store/memory.js +10 -0
  130. package/dist/testing/record.js +2 -2
  131. package/dist/tracing/stores.js +2 -1
  132. package/dist/tracing/tracer.js +2 -2
  133. package/dist/types/config.d.ts +20 -0
  134. package/dist/types/graph.d.ts +38 -0
  135. package/dist/types/messages.d.ts +36 -2
  136. package/dist/types/principal.d.ts +21 -0
  137. package/dist/types/principal.js +1 -0
  138. package/dist/types/server.d.ts +8 -9
  139. package/dist/utils/base64.d.ts +4 -0
  140. package/dist/utils/base64.js +21 -0
  141. package/dist/utils/checksums.d.ts +22 -0
  142. package/dist/utils/checksums.js +39 -0
  143. package/dist/utils/ids.d.ts +8 -0
  144. package/dist/utils/ids.js +26 -2
  145. package/dist/utils/sha256.d.ts +22 -0
  146. package/dist/utils/sha256.js +120 -0
  147. package/dist/utils/tenant.d.ts +11 -0
  148. package/dist/utils/tenant.js +14 -0
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/dist-cjs/adapter-kit/contracts.d.ts +87 -0
  152. package/dist-cjs/adapter-kit/contracts.js +243 -0
  153. package/dist-cjs/adapter-kit/errors.d.ts +39 -0
  154. package/dist-cjs/adapter-kit/errors.js +57 -0
  155. package/dist-cjs/adapter-kit/index.d.ts +144 -0
  156. package/dist-cjs/adapter-kit/index.js +194 -0
  157. package/dist-cjs/adapter-kit/semver.d.ts +12 -0
  158. package/dist-cjs/adapter-kit/semver.js +59 -0
  159. package/dist-cjs/agent/capabilities.d.ts +39 -0
  160. package/dist-cjs/agent/capabilities.js +41 -0
  161. package/dist-cjs/agent/create-agent.d.ts +61 -11
  162. package/dist-cjs/agent/create-agent.js +136 -22
  163. package/dist-cjs/agent/index.d.ts +3 -2
  164. package/dist-cjs/agent/index.js +4 -4
  165. package/dist-cjs/agent/middleware/approval.d.ts +26 -0
  166. package/dist-cjs/agent/middleware/approval.js +26 -0
  167. package/dist-cjs/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
  168. package/dist-cjs/agent/{middleware.js → middleware/basic.js} +3 -5
  169. package/dist-cjs/agent/middleware/context-editor.d.ts +52 -0
  170. package/dist-cjs/agent/middleware/context-editor.js +70 -0
  171. package/dist-cjs/agent/middleware/filesystem.d.ts +44 -0
  172. package/dist-cjs/agent/middleware/filesystem.js +86 -0
  173. package/dist-cjs/agent/middleware/index.d.ts +19 -0
  174. package/dist-cjs/agent/middleware/index.js +28 -0
  175. package/dist-cjs/agent/middleware/limits.d.ts +42 -0
  176. package/dist-cjs/agent/middleware/limits.js +57 -0
  177. package/dist-cjs/agent/middleware/models.d.ts +48 -0
  178. package/dist-cjs/agent/middleware/models.js +48 -0
  179. package/dist-cjs/agent/middleware/pii.d.ts +67 -0
  180. package/dist-cjs/agent/middleware/pii.js +147 -0
  181. package/dist-cjs/agent/middleware/retry.d.ts +66 -0
  182. package/dist-cjs/agent/middleware/retry.js +63 -0
  183. package/dist-cjs/agent/middleware/shared.d.ts +41 -0
  184. package/dist-cjs/agent/middleware/shared.js +83 -0
  185. package/dist-cjs/agent/middleware/tool-selector.d.ts +64 -0
  186. package/dist-cjs/agent/middleware/tool-selector.js +109 -0
  187. package/dist-cjs/agent/permissions.d.ts +113 -0
  188. package/dist-cjs/agent/permissions.js +189 -0
  189. package/dist-cjs/agent/sandbox.d.ts +151 -0
  190. package/dist-cjs/agent/sandbox.js +297 -0
  191. package/dist-cjs/agent/tool.d.ts +8 -3
  192. package/dist-cjs/agent/tool.js +14 -3
  193. package/dist-cjs/core/secure-stream.js +4 -3
  194. package/dist-cjs/deep-agent/index.d.ts +109 -0
  195. package/dist-cjs/deep-agent/index.js +219 -0
  196. package/dist-cjs/embeddings/adapters.js +2 -1
  197. package/dist-cjs/evals/judge.js +2 -5
  198. package/dist-cjs/evaluate/cache.d.ts +0 -14
  199. package/dist-cjs/evaluate/cache.js +1 -65
  200. package/dist-cjs/evaluate/file-cache.d.ts +15 -0
  201. package/dist-cjs/evaluate/file-cache.js +67 -0
  202. package/dist-cjs/evaluate/index.d.ts +2 -1
  203. package/dist-cjs/evaluate/index.js +3 -2
  204. package/dist-cjs/evaluate/review.js +2 -2
  205. package/dist-cjs/evaluate/run.js +2 -2
  206. package/dist-cjs/evaluate/version.js +3 -3
  207. package/dist-cjs/graph/functional.d.ts +9 -0
  208. package/dist-cjs/graph/functional.js +20 -3
  209. package/dist-cjs/graph/graph.js +27 -6
  210. package/dist-cjs/graph/lint.d.ts +4 -1
  211. package/dist-cjs/graph/lint.js +35 -0
  212. package/dist-cjs/graph/node-cache.js +3 -3
  213. package/dist-cjs/hallucination/rag.d.ts +4 -1
  214. package/dist-cjs/hallucination/rag.js +6 -1
  215. package/dist-cjs/images/asset-stores.js +2 -3
  216. package/dist-cjs/images/asset-support.js +2 -2
  217. package/dist-cjs/images/assets.js +1 -2
  218. package/dist-cjs/images/evals.js +4 -4
  219. package/dist-cjs/insights/proposals.js +1 -2
  220. package/dist-cjs/insights/regressions.d.ts +26 -2
  221. package/dist-cjs/insights/regressions.js +129 -3
  222. package/dist-cjs/loaders/git.js +8 -1
  223. package/dist-cjs/loaders/web.js +7 -1
  224. package/dist-cjs/mcp/client.d.ts +14 -0
  225. package/dist-cjs/mcp/client.js +60 -1
  226. package/dist-cjs/mcp/registry.d.ts +6 -0
  227. package/dist-cjs/mcp/registry.js +7 -0
  228. package/dist-cjs/postgres/fulltext.d.ts +67 -0
  229. package/dist-cjs/postgres/fulltext.js +129 -0
  230. package/dist-cjs/prompts/file.js +28 -11
  231. package/dist-cjs/protocols/a2a.d.ts +235 -0
  232. package/dist-cjs/protocols/a2a.js +393 -0
  233. package/dist-cjs/protocols/acp.d.ts +87 -0
  234. package/dist-cjs/protocols/acp.js +326 -0
  235. package/dist-cjs/protocols/ag-ui.d.ts +120 -0
  236. package/dist-cjs/protocols/ag-ui.js +186 -0
  237. package/dist-cjs/protocols/shared.d.ts +80 -0
  238. package/dist-cjs/protocols/shared.js +59 -0
  239. package/dist-cjs/providers/anthropic.js +1 -0
  240. package/dist-cjs/providers/cohere.js +2 -2
  241. package/dist-cjs/providers/google.js +3 -12
  242. package/dist-cjs/providers/openai.js +1 -0
  243. package/dist-cjs/rag/elasticsearch.d.ts +71 -0
  244. package/dist-cjs/rag/elasticsearch.js +135 -0
  245. package/dist-cjs/rag/file-ingestion.js +14 -8
  246. package/dist-cjs/rag/pipeline.d.ts +140 -0
  247. package/dist-cjs/rag/pipeline.js +191 -0
  248. package/dist-cjs/rag/rerankers.d.ts +92 -0
  249. package/dist-cjs/rag/rerankers.js +107 -0
  250. package/dist-cjs/rag/retrievers.d.ts +12 -1
  251. package/dist-cjs/runtime/detect.d.ts +17 -0
  252. package/dist-cjs/runtime/detect.js +19 -0
  253. package/dist-cjs/runtime/index.d.ts +19 -0
  254. package/dist-cjs/runtime/index.js +31 -0
  255. package/dist-cjs/security/injection-detector.d.ts +2 -0
  256. package/dist-cjs/security/injection-detector.js +7 -1
  257. package/dist-cjs/security/input-guard.js +5 -2
  258. package/dist-cjs/security/output-guard.js +8 -4
  259. package/dist-cjs/security/pii-detector.js +22 -6
  260. package/dist-cjs/security/prompt-hardening.js +6 -2
  261. package/dist-cjs/security/semantic-injection-classifier.d.ts +2 -1
  262. package/dist-cjs/security/semantic-injection-classifier.js +31 -35
  263. package/dist-cjs/security/upload-scanner.d.ts +8 -0
  264. package/dist-cjs/security/upload-scanner.js +53 -9
  265. package/dist-cjs/server/assistant.d.ts +5 -1
  266. package/dist-cjs/server/assistant.js +4 -2
  267. package/dist-cjs/server/auth.d.ts +250 -0
  268. package/dist-cjs/server/auth.js +402 -0
  269. package/dist-cjs/server/deployments.d.ts +63 -1
  270. package/dist-cjs/server/deployments.js +75 -2
  271. package/dist-cjs/server/runs.d.ts +9 -0
  272. package/dist-cjs/server/runs.js +20 -0
  273. package/dist-cjs/server/server.js +5 -0
  274. package/dist-cjs/store/memory.d.ts +10 -0
  275. package/dist-cjs/store/memory.js +10 -0
  276. package/dist-cjs/testing/record.js +2 -2
  277. package/dist-cjs/tracing/stores.js +2 -1
  278. package/dist-cjs/tracing/tracer.js +2 -2
  279. package/dist-cjs/types/config.d.ts +20 -0
  280. package/dist-cjs/types/graph.d.ts +38 -0
  281. package/dist-cjs/types/messages.d.ts +36 -2
  282. package/dist-cjs/types/principal.d.ts +21 -0
  283. package/dist-cjs/types/principal.js +2 -0
  284. package/dist-cjs/types/server.d.ts +8 -9
  285. package/dist-cjs/utils/base64.d.ts +4 -0
  286. package/dist-cjs/utils/base64.js +25 -0
  287. package/dist-cjs/utils/checksums.d.ts +22 -0
  288. package/dist-cjs/utils/checksums.js +46 -0
  289. package/dist-cjs/utils/ids.d.ts +8 -0
  290. package/dist-cjs/utils/ids.js +28 -2
  291. package/dist-cjs/utils/sha256.d.ts +22 -0
  292. package/dist-cjs/utils/sha256.js +125 -0
  293. package/dist-cjs/utils/tenant.d.ts +11 -0
  294. package/dist-cjs/utils/tenant.js +15 -0
  295. package/dist-cjs/version.d.ts +1 -1
  296. package/dist-cjs/version.js +1 -1
  297. package/llms.txt +7 -3
  298. package/package.json +193 -6
package/README.md CHANGED
@@ -8,7 +8,10 @@ team can share.
8
8
  Import only what you use. The root import is the client and nothing else, and every other capability
9
9
  has its own entry point with a size budget CI enforces; [the packaging guide](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md)
10
10
  publishes what each one costs. The package has no required dependencies: a graph-only application
11
- loads 78 KB, and installs nothing but this package.
11
+ loads 81 KB, and installs nothing but this package.
12
+ The client, the provider adapters, graphs, agents, and the agent protocols run on Node.js, Deno,
13
+ Bun, edge runtimes, and browsers; [the runtimes guide](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/runtimes.md)
14
+ lists the entry points that need Node.js, and why.
12
15
 
13
16
  - NPM: https://www.npmjs.com/package/nexus-ai-pro
14
17
  - GitHub: https://github.com/mkhitar-abrahamyan/nexus-ai
@@ -132,9 +135,11 @@ from the doc comments. The guides live in the repository, so these links go to G
132
135
  | [Providers and routing](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/providers.md) | The twelve completion providers, routing and failover, the model registry |
133
136
  | [Guardrails](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/security.md) | Injection, PII, secrets, schema validation, output redaction |
134
137
  | [Graphs](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/graphs.md) | Parallel branches, interrupts, checkpoints, time travel, subgraphs, caching, diagrams |
135
- | [Agents and tools](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/agents.md) | Graph-based agents with approvals and middleware, and the simple tool loop |
138
+ | [Agents and tools](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/agents.md) | Graph-based agents with approvals, capabilities and permission policies, a sandbox contract, the middleware catalog, and the simple tool loop |
139
+ | [The deep-agent preset](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/deep-agents.md) | Long, multi-step agents: a plan, a sandboxed workspace, helpers, skills, and a context that stays small |
136
140
  | [Long-term memory](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/memory.md) | Namespaced memory with semantic search, in memory or Redis |
137
141
  | [MCP](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/mcp.md) | Borrowing tools from MCP servers, a registry of many, and serving your own |
142
+ | [Agent protocols](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/protocols.md) | Serving an agent to user interfaces over AG-UI, to other agents over A2A, and to code editors over ACP, and calling remote agents as tools |
138
143
  | [Traces](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/tracing.md) | Run trees, queries, feedback, comparison, and alerts |
139
144
  | [Evaluation](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/evaluation.md) | Datasets, evaluators, experiments, comparisons with a verdict, review queues, LLM judges |
140
145
  | [Context hub](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/context-hub.md) | Prompts, instructions, tools, and skills versioned together, promoted through gates, and moved between projects |
@@ -155,6 +160,7 @@ from the doc comments. The guides live in the repository, so these links go to G
155
160
  | [Realtime voice](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/realtime.md) | Browser and server realtime sessions with barge-in, tools, and exports |
156
161
  | [Telephony](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/telephony.md) | Calls, webhooks, phone numbers, and phone agents on realtime sessions |
157
162
  | [Testing](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/testing.md) | Recording and replaying provider traffic, and conformance suites |
163
+ | [Adapter kit](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/adapter-kit.md) | Building a provider, store, or retriever outside this repository, and verifying it against the same contracts |
158
164
  | [Studio](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/studio.md) | A shared UI for traces, threads, approvals, experiments, prompts, deployments, costs, and health, as a separate package |
159
165
  | [Agent server](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/server.md) | Self-hosted HTTP server for assistants: threads, durable runs, resumable streams, cron, a worker queue, and metrics |
160
166
  | [Deployments](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/deployments.md) | Revisions and canaries with automatic rollback, autoscaling on Kubernetes and Helm, and per-tenant limits |
@@ -162,6 +168,7 @@ from the doc comments. The guides live in the repository, so these links go to G
162
168
  | [Workflows](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/workflows.md) | Ready-made chains and domain workflows |
163
169
  | [Command line](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/cli.md) | The `nexus` command |
164
170
  | [Packaging and install weight](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md) | Every entry point and what it costs |
171
+ | [Runtimes](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/runtimes.md) | The portable kernel, and running the client, providers, graphs, and agents on Deno, Bun, edge runtimes, and browsers |
165
172
 
166
173
  ## Examples
167
174
 
@@ -251,13 +258,20 @@ Shipped so far:
251
258
  backend, one event stream, and dashboard rollups;
252
259
  - 2.2 production scale and safe upgrades: versioned database migrations, queue claims and routing
253
260
  that cost the same at any size, a circuit breaker that counts across workers, one tenant model
254
- across every store, and `nexus doctor` to check a deployment before it serves traffic.
261
+ across every store, and `nexus doctor` to check a deployment before it serves traffic;
262
+ - 2.3 agents that act safely, on production retrieval: capabilities and permission policies, a
263
+ sandbox contract, a middleware catalog, server authentication with one principal on every run,
264
+ keyword search in Postgres and Elasticsearch with rerankers, durable ingestion, and a deep-agent
265
+ preset;
266
+ - 2.4 graduation and interoperability: SQLite, loaders, retrievers, the MCP registry, the context
267
+ hub, insights, and tenant limits out of experimental with published evidence; canaries judged with
268
+ confidence and on quality; an adapter kit; AG-UI, A2A, and ACP; and a portable kernel, with the
269
+ client, providers, graphs, and agents running on Deno, Bun, edge runtimes, and browsers.
255
270
 
256
271
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
257
272
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
258
- Next, 2.3 gives agents a real security boundary — capabilities, permissions, and a sandbox contract —
259
- with a complete middleware catalog, and retrieval that works at production scale. The [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists the
260
- releases after it.
273
+ What comes next is on the [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md):
274
+ the gaps 2.4 left open, and the work applications ask for after them.
261
275
 
262
276
  ## Before Production
263
277
 
package/SECURITY.md CHANGED
@@ -44,3 +44,143 @@ Nexus guardrails reduce common AI application risks, but they are not a complete
44
44
  - Browser WebRTC still requires a backend security boundary. A direct media path reduces latency; it
45
45
  does not remove the need for server-owned credentials, session policy, tool authorization, abuse
46
46
  monitoring, and provider-side safety controls.
47
+
48
+ ## Threat reviews
49
+
50
+ Every surface that leaves experimental status is reviewed for how untrusted input reaches it and
51
+ what it could do with that input. Each review is published here with its findings, which are closed
52
+ before the surface graduates. A finding is closed in one of two ways: by a fix with a test that
53
+ names the attack, or by a documented limitation that the surface's guide states.
54
+
55
+ These reviews were done for 2.4.
56
+
57
+ ### SQLite adapters
58
+
59
+ Trust boundary: an application's data, an operation's payload, and metadata filters that may come
60
+ from a request.
61
+ - Table names are checked against a strict pattern and quoted. Every value, including each metadata
62
+ filter key and its JSON path, is a bound parameter, so a filter cannot change a statement.
63
+ - A migration records its checksum, and a changed migration is refused rather than reapplied.
64
+ - Every change to an operation is one `UPDATE … WHERE sequence = expected`, so two workers sharing
65
+ a file never both claim it. The ten-worker race tests run on SQLite.
66
+ - Tenant data is scoped by key prefix, and the isolation suite runs on every SQLite store.
67
+
68
+ Findings: none.
69
+
70
+ ### The Postgres and SQLite vector stores
71
+
72
+ Trust boundary: metadata filters, which applications often build from a request.
73
+ - The pgvector store sends a filter as one bound `jsonb` containment parameter, and quotes its table
74
+ and index names.
75
+ - The SQLite store binds each key's JSON path and value, and matches a value's JSON type as well as
76
+ its value, so `"1"` never matches `1`.
77
+
78
+ Findings: none.
79
+
80
+ ### Loaders
81
+
82
+ Trust boundary: everything a loader reads is untrusted. A repository, a website, a sitemap, or an
83
+ uploaded file may be written by an attacker who wants it in a knowledge base, or who wants the
84
+ loader to read something else.
85
+ - Web pages and sitemaps go through the SSRF-safe fetch. Private, loopback, and cloud-metadata
86
+ addresses are refused, every redirect is checked again, and bodies are capped.
87
+ - The Git loader runs `git` without a shell. It disables the `ext::` transport, puts `--` before
88
+ the repository, and refuses a repository that starts with `-`.
89
+ - `loadDirectory()` does not follow symbolic links.
90
+
91
+ Findings, both fixed in 2.4:
92
+ - **The Git loader followed symbolic links.** A tracked link such as `notes.md` pointing to
93
+ `~/.ssh/id_rsa` was read and ingested, so a knowledge base could quote a file of the host. A
94
+ link is now never followed, nor a path that resolves outside the checkout. Proof: a repository
95
+ holding a tracked link to a file outside it loads without that file, cloned and in place. On
96
+ Linux, the test fails on the earlier loader.
97
+ - **A gzipped sitemap could be a decompression bomb.** A few megabytes of gzip inflated without a
98
+ limit. Inflation now stops at 50 MB, the sitemap protocol's own maximum. Proof: 60 MB of gzip
99
+ that inflates past it is refused.
100
+
101
+ ### Retrievers
102
+
103
+ Trust boundary: queries come from users, and documents may hold text written to steer a model.
104
+ - Tokenizing is one linear regular expression, with no backtracking on hostile input.
105
+ - Filters compare values; they never become code or a query language.
106
+ - A model reranker or query-variant generator reads documents and queries as data in its prompt. Its
107
+ output is parsed as JSON and used only to order or widen a search, never to call a tool.
108
+
109
+ Findings: none.
110
+
111
+ ### MCP registry
112
+
113
+ Trust boundary: the configuration file is trusted, since it names programs to run. What a server
114
+ says about itself is not: its tool names, its descriptions, and its results.
115
+ - A server's tools are prefixed with its name by default, so one server cannot shadow another's
116
+ tool.
117
+ - Allow and deny lists limit what agents see, and deny wins.
118
+ - A tool from a server declares no capabilities. A permission policy denies such a tool unless its
119
+ `undeclared` rule says otherwise, and `humanApproval()` asks about it by default.
120
+ - `${NAME}` placeholders keep credentials out of the file. A missing variable is named in the error,
121
+ never its value.
122
+
123
+ Finding, fixed in 2.4: **a local server inherited this process's whole environment.** Any
124
+ `npx`-started server, which is usually third-party code, saw every API key and database URL the
125
+ application held. A server now starts with `env` and only what a process needs to run: the path,
126
+ the home and temporary directories, the locale, proxies, and certificates. The new `inheritEnv`
127
+ option widens this by name, or entirely. Proof: a real child process started by the transport sees
128
+ the variables it was given, and not a secret set in the parent.
129
+
130
+ ### Context hub
131
+
132
+ Trust boundary: an imported bundle may come from another team or another organization. Who may
133
+ commit and promote is the application's access control, which the studio's roles enforce.
134
+ - An import recomputes each bundle's and each prompt's content version, and refuses one whose content
135
+ changed after export.
136
+ - A bundle names tools, but `bindTools()` binds only implementations the application supplies, so
137
+ a bundle cannot add code.
138
+ - Promotion runs the label's gates, an experiment over the exact version among them.
139
+ - Labels move by compare-and-set, so a promotion decided on a stale version is refused.
140
+
141
+ Finding, fixed in 2.4: **the file store's compare-and-set could lose a move.** Two label moves at
142
+ once in one process could both pass the check, and the later write erased the earlier one. Label
143
+ writes to one file are now serialized across the process. Proof: two hubs whose writes are held
144
+ until both arrive race for one label on every backend, and on the file store the earlier code
145
+ loses a move every time. Several processes promoting the same label at once still need the Redis or
146
+ Postgres store, as the prompts guide says.
147
+
148
+ ### Insights
149
+
150
+ Trust boundary: runs and their errors carry user data, and a model proposes fixes.
151
+ - An error signature replaces numbers, quoted values, URLs, ids, and long hexadecimal strings, so a
152
+ cluster's summary does not carry a customer's data.
153
+ - A model's proposed fix is parsed as data, evaluated on a dataset, and never applied without a
154
+ person. A promotion still runs the label's gates.
155
+ - A proposal id is checked against a strict pattern before it becomes a file name.
156
+
157
+ Findings: none.
158
+
159
+ ### Tenant limits and the worker queue
160
+
161
+ Trust boundary: a tenant is whoever the application's `authenticate()` says, never a header the
162
+ server trusts by itself.
163
+ - Concurrency, rate, and budget limits are kept in a shared store, so several replicas enforce one
164
+ limit. Admission and release are tested by ten racing workers.
165
+ - A run is released exactly once, however it ends.
166
+ - The queue claims an operation under a lease in one atomic step on every store. A lost lease
167
+ cancels the run's signal, and a crash at any durable boundary resumes to the same state.
168
+
169
+ Findings: none.
170
+
171
+ ### Deployments (reviewed; still experimental)
172
+
173
+ Findings:
174
+ - **Fixed in 2.4: one caller's ratings could decide a canary.** A canary was judged on feedback
175
+ entries, not on runs. A hundred low ratings of one run met the minimum sample alone, and could
176
+ roll back a canary for everyone, or keep a bad one alive. Each run now counts once per feedback
177
+ key, as the mean of its ratings.
178
+ - **Fixed in 2.4: two changes at once on one replica could lose one**, such as a guard's rollback
179
+ and an operator's promotion. Changes to one deployment now apply in order.
180
+ - **Open: two replicas changing one deployment in the same instant can lose one change.** The
181
+ server's state store has no atomic compare-and-set. Until it does, deployments stay experimental.
182
+ The guide says to make changes from one place, or to pass `expectedVersion` and retry a conflict.
183
+ - **Documented limitation: `bucket()` spreads sequential ids unevenly.** Ids that differ only in their
184
+ last characters spread unevenly over a few hundred threads. Random ids, which the server gives by
185
+ default, spread evenly. The function's output is part of the stability promise, so it is not
186
+ changed in 2.x.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The contract suites the adapter kit ships: what every vector store, retriever, and long-term store
3
+ * must do, as checks that need no test framework. Each check is named for the behavior it proves,
4
+ * and a capability the adapter does not declare is reported as skipped, never as a failure.
5
+ */
6
+ import type { RagChunk } from '../hallucination/rag.js';
7
+ import type { EmbeddingProvider, VectorStore } from '../hallucination/retrieval.js';
8
+ import type { Retriever } from '../rag/retrievers.js';
9
+ import type { Store } from '../types/store.js';
10
+ /** One check of a contract suite. */
11
+ export interface AdapterCheck {
12
+ /** What was checked, as a sentence. */
13
+ name: string;
14
+ /** Whether it held. */
15
+ ok: boolean;
16
+ /** Why it did not hold, or why it was skipped. */
17
+ detail?: string;
18
+ /** True when the adapter does not declare the capability the check needs. */
19
+ skipped?: boolean;
20
+ }
21
+ /** The passages every retrieval contract runs on, and that benchmarks reuse. */
22
+ export declare const ADAPTER_FIXTURE_PASSAGES: readonly RagChunk[];
23
+ /** What a vector store declares it supports, for its contract. */
24
+ export interface VectorStoreCapabilities {
25
+ /** `searchVector()` with a vector computed elsewhere. Defaults to true. */
26
+ searchVector?: boolean;
27
+ /** `minScore` drops weak matches. Defaults to true. */
28
+ minScore?: boolean;
29
+ /** Metadata filters: `exact` matches values, `typed` also tells `3` from `'3'`, `none` has no filters. Defaults to `exact`. */
30
+ filters?: 'none' | 'exact' | 'typed';
31
+ }
32
+ /** Options for `runVectorStoreContract()`. */
33
+ export interface VectorStoreContractOptions {
34
+ /** The embedding function the store was given, to compute a query vector for `searchVector()`. */
35
+ embed: EmbeddingProvider;
36
+ /** What the store supports. */
37
+ capabilities?: VectorStoreCapabilities;
38
+ /** Waits for writes to become searchable, for a store whose index is eventually consistent. */
39
+ settle?: () => Promise<void>;
40
+ }
41
+ /**
42
+ * The `VectorStore` contract: ranking, `topK`, sources and metadata round-tripped, filters,
43
+ * `minScore`, vector search, replacement by id, deletes, and empty writes. The store should be empty
44
+ * when it starts.
45
+ */
46
+ export declare function runVectorStoreContract(store: VectorStore, options: VectorStoreContractOptions): Promise<AdapterCheck[]>;
47
+ /** What a retriever declares it supports, for its contract. */
48
+ export interface RetrieverCapabilities {
49
+ /** It holds its own passages, with `add()` and `delete()`: a `SparseRetriever`. Defaults to whether it has them. */
50
+ writes?: boolean;
51
+ /** Metadata filters. Defaults to true. */
52
+ filters?: boolean;
53
+ }
54
+ /** Options for `runRetrieverContract()`. */
55
+ export interface RetrieverContractOptions {
56
+ /** What the retriever supports. */
57
+ capabilities?: RetrieverCapabilities;
58
+ /**
59
+ * Loads the fixture passages, for a retriever that does not hold its own: into the vector store
60
+ * behind it, say. A retriever with `add()` is loaded through that.
61
+ */
62
+ load?: (passages: RagChunk[]) => Promise<void>;
63
+ /** Waits for writes to become searchable. */
64
+ settle?: () => Promise<void>;
65
+ }
66
+ /**
67
+ * The `Retriever` contract, and the `SparseRetriever` contract when the retriever takes writes:
68
+ * an exact term found, ranking, `topK`, filters, replacement, deletes, and an empty query.
69
+ */
70
+ export declare function runRetrieverContract(retriever: Retriever, options?: RetrieverContractOptions): Promise<AdapterCheck[]>;
71
+ /** What a long-term store declares it supports, for its contract. */
72
+ export interface StoreCapabilities {
73
+ /** `search()` lists a namespace prefix, newest first. Defaults to true. */
74
+ search?: boolean;
75
+ /** `search()` filters on value fields. Defaults to true. */
76
+ filters?: boolean;
77
+ /** `listNamespaces()`. Defaults to true. */
78
+ namespaces?: boolean;
79
+ }
80
+ /**
81
+ * The long-term `Store` contract: values round-trip, a replaced item keeps its creation time, a
82
+ * missing key reads as nothing, deletes, prefix search with filters and paging, and namespace
83
+ * listing. The store should be empty when it starts.
84
+ */
85
+ export declare function runStoreContract(store: Store, options?: {
86
+ capabilities?: StoreCapabilities;
87
+ }): Promise<AdapterCheck[]>;
@@ -0,0 +1,237 @@
1
+ export const ADAPTER_FIXTURE_PASSAGES = [
2
+ {
3
+ id: 'refunds#0',
4
+ content: 'Refunds are issued within fourteen days of a return',
5
+ source: 'policy.md',
6
+ metadata: { tenant: 'acme', page: 3 },
7
+ },
8
+ {
9
+ id: 'shipping#0',
10
+ content: 'Orders ship from the warehouse within two business days',
11
+ source: 'policy.md',
12
+ metadata: { tenant: 'acme' },
13
+ },
14
+ { id: 'refunds#1', content: 'A refund goes back to the original payment method', metadata: { tenant: 'globex' } },
15
+ { id: 'careers#0', content: 'We are hiring engineers in three offices' },
16
+ ];
17
+ function fixturePassages() {
18
+ return ADAPTER_FIXTURE_PASSAGES.map((passage) => ({
19
+ ...passage,
20
+ ...(passage.metadata ? { metadata: { ...passage.metadata } } : {}),
21
+ }));
22
+ }
23
+ class Suite {
24
+ checks = [];
25
+ async check(name, run) {
26
+ try {
27
+ const result = await run();
28
+ this.checks.push(result === true ? { name, ok: true } : { name, ok: false, detail: result === false ? 'did not hold' : result });
29
+ }
30
+ catch (error) {
31
+ this.checks.push({ name, ok: false, detail: error instanceof Error ? error.message : String(error) });
32
+ }
33
+ }
34
+ skip(name, reason) {
35
+ this.checks.push({ name, ok: true, skipped: true, detail: reason });
36
+ }
37
+ }
38
+ export async function runVectorStoreContract(store, options) {
39
+ const suite = new Suite();
40
+ const settle = options.settle ?? (async () => undefined);
41
+ const capabilities = options.capabilities ?? {};
42
+ const filters = capabilities.filters ?? 'exact';
43
+ await suite.check('adds passages', async () => {
44
+ await store.add(fixturePassages());
45
+ await settle();
46
+ return true;
47
+ });
48
+ await suite.check('ranks the closest passage first, best first, within topK', async () => {
49
+ const results = await store.search('how long do refunds take', { topK: 2 });
50
+ if (results.length !== 2)
51
+ return `expected 2 results for topK 2, got ${results.length}`;
52
+ if (results[0]?.id !== 'refunds#0')
53
+ return `expected refunds#0 first, got ${results[0]?.id}`;
54
+ return (results[0]?.score ?? 0) > (results[1]?.score ?? 0) || 'results are not ordered by score';
55
+ });
56
+ await suite.check('returns a passage source and its metadata whole', async () => {
57
+ const [first] = await store.search('how long do refunds take', { topK: 1 });
58
+ if (first?.source !== 'policy.md')
59
+ return `expected source policy.md, got ${first?.source}`;
60
+ return (JSON.stringify(first?.metadata) === JSON.stringify({ tenant: 'acme', page: 3 }) ||
61
+ `metadata came back as ${JSON.stringify(first?.metadata)}`);
62
+ });
63
+ if (filters === 'none')
64
+ suite.skip('narrows a search by metadata', 'the store declares no filters');
65
+ else {
66
+ await suite.check('narrows a search by metadata', async () => {
67
+ const results = await store.search('refund', { filter: { tenant: 'globex' } });
68
+ return ((results.length === 1 && results[0]?.id === 'refunds#1') ||
69
+ `got ${results.map((result) => result.id).join(', ')}`);
70
+ });
71
+ }
72
+ if (filters === 'typed') {
73
+ await suite.check('tells a number from the same digits as text in a filter', async () => {
74
+ const numeric = await store.search('refunds', { filter: { page: 3 }, topK: 10 });
75
+ const text = await store.search('refunds', { filter: { page: '3' }, topK: 10 });
76
+ return ((numeric.length === 1 && text.length === 0) || `number matched ${numeric.length}, text matched ${text.length}`);
77
+ });
78
+ }
79
+ else
80
+ suite.skip('tells a number from the same digits as text in a filter', 'the store declares untyped filters');
81
+ if (capabilities.minScore === false)
82
+ suite.skip('drops matches under minScore', 'the store declares no minScore');
83
+ else {
84
+ await suite.check('drops matches under minScore', async () => {
85
+ const results = await store.search('how long do refunds take', { minScore: 0.99 });
86
+ return results.length === 0 || `${results.length} results above 0.99`;
87
+ });
88
+ }
89
+ if (capabilities.searchVector === false)
90
+ suite.skip('searches by a precomputed vector', 'the store declares no searchVector');
91
+ else {
92
+ await suite.check('searches by a precomputed vector', async () => {
93
+ const [vector] = await options.embed(['orders ship from the warehouse']);
94
+ const [first] = await store.searchVector(vector, { topK: 1 });
95
+ return first?.id === 'shipping#0' || `expected shipping#0, got ${first?.id}`;
96
+ });
97
+ }
98
+ await suite.check('replaces a passage added again under its id', async () => {
99
+ await store.add([
100
+ { id: 'careers#0', content: 'Refunds for event tickets are not available', metadata: { tenant: 'acme' } },
101
+ ]);
102
+ await settle();
103
+ const results = await store.search('event tickets refunds', { topK: 10 });
104
+ const copies = results.filter((result) => result.id === 'careers#0');
105
+ if (copies.length !== 1)
106
+ return `found ${copies.length} copies`;
107
+ return /event tickets/.test(copies[0]?.content ?? '') || 'the old content is still there';
108
+ });
109
+ await suite.check('deletes by id, ignoring ids it does not hold', async () => {
110
+ await store.delete(['refunds#0', 'not-stored']);
111
+ await settle();
112
+ const results = await store.search('how long do refunds take', { topK: 10 });
113
+ return !results.some((result) => result.id === 'refunds#0') || 'the deleted passage is still found';
114
+ });
115
+ await suite.check('accepts empty writes', async () => {
116
+ await store.add([]);
117
+ await store.delete([]);
118
+ return true;
119
+ });
120
+ return suite.checks;
121
+ }
122
+ export async function runRetrieverContract(retriever, options = {}) {
123
+ const suite = new Suite();
124
+ const settle = options.settle ?? (async () => undefined);
125
+ const sparse = retriever;
126
+ const writes = options.capabilities?.writes ?? (typeof sparse.add === 'function' && typeof sparse.delete === 'function');
127
+ const passages = fixturePassages();
128
+ await suite.check('loads the fixture passages', async () => {
129
+ if (writes)
130
+ await sparse.add?.(passages);
131
+ else if (options.load)
132
+ await options.load(passages);
133
+ else
134
+ return 'a retriever without add() needs a load() to fill what it reads from';
135
+ await settle();
136
+ return true;
137
+ });
138
+ await suite.check('finds the passage holding the query terms first', async () => {
139
+ const [first] = await retriever.retrieve('warehouse business days', { topK: 3 });
140
+ return first?.id === 'shipping#0' || `expected shipping#0, got ${first?.id}`;
141
+ });
142
+ await suite.check('returns at most topK results, best first', async () => {
143
+ const results = await retriever.retrieve('refund refunds return', { topK: 1 });
144
+ return results.length <= 1 || `${results.length} results for topK 1`;
145
+ });
146
+ if (options.capabilities?.filters === false)
147
+ suite.skip('narrows by metadata', 'the retriever declares no filters');
148
+ else {
149
+ await suite.check('narrows by metadata', async () => {
150
+ const results = await retriever.retrieve('refund', { filter: { tenant: 'globex' }, topK: 10 });
151
+ return ((results.every((result) => result.metadata?.tenant === 'globex') && results.length > 0) ||
152
+ `got ${results.map((result) => result.id).join(', ')}`);
153
+ });
154
+ }
155
+ if (!writes) {
156
+ suite.skip('replaces a passage added again under its id', 'the retriever does not take writes');
157
+ suite.skip('deletes by id, ignoring ids it does not hold', 'the retriever does not take writes');
158
+ }
159
+ else {
160
+ await suite.check('replaces a passage added again under its id', async () => {
161
+ await sparse.add?.([{ id: 'careers#0', content: 'Engineers relocate with a stipend' }]);
162
+ await settle();
163
+ const results = await retriever.retrieve('stipend relocate', { topK: 10 });
164
+ return (results.filter((result) => result.id === 'careers#0').length === 1 || 'the passage is missing or duplicated');
165
+ });
166
+ await suite.check('deletes by id, ignoring ids it does not hold', async () => {
167
+ await sparse.delete?.(['shipping#0', 'not-stored']);
168
+ await settle();
169
+ const results = await retriever.retrieve('warehouse business days', { topK: 10 });
170
+ return !results.some((result) => result.id === 'shipping#0') || 'the deleted passage is still found';
171
+ });
172
+ }
173
+ await suite.check('answers an empty query without failing', async () => {
174
+ await retriever.retrieve('', { topK: 3 });
175
+ return true;
176
+ });
177
+ return suite.checks;
178
+ }
179
+ export async function runStoreContract(store, options = {}) {
180
+ const suite = new Suite();
181
+ const capabilities = options.capabilities ?? {};
182
+ const namespace = ['contract', 'users'];
183
+ await suite.check('round-trips a value', async () => {
184
+ await store.put(namespace, 'ann', { name: 'Ann', plan: 'pro' });
185
+ const item = await store.get(namespace, 'ann');
186
+ return (item?.value.name === 'Ann' && item.key === 'ann') || `read ${JSON.stringify(item)}`;
187
+ });
188
+ await suite.check('keeps the creation time when an item is replaced', async () => {
189
+ const before = await store.get(namespace, 'ann');
190
+ await new Promise((resolve) => setTimeout(resolve, 5));
191
+ await store.put(namespace, 'ann', { name: 'Ann', plan: 'team' });
192
+ const after = await store.get(namespace, 'ann');
193
+ if (after?.value.plan !== 'team')
194
+ return 'the replacement was not stored';
195
+ return after.createdAt === before?.createdAt || `createdAt moved from ${before?.createdAt} to ${after.createdAt}`;
196
+ });
197
+ await suite.check('reads a missing key as nothing', async () => (await store.get(namespace, 'nobody')) === undefined);
198
+ await suite.check('deletes an item', async () => {
199
+ await store.put(namespace, 'temp', { name: 'Temp' });
200
+ await store.delete(namespace, 'temp');
201
+ return (await store.get(namespace, 'temp')) === undefined;
202
+ });
203
+ if (capabilities.search === false)
204
+ suite.skip('lists a namespace prefix, with paging', 'the store declares no search');
205
+ else {
206
+ await suite.check('lists a namespace prefix, with paging', async () => {
207
+ await store.put([...namespace, 'archived'], 'bob', { name: 'Bob', plan: 'free' });
208
+ await store.put(namespace, 'cy', { name: 'Cy', plan: 'free' });
209
+ const all = await store.search(['contract'], { limit: 10 });
210
+ if (all.length !== 3)
211
+ return `expected 3 items under the prefix, got ${all.length}`;
212
+ const page = await store.search(['contract'], { limit: 2, offset: 2 });
213
+ return page.length === 1 || `expected 1 item on the second page, got ${page.length}`;
214
+ });
215
+ }
216
+ if (capabilities.search === false || capabilities.filters === false)
217
+ suite.skip('filters by a value field', 'the store declares no filters');
218
+ else {
219
+ await suite.check('filters by a value field', async () => {
220
+ const free = await store.search(['contract'], { filter: { plan: 'free' }, limit: 10 });
221
+ const names = free
222
+ .map((item) => item.value.name)
223
+ .sort()
224
+ .join(',');
225
+ return names === 'Bob,Cy' || `got ${names}`;
226
+ });
227
+ }
228
+ if (capabilities.namespaces === false)
229
+ suite.skip('lists namespaces under a prefix', 'the store declares no namespace listing');
230
+ else {
231
+ await suite.check('lists namespaces under a prefix', async () => {
232
+ const listed = (await store.listNamespaces({ prefix: ['contract'] })).map((parts) => parts.join('/')).sort();
233
+ return listed.join(',') === 'contract/users,contract/users/archived' || `got ${listed.join(', ')}`;
234
+ });
235
+ }
236
+ return suite.checks;
237
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * One vocabulary for adapter failures, so code above an adapter retries, alerts, and reports the same
3
+ * way whichever database, provider, or service the adapter talks to.
4
+ */
5
+ /** What kind of failure an adapter call met. */
6
+ export type AdapterErrorCode = 'AUTH' | 'RATE_LIMIT' | 'NOT_FOUND' | 'INVALID' | 'CONFLICT' | 'TIMEOUT' | 'UNAVAILABLE' | 'CANCELLED' | 'UNKNOWN';
7
+ /** A failure's code, and whether another attempt could succeed. */
8
+ export interface AdapterErrorClass {
9
+ /** The kind of failure. */
10
+ code: AdapterErrorCode;
11
+ /** True for a failure a retry could get past: a rate limit, a timeout, an unavailable service. */
12
+ retryable: boolean;
13
+ /** The HTTP status, when the failure carried one. */
14
+ status?: number;
15
+ }
16
+ /** A failed adapter call, normalized. The original error is its `cause`. */
17
+ export declare class AdapterError extends Error {
18
+ /** The adapter that failed. */
19
+ readonly adapter: string;
20
+ /** The method that failed, such as `search`. */
21
+ readonly operation: string;
22
+ /** The kind of failure. */
23
+ readonly code: AdapterErrorCode;
24
+ /** Whether another attempt could succeed. */
25
+ readonly retryable: boolean;
26
+ /** The HTTP status, when the failure carried one. */
27
+ readonly status?: number;
28
+ constructor(
29
+ /** The adapter that failed. */
30
+ adapter: string,
31
+ /** The method that failed, such as `search`. */
32
+ operation: string, failure: AdapterErrorClass, cause: unknown);
33
+ }
34
+ /**
35
+ * Classifies any error by what it carries: an HTTP status (`status`, `statusCode`, or
36
+ * `response.status`), a Node network code such as `ECONNREFUSED`, or an `AbortError` or
37
+ * `TimeoutError` name. An adapter's own `normalizeError` runs first and may override it.
38
+ */
39
+ export declare function classifyAdapterError(error: unknown): AdapterErrorClass;
@@ -0,0 +1,52 @@
1
+ export class AdapterError extends Error {
2
+ adapter;
3
+ operation;
4
+ code;
5
+ retryable;
6
+ status;
7
+ constructor(adapter, operation, failure, cause) {
8
+ super(`${adapter} ${operation} failed (${failure.code}): ${cause instanceof Error ? cause.message : String(cause)}`, {
9
+ cause,
10
+ });
11
+ this.adapter = adapter;
12
+ this.operation = operation;
13
+ this.name = 'AdapterError';
14
+ this.code = failure.code;
15
+ this.retryable = failure.retryable;
16
+ if (failure.status !== undefined)
17
+ this.status = failure.status;
18
+ }
19
+ }
20
+ const RETRYABLE = new Set(['RATE_LIMIT', 'TIMEOUT', 'UNAVAILABLE']);
21
+ export function classifyAdapterError(error) {
22
+ const value = error;
23
+ const raw = value?.status ?? value?.statusCode ?? value?.response?.status;
24
+ const status = typeof raw === 'number' ? raw : undefined;
25
+ const code = (() => {
26
+ if (value?.name === 'AbortError')
27
+ return 'CANCELLED';
28
+ if (value?.name === 'TimeoutError' || value?.code === 'ETIMEDOUT')
29
+ return 'TIMEOUT';
30
+ if (status !== undefined) {
31
+ if (status === 401 || status === 403)
32
+ return 'AUTH';
33
+ if (status === 404)
34
+ return 'NOT_FOUND';
35
+ if (status === 408)
36
+ return 'TIMEOUT';
37
+ if (status === 409 || status === 412)
38
+ return 'CONFLICT';
39
+ if (status === 429)
40
+ return 'RATE_LIMIT';
41
+ if (status >= 500)
42
+ return 'UNAVAILABLE';
43
+ if (status >= 400)
44
+ return 'INVALID';
45
+ }
46
+ if (typeof value?.code === 'string' && /^(ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|EPIPE)$/.test(value.code)) {
47
+ return 'UNAVAILABLE';
48
+ }
49
+ return 'UNKNOWN';
50
+ })();
51
+ return { code, retryable: RETRYABLE.has(code), ...(status === undefined ? {} : { status }) };
52
+ }