failproofai 1.0.7 → 1.0.8

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 (268) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +9 -7
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +11 -10
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +7 -6
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -7
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  74. package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
  109. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  110. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  113. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  120. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  130. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  131. package/.next/standalone/.next/server/pages/404.html +1 -1
  132. package/.next/standalone/.next/server/pages/500.html +1 -1
  133. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
  135. package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
  136. package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
  137. package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
  140. package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
  142. package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
  143. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  144. package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
  145. package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
  146. package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
  147. package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
  148. package/.next/standalone/app/actions/get-jev-config.ts +604 -0
  149. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  150. package/.next/standalone/app/actions/update-jev-config.ts +569 -0
  151. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  152. package/.next/standalone/app/policies/hooks-client.tsx +14 -3
  153. package/.next/standalone/app/settings/jev-panel.tsx +630 -0
  154. package/.next/standalone/app/settings/page.tsx +20 -1
  155. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  156. package/.next/standalone/app/settings/settings.css +79 -0
  157. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  158. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  159. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  160. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  163. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  164. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  165. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  166. package/.next/standalone/package.json +10 -10
  167. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  168. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  169. package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
  171. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  172. package/.next/standalone/server.js +1 -1
  173. package/README.md +3 -2
  174. package/bin/failproofai.mjs +148 -4
  175. package/dist/cli.mjs +18599 -9478
  176. package/dist/index.js +19 -1
  177. package/dist/worker.mjs +10500 -3773
  178. package/package.json +10 -10
  179. package/pi-extension/index.ts +11 -0
  180. package/scripts/build-policy-pack.mjs +53 -2
  181. package/src/audit/features.ts +3 -2
  182. package/src/hooks/builtin-policies.ts +195 -8
  183. package/src/hooks/cloud-connection.ts +190 -1
  184. package/src/hooks/cloud-enrollment-cli.ts +57 -8
  185. package/src/hooks/cloud-introspect.ts +6 -0
  186. package/src/hooks/cloud-managed-policies.ts +22 -0
  187. package/src/hooks/configure-wizard.ts +26 -6
  188. package/src/hooks/custom-hooks-loader.ts +98 -10
  189. package/src/hooks/custom-hooks-registry.ts +45 -1
  190. package/src/hooks/effective-reviewers.ts +243 -0
  191. package/src/hooks/first-run-gate.ts +5 -0
  192. package/src/hooks/flush-cli.ts +35 -8
  193. package/src/hooks/fp-config.ts +266 -1
  194. package/src/hooks/fp-home.ts +23 -0
  195. package/src/hooks/fp-reset.ts +22 -4
  196. package/src/hooks/handler.ts +303 -7
  197. package/src/hooks/hook-activity-store.ts +118 -6
  198. package/src/hooks/hook-telemetry.ts +41 -0
  199. package/src/hooks/jev-activity.ts +385 -0
  200. package/src/hooks/jev-cli.ts +2172 -0
  201. package/src/hooks/jev-cloud-connection.ts +392 -0
  202. package/src/hooks/loader-utils.ts +6 -0
  203. package/src/hooks/manager.ts +45 -7
  204. package/src/hooks/pack-cli.ts +568 -37
  205. package/src/hooks/pack-failclosed.ts +3 -0
  206. package/src/hooks/pack-manifest.ts +489 -7
  207. package/src/hooks/pack-store.ts +194 -11
  208. package/src/hooks/policy-authority.ts +386 -0
  209. package/src/hooks/policy-catalog.ts +206 -0
  210. package/src/hooks/policy-evaluator.ts +963 -796
  211. package/src/hooks/policy-registry.ts +27 -1
  212. package/src/hooks/policy-reviewability.ts +258 -0
  213. package/src/hooks/policy-types.ts +128 -0
  214. package/src/hooks/semantic/combine.ts +581 -0
  215. package/src/hooks/semantic/compile.ts +176 -0
  216. package/src/hooks/semantic/decide.ts +513 -0
  217. package/src/hooks/semantic/envelope.ts +1255 -0
  218. package/src/hooks/semantic/evaluator.ts +556 -0
  219. package/src/hooks/semantic/facts.ts +387 -0
  220. package/src/hooks/semantic/intent.ts +1190 -0
  221. package/src/hooks/semantic/jev-client.ts +1155 -0
  222. package/src/hooks/semantic/jev-config.ts +1142 -0
  223. package/src/hooks/semantic/jev-review.ts +389 -0
  224. package/src/hooks/semantic/jev-stats.ts +289 -0
  225. package/src/hooks/semantic/jev-throttle.ts +427 -0
  226. package/src/hooks/semantic/pack-policies.ts +294 -0
  227. package/src/hooks/semantic/policies.ts +597 -0
  228. package/src/hooks/semantic/precondition-names.ts +60 -0
  229. package/src/hooks/semantic/preconditions.ts +58 -0
  230. package/src/hooks/semantic/redact.ts +2937 -0
  231. package/src/hooks/semantic/session-root.ts +116 -0
  232. package/src/hooks/semantic/types.ts +162 -0
  233. package/src/hooks/semver-precedence.ts +128 -0
  234. package/src/hooks/tui.ts +4 -0
  235. package/src/hooks/types.ts +1 -1
  236. package/src/hooks/worker-server.ts +121 -27
  237. package/src/index.ts +6 -0
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
  240. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  253. package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
  255. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  256. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  258. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
  259. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
  260. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  261. package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
  262. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
  263. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
  264. package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
  265. package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
  266. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
  267. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
  268. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
@@ -0,0 +1,1155 @@
1
+ /**
2
+ * The client for Jev, and the provider layer that turns a customer's own
3
+ * config (BYOK, `jev-config.ts`) into a transport.
4
+ *
5
+ * # Six routes, two wire shapes
6
+ *
7
+ * | Provider | Endpoint | Default model |
8
+ * |-------------|-------------------------------------------------------|----------------------|
9
+ * | typesafe | `https://api.typesafe.ai/v1/systemone` | `jev-1.13.0` |
10
+ * | openrouter | `https://openrouter.ai/api/v1/systemone` | `typesafe/jev-1.13` |
11
+ * | vercel | `https://ai-gateway.vercel.sh/typesafe/v1/systemone` | `typesafe-ai/jev` |
12
+ * | cloudflare | `https://api.cloudflare.com/client/v4/accounts/<id>/ai/run` | `typesafe/jev` |
13
+ * | custom | `<baseUrl>/systemone` | `jev-1.13.0` |
14
+ * | failproofai | `<Cloud origin>/enforcement/v1/jev/systemone` | `jev-1.13.0` |
15
+ *
16
+ * `failproofai` is FailproofAI Cloud: the machine's own Cloud key (the `jev`
17
+ * slot of `credentials.json`, see `jev-config.ts`) as the bearer, the org's
18
+ * plan allowance as the budget. The server forces `jev-1.13.0` and passes
19
+ * TypeSafe's answer through with its `model`, so this route accepts only a
20
+ * reported 1.13 model, never silence. Its documented statuses map like every
21
+ * other route's: 402 `{"error":"out_of_credits"}` → `out-of-credits` (the body
22
+ * never says "model execution failed"), 400/401/403/413/422/429/502/503 →
23
+ * `http-<status>`, and a redirect is refused. After a 429 the route sends
24
+ * nothing for as long as its `Retry-After` asks (capped; `cloudRetryAfter`).
25
+ *
26
+ * `baseUrl` is a BASE — the provider's version root — and `/systemone` is this
27
+ * file's own suffix (`nativeEndpoint`). Measured 2026-09-25, unauthenticated:
28
+ * `POST <base>/systemone` answers 403 / 401 / 400 / 401 on the four bases above
29
+ * and NEVER 404, so a 404 from it is evidence that the base is wrong rather
30
+ * than that the service is down — which is what `postJson` says when it sees
31
+ * one.
32
+ *
33
+ * TypeSafe, OpenRouter, Vercel and a custom URL all take TypeSafe's native body
34
+ * `{model, state, questions}` and answer `{model, answers, usage}`, so one
35
+ * transport (`nativeTransport`) serves all four: base URL, `Bearer` key and the
36
+ * right model id. OpenRouter additionally gets
37
+ * `provider: {zdr: true, data_collection: "deny", allow_fallbacks: false}` so a
38
+ * tool call is only ever routed to a zero-data-retention endpoint and never
39
+ * silently to another model. Cloudflare Workers AI wraps the request as
40
+ * `{model: "typesafe/jev", input: {state, questions}}` and nests the answer
41
+ * under a job layer (`result.result`), so it keeps its own transport.
42
+ *
43
+ * Auth everywhere is `Authorization: Bearer <key>`. The key never appears in an
44
+ * error message: provider error text is passed through with the key scrubbed.
45
+ *
46
+ * # `GET <base>/models`: two shapes, and only one of them answers the question
47
+ *
48
+ * Measured 2026-09-25, unauthenticated except where noted:
49
+ *
50
+ * | base | status | body |
51
+ * |------------------------------------------|--------|--------------------------------------------------------|
52
+ * | `https://api.typesafe.ai/v1` | 403 | `{detail:{error_type,message}}` |
53
+ * | `https://openrouter.ai/api/v1` | 200 | `{data:[{id,canonical_slug,name,…}]}` — OpenAI shape |
54
+ * | `https://ai-gateway.vercel.sh/typesafe/v1` | 200 | `{models:[{name,description,release_date}]}` — TypeSafe shape |
55
+ * | a LiteLLM proxy (`…/typesafe/v1`) | 401 | `{error:{message,type,param,code}}` |
56
+ * | the same proxy, with a key | 200 | TypeSafe shape: `jev-latest`, `jev-preview` |
57
+ *
58
+ * `readJevModelList` parses both shapes, and `parseJevModelList` reports "could
59
+ * not read the list" rather than throwing, because a proxy may serve neither.
60
+ *
61
+ * The two shapes do not mean the same thing, and only one of them may be used to
62
+ * REFUSE a model (`jev setup`, see `listDescribesSystemOne`):
63
+ *
64
+ * - The TypeSafe shape is the System One inventory of the base it was read from,
65
+ * as ALIASES. Vercel's typesafe-scoped passthrough and a LiteLLM passthrough
66
+ * both return it. It is not exhaustive: the models.aikin.club upstream lists
67
+ * only `jev-latest` and `jev-preview`, yet its `/systemone` answers
68
+ * `jev-1.13.0` (and "Unknown model" for `jev-1.13` or `jev-1.13.5`; measured
69
+ * 2026-09-27). So it may refuse an unlisted alias, never a versioned id.
70
+ * - The OpenAI shape is a gateway's chat-completions catalog, and demonstrably
71
+ * does not enumerate `/systemone`: OpenRouter's 458-model catalog contains no
72
+ * Jev entry of any spelling, while `POST https://openrouter.ai/api/v1/systemone`
73
+ * exists (401, not 404) and this table addresses it as `typesafe/jev-1.13`. So
74
+ * an OpenAI-shaped list is shown to a person and never used to refuse one.
75
+ *
76
+ * # Vercel names the same model twice, once per base
77
+ *
78
+ * `JEV_PROVIDER_DEFAULTS.vercel.model` is `typesafe-ai/jev` while Vercel's own
79
+ * `/typesafe/v1/models` calls it `jev`. Both are real, at two different bases
80
+ * (measured 2026-09-25):
81
+ *
82
+ * - `GET https://ai-gateway.vercel.sh/v1/models` — the gateway's own
83
+ * OpenAI-shaped catalog, 390 models — carries exactly one match:
84
+ * `{id: "typesafe-ai/jev", owned_by: "typesafe-ai", name: "Jev", type: "evaluation"}`.
85
+ * `<owner>/<model>` is how that catalog addresses every model in it
86
+ * (`alibaba/qwen-3-14b`, …), so the longer form is the GATEWAY-level id.
87
+ * - `GET https://ai-gateway.vercel.sh/typesafe/v1/models` — the typesafe-scoped
88
+ * passthrough, which is the base we POST to — reports the provider-native
89
+ * `jev`.
90
+ *
91
+ * Which one `POST /typesafe/v1/systemone` wants cannot be settled from outside:
92
+ * that route validates the BODY before the key (400 `model: Invalid input:
93
+ * expected string, received undefined` with no model, 401 `Authentication
94
+ * failed` with any model, valid or not), so an unauthenticated probe cannot
95
+ * distinguish a model it knows from one it does not. The default is therefore
96
+ * left as it is — it is what the route was configured and verified with — and
97
+ * `modelListHasModel` treats a listed `jev` as covering a configured
98
+ * `typesafe-ai/jev`, since Vercel's two catalogs differ by exactly that
99
+ * `<owner>/` prefix. Settling it needs one authenticated request with each
100
+ * spelling.
101
+ *
102
+ * # Which Jev answered
103
+ *
104
+ * The decision thresholds were calibrated against Jev 1.13, so `readAnswers`
105
+ * accepts an answer only from that family: `jev-1.13.x`, OpenRouter's
106
+ * `typesafe/jev-1.13-<date>` snapshot, or — where the provider reports no
107
+ * version at all (Vercel's `typesafe-ai/jev` or no model, Cloudflare's
108
+ * `typesafe/jev` or no model, a custom proxy echoing back the unversioned name
109
+ * the customer configured for it) — an answer the TRANSPORT marked
110
+ * `modelUnverified`, which the evaluator records as `modelVerified: false`. A
111
+ * reported version of another major.minor is a `model-mismatch`, and the caller
112
+ * falls back to regex; so is a custom endpoint that reports no model at all. The flag is set only by the transports in this file from
113
+ * what the provider reported; a `modelUnverified` field in a response body is
114
+ * never copied through.
115
+ *
116
+ * # Errors
117
+ *
118
+ * Every failure is a `JevError`, and the caller falls back to regex:
119
+ * `timeout`, `network`, `http-<status>` (429 and every 5xx included, and every
120
+ * 3xx: a redirect is never followed, so the answer only ever comes from the
121
+ * configured origin),
122
+ * `out-of-credits` and `provider-refused` (the two meanings of HTTP 402, or of
123
+ * a 402 inside a 200 body — see `paymentRequiredCode`), `upstream-error`,
124
+ * `cloudflare-error`, `cloudflare-incomplete`, `malformed`, `model-mismatch`,
125
+ * `config`.
126
+ *
127
+ * The CODE is the stable part — the activity store keeps a closed list of them
128
+ * (`JEV_REASON_CODE_LIST`, and its twin in `fpai-collect`) — so the provider's
129
+ * own sentence goes in the MESSAGE, which nothing parses.
130
+ * `providerErrorDetail` reads it out of whichever envelope arrived; all three
131
+ * observed shapes are in that comment, and one of them (TypeSafe's
132
+ * `{detail:{message}}`) used to be dropped on the floor, which is how "Must
133
+ * supply an API key!" reached a person as the bare words `HTTP 403`.
134
+ *
135
+ * # When the provider refuses the call
136
+ *
137
+ * A refusal is not spread evenly over the traffic: the provider is likeliest to
138
+ * decline exactly the calls that matter most — the curl that reads like
139
+ * exfiltration, the file listing carrying `rm -rf /` and `dd if=/dev/zero`.
140
+ * Both the judged call and our own question text ride in the request, so either
141
+ * can trip it; one of our own shipped examples did, live (see
142
+ * `download_and_run` in `policies.ts`).
143
+ *
144
+ * The consequence is a security property, not a nicety. A refusal degrades the
145
+ * call to the regex policies — which is the correct fallback: every hard policy
146
+ * still denies, and no reviewable deny is ever cleared by an answer nobody got
147
+ * — but the degrade has to be VISIBLE, because it lands on the worst call of
148
+ * the day rather than a random one. It is visible as `jev-fallback` /
149
+ * `provider-refused` on the activity row and in `failproofai jev status`.
150
+ * Filing it as `out-of-credits` was the opposite of visible: it sent the
151
+ * operator to their billing page while that call went through on regex alone.
152
+ *
153
+ * # Not an opt-in: `resolveJevProvider`
154
+ *
155
+ * The research harness found credentials in `~/.config/typesafe/` (or
156
+ * `FAILPROOFAI_JEV_CONFIG_DIR`, or `TYPESAFE_API_KEY`). That lookup is kept for
157
+ * the harness only. The product's ONLY opt-in is the global
158
+ * `~/.failproofai/jev.json`, read by `loadJevConfig()`; the hook path always
159
+ * builds its transport with `transportForConfig(loadJevConfig())` and never
160
+ * falls back to `resolveJevProvider`.
161
+ *
162
+ * Deliberately raw `fetch` and no SDK: `policy-evaluator.ts` records a single
163
+ * import being weighed in bytes on the hook path.
164
+ */
165
+ import { readFileSync } from "node:fs";
166
+ import { homedir } from "node:os";
167
+ import { resolve } from "node:path";
168
+ // A value, not a literal: the code has to be one the activity store's closed
169
+ // list names, and importing the constant makes a rename there a compile error
170
+ // here instead of a row quietly stored as `other`. `jev-activity.ts` is pure
171
+ // (no node imports, no semantic modules), as `combine.ts` already relies on.
172
+ import { JEV_REASON_PROVIDER_REFUSED } from "../jev-activity";
173
+ import {
174
+ CLOUDFLARE_ACCOUNT_ID_RE,
175
+ isCalibratedJevModel,
176
+ isModelIdShaped,
177
+ jevModelVersion,
178
+ validateLoadedJevConfig,
179
+ type JevConfig,
180
+ type JevProviderKind,
181
+ } from "./jev-config";
182
+ import type { JevRequest, JevResponse } from "./types";
183
+
184
+ export const JEV_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
185
+ /** Cloudflare's name for Jev. Unversioned — see `unwrapCloudflare`. */
186
+ export const CLOUDFLARE_JEV_MODEL = "typesafe/jev";
187
+ /** Vercel AI Gateway's name for Jev. An alias: Vercel reports no version. */
188
+ export const VERCEL_JEV_MODEL = "typesafe-ai/jev";
189
+ export const cloudflareRunEndpoint = (accountId: string, apiBase = JEV_PROVIDER_DEFAULTS.cloudflare.baseUrl): string =>
190
+ `${apiBase.replace(/\/+$/, "")}/accounts/${accountId}/ai/run`;
191
+
192
+ /** $0.042 per million input tokens; output tokens are free. */
193
+ export const JEV_USD_PER_INPUT_TOKEN = 0.042 / 1_000_000;
194
+
195
+ /**
196
+ * Where each provider lives and which model id it knows Jev by. `baseUrl` is the
197
+ * API base: native providers POST to `<baseUrl>/systemone`, Cloudflare to
198
+ * `<baseUrl>/accounts/<id>/ai/run`. A config's `baseUrl` replaces it.
199
+ */
200
+ export const JEV_PROVIDER_DEFAULTS = {
201
+ typesafe: { baseUrl: "https://api.typesafe.ai/v1", model: "jev-1.13.0" },
202
+ openrouter: { baseUrl: "https://openrouter.ai/api/v1", model: "typesafe/jev-1.13" },
203
+ vercel: { baseUrl: "https://ai-gateway.vercel.sh/typesafe/v1", model: VERCEL_JEV_MODEL },
204
+ cloudflare: { baseUrl: "https://api.cloudflare.com/client/v4", model: CLOUDFLARE_JEV_MODEL },
205
+ custom: { baseUrl: null, model: "jev-1.13.0" },
206
+ // No default base: it is the Cloud origin this machine connected to, written
207
+ // into jev.json by `config --token` and checked against the credential's own
208
+ // origin by the loader. The server forces `jev-1.13.0` whatever is sent.
209
+ failproofai: { baseUrl: null, model: "jev-1.13.0" },
210
+ } as const satisfies Record<JevProviderKind, { baseUrl: string | null; model: string }>;
211
+
212
+ /**
213
+ * OpenRouter provider routing: zero-data-retention endpoints only, no training
214
+ * on the data, and no fallback to a different provider or model when the pinned
215
+ * one is down — a fallback answer is exactly the "different model" the version
216
+ * check exists to refuse.
217
+ */
218
+ export const OPENROUTER_PROVIDER_PREFS = { zdr: true, data_collection: "deny", allow_fallbacks: false } as const;
219
+
220
+ export type JevTransport = (request: JevRequest, signal: AbortSignal) => Promise<JevResponse>;
221
+
222
+ export class JevError extends Error {
223
+ /**
224
+ * Short, stable cause used in logs and as the fallback reason: timeout,
225
+ * network, http-<status>, out-of-credits, provider-refused, upstream-error,
226
+ * cloudflare-error, cloudflare-incomplete, malformed, model-mismatch, config.
227
+ */
228
+ readonly code: string;
229
+ /**
230
+ * An HTTP 429's `Retry-After` header, verbatim (null when it sent none).
231
+ * Recorded for every route and read by the FailproofAI Cloud one only (see
232
+ * `cloudRetryAfter`); a BYOK route behaves exactly as it did without it.
233
+ */
234
+ readonly retryAfter: string | null;
235
+ constructor(code: string, message: string, opts: { retryAfter?: string | null } = {}) {
236
+ super(printable(message));
237
+ this.name = "JevError";
238
+ this.code = code;
239
+ this.retryAfter = opts.retryAfter ?? null;
240
+ }
241
+ }
242
+
243
+ // ── Research-harness credentials (NOT an opt-in) ─────────────────────────────
244
+
245
+ export function jevConfigDir(): string {
246
+ return process.env.FAILPROOFAI_JEV_CONFIG_DIR || resolve(homedir(), ".config", "typesafe");
247
+ }
248
+
249
+ function readTrimmed(file: string): string | null {
250
+ try {
251
+ const v = readFileSync(file, "utf8").trim();
252
+ return v.length > 0 ? v : null;
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
257
+
258
+ export type JevProvider =
259
+ | { kind: "typesafe"; apiKey: string }
260
+ | { kind: "cloudflare"; token: string; accountId: string };
261
+
262
+ /**
263
+ * The research harness's credential lookup: a TypeSafe key (env or
264
+ * `api_key`), else a Cloudflare token plus `cloudflare.json`. A Cloudflare
265
+ * account id that is not 32 hex characters is treated as not configured — it
266
+ * is interpolated into a URL path.
267
+ *
268
+ * NOT an opt-in and never consulted on the hook path: the product turns Jev on
269
+ * only through `~/.failproofai/jev.json` (`loadJevConfig`), and the handler
270
+ * always passes an explicit transport from `transportForConfig`.
271
+ */
272
+ export function resolveJevProvider(): JevProvider | null {
273
+ const dir = jevConfigDir();
274
+ const apiKey = process.env.TYPESAFE_API_KEY?.trim() || readTrimmed(resolve(dir, "api_key"));
275
+ if (apiKey) return { kind: "typesafe", apiKey };
276
+ const token = readTrimmed(resolve(dir, "cloudflare_token"));
277
+ if (!token) return null;
278
+ let accountId: unknown;
279
+ try {
280
+ accountId = (JSON.parse(readFileSync(resolve(dir, "cloudflare.json"), "utf8")) as { accountId?: unknown }).accountId;
281
+ } catch {
282
+ return null;
283
+ }
284
+ if (typeof accountId !== "string" || !CLOUDFLARE_ACCOUNT_ID_RE.test(accountId)) return null;
285
+ return { kind: "cloudflare", token, accountId };
286
+ }
287
+
288
+ /** Research harness only; see `resolveJevProvider`. */
289
+ export function transportFor(provider: JevProvider): JevTransport {
290
+ return provider.kind === "typesafe" ? httpTransport(provider.apiKey) : cloudflareTransport(provider.token, provider.accountId);
291
+ }
292
+
293
+ // ── HTTP ─────────────────────────────────────────────────────────────────────
294
+
295
+ const MAX_ERROR_DETAIL = 300;
296
+
297
+ /**
298
+ * The provider's own words about a failure, from whichever envelope it uses,
299
+ * with the key scrubbed. Every shape below was observed live; none is guessed at,
300
+ * because a shape nobody has seen adds a branch that can only ever misread a
301
+ * body some future provider sends.
302
+ *
303
+ * | shape | seen on |
304
+ * |-----------------------------|------------------------------------------------------|
305
+ * | `{errors:[{message}]}` | Cloudflare Workers AI |
306
+ * | `{error:{message}}` | OpenRouter, and LiteLLM-style proxies |
307
+ * | `{error:"…"}` | gateways that report an upstream failure as a string |
308
+ * | `{message}` | Vercel AI Gateway (`400 model: Invalid input: …`) |
309
+ * | `{detail:{message}}` | TypeSafe direct (`403 Must supply an API key!`) |
310
+ * | `{detail:"…"}` | a FastAPI 404 behind a proxy (`Not Found`) |
311
+ *
312
+ * `{detail:{message}}` is the one that was missing, and it is TypeSafe's own —
313
+ * so the provider this whole file exists to talk to was the one provider whose
314
+ * explanation never reached a screen.
315
+ *
316
+ * `{error:"<code>", message:"…"}` is FailproofAI Cloud's own error body (its
317
+ * Jev route's contract, `{"error":"forbidden","message":"… jev:evaluate …"}`).
318
+ * Read as `{error:"…"}` alone it reported the bare word `forbidden` and dropped
319
+ * the sentence naming the missing permission, so when both are strings both
320
+ * are kept, code first.
321
+ */
322
+ /**
323
+ * Provider text with its control characters (C0, DEL, C1) replaced by spaces.
324
+ * Error text is printed to a terminal, where OSC 52 writes the clipboard, OSC 8
325
+ * plants a link and ESC[2J erases the real diagnostic. Every field it is used
326
+ * for is one line.
327
+ */
328
+ export function printable(s: string): string {
329
+ return s.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ");
330
+ }
331
+
332
+ export function providerErrorDetail(body: unknown, secret: string): string {
333
+ const b = body as {
334
+ errors?: Array<{ message?: unknown }>;
335
+ error?: unknown;
336
+ message?: unknown;
337
+ detail?: unknown;
338
+ } | null;
339
+ let detail = "";
340
+ if (b && typeof b === "object") {
341
+ if (Array.isArray(b.errors)) {
342
+ detail = b.errors.map((e) => (typeof e?.message === "string" ? e.message : "")).filter(Boolean).join("; ");
343
+ }
344
+ if (!detail && typeof b.error === "string" && b.error && typeof b.message === "string" && b.message) {
345
+ detail = `${b.error}: ${b.message}`;
346
+ }
347
+ if (!detail) detail = messageOf(b.error);
348
+ if (!detail && typeof b.message === "string") detail = b.message;
349
+ if (!detail) detail = messageOf(b.detail);
350
+ }
351
+ return printable(scrubSecret(detail, secret)).slice(0, MAX_ERROR_DETAIL);
352
+ }
353
+
354
+ /** An envelope member that is either the sentence itself or an object carrying it. */
355
+ function messageOf(value: unknown): string {
356
+ if (typeof value === "string") return value;
357
+ if (value && typeof value === "object" && !Array.isArray(value)) {
358
+ const m = (value as { message?: unknown }).message;
359
+ if (typeof m === "string") return m;
360
+ }
361
+ return "";
362
+ }
363
+
364
+ /**
365
+ * Replace every occurrence of the key. A provider echoing a credential must not
366
+ * put it in a log line. Exported so a caller that prints an error (`jev test`)
367
+ * can scrub it again, whatever path the text took to get there.
368
+ */
369
+ export function scrubSecret(text: string, secret: string): string {
370
+ return secret.length >= 4 ? text.split(secret).join("[key]") : text;
371
+ }
372
+
373
+ /**
374
+ * Cloudflare's wording for a request it would not run the model on, seen live
375
+ * (2026-09-21 to 24) as HTTP 402 with
376
+ * `{"errors":[{"message":"Model execution failed (Payment error)","code":2021}]}`.
377
+ *
378
+ * Matched on the wording, not on the numeric code: 2021 is the only value
379
+ * observed and nothing here knows which other codes Cloudflare files under the
380
+ * same meaning, so testing the code would be a guess where the text is an
381
+ * observation.
382
+ */
383
+ const MODEL_EXECUTION_402_RE = /model execution failed/i;
384
+
385
+ /**
386
+ * Which of the two quite different things an HTTP 402 means here, from the
387
+ * provider's own words (already scrubbed; `""` when it sent none).
388
+ *
389
+ * The two are: the account really has no credits left, and the provider
390
+ * DECLINED TO PROCESS THE REQUEST. The second is not a theory — on the
391
+ * Cloudflare route it is reproducible: one shipped question's example text made
392
+ * every call that selected that policy fail with 402, deterministically, on any
393
+ * state, at well under a request a second, with no quota, size or rate
394
+ * condition in play, and the failures stopped when a few characters of the
395
+ * question changed (`download_and_run` in `policies.ts`). Either half of the
396
+ * request can trip it — our questions or the judged call itself, which is why
397
+ * an agent's own command can cause it.
398
+ *
399
+ * The response is all the evidence there is, and it does not settle the two in
400
+ * general. "Model execution failed (Payment error)" is Cloudflare reporting
401
+ * that the model RUN was refused for a payment-category reason: what we have
402
+ * watched it mean is a content refusal, but a partner-side billing failure
403
+ * would arrive wearing the same wrapper, and nothing in the body tells those
404
+ * apart. So this shape is `provider-refused` — a name that says who refused and
405
+ * that no answer arrived, and accuses the operator's balance under neither
406
+ * reading. Any other 402 (a body that names credits, a non-JSON body, no body)
407
+ * stays `out-of-credits`: the status's own meaning, and the only thing left to
408
+ * call it.
409
+ *
410
+ * The gap that remains: a route refusing content in different words is still
411
+ * filed as `out-of-credits`. A fallback histogram filling with `out-of-credits`
412
+ * while the account demonstrably has money is the sign to sweep the questions
413
+ * again, the way `policies.ts` describes.
414
+ */
415
+ function paymentRequiredCode(detail: string): string {
416
+ return MODEL_EXECUTION_402_RE.test(detail) ? JEV_REASON_PROVIDER_REFUSED : "out-of-credits";
417
+ }
418
+
419
+ /** Whether this URL is one THIS file built by appending its own suffix (see `nativeEndpoint`). */
420
+ function isSystemOneUrl(url: string): boolean {
421
+ try {
422
+ return /\/systemone$/i.test(new URL(url).pathname);
423
+ } catch {
424
+ return false;
425
+ }
426
+ }
427
+
428
+ /**
429
+ * The message for a failed status, which is the provider's own sentence when it
430
+ * sent one — except for a 404 on `<base>/systemone`, which needs saying rather
431
+ * than repeating.
432
+ *
433
+ * `/systemone` is appended here, not typed by the customer, and every provider
434
+ * serves it at its version root: measured 2026-09-25, an unauthenticated POST to
435
+ * that path answers 403 / 401 / 400 / 401 on TypeSafe, OpenRouter, Vercel and a
436
+ * LiteLLM proxy, and 404 on none of them. So a 404 there is near-certain evidence
437
+ * that the BASE is wrong — and the body is no help, because the one seen in the
438
+ * field said `Not Found` and nothing else. The URL is named because it is the
439
+ * constructed one, which is what the person has to compare against what they
440
+ * typed; it goes through `displayEndpoint`, so a base carrying a token in its
441
+ * query string does not put it in an error message.
442
+ */
443
+ function httpFailureMessage(status: number, url: string, detail: string, derivedBase = false): string {
444
+ // A base URL the connection supplied is nobody's typo: a Cloud 404 is a
445
+ // server without the route, which `jev test`'s remedy line says.
446
+ if (status === 404 && derivedBase) return `nothing is served at ${displayEndpoint(url)}${detail ? ` (${detail})` : ""}`;
447
+ if (status === 404 && isSystemOneUrl(url)) {
448
+ // No `HTTP 404:` prefix: every caller prints the code beside the message.
449
+ return (
450
+ `nothing is served at ${displayEndpoint(url)}${detail ? ` (${detail})` : ""}. ` +
451
+ "`/systemone` is appended to the base URL you configured, and every Jev route serves it at the provider's version root — " +
452
+ "so this is a base URL that is wrong, not a provider that is down."
453
+ );
454
+ }
455
+ return detail || `HTTP ${status}`;
456
+ }
457
+
458
+ /** A redirect, including the opaque form a browser-style fetch returns for `redirect: "manual"` (status 0). */
459
+ function isRedirect(res: Response): boolean {
460
+ return res.type === "opaqueredirect" || (res.status >= 300 && res.status < 400);
461
+ }
462
+
463
+ async function postJson(url: string, bearer: string, body: unknown, signal: AbortSignal, derivedBase = false): Promise<unknown> {
464
+ let res: Response;
465
+ try {
466
+ res = await fetch(url, {
467
+ method: "POST",
468
+ headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
469
+ body: JSON.stringify(body),
470
+ signal,
471
+ // Never followed. The configured URL is the one `validateBaseUrl` checked
472
+ // (https, or loopback http in shadow mode only); a redirect would hand the
473
+ // answer — the thing that can clear a deny — to an origin nobody checked,
474
+ // plain http included. No provider redirects this POST.
475
+ redirect: "manual",
476
+ });
477
+ } catch (err) {
478
+ if (signal.aborted) throw new JevError("timeout", "Jev did not answer in time");
479
+ throw new JevError("network", scrubSecret(err instanceof Error ? err.message : String(err), bearer));
480
+ }
481
+ if (isRedirect(res)) {
482
+ try {
483
+ void res.body?.cancel().catch(() => {});
484
+ } catch {
485
+ // The body is irrelevant; freeing it is best effort.
486
+ }
487
+ const status = res.status >= 300 && res.status < 400 ? String(res.status) : "3xx";
488
+ throw new JevError(`http-${status}`, `HTTP ${status}: the endpoint answered with a redirect, which is never followed`);
489
+ }
490
+ // Kept for a 429 and a 503: the statuses whose header says when to ask again.
491
+ const retryAfter = res.status === 429 || res.status === 503 ? res.headers.get("retry-after") : null;
492
+ let parsed: unknown;
493
+ try {
494
+ parsed = await res.json();
495
+ } catch {
496
+ if (signal.aborted) throw new JevError("timeout", "Jev did not answer in time");
497
+ // No provider words to read, so nothing distinguishes a refusal from an
498
+ // empty account: the status's own meaning stands (see `paymentRequiredCode`).
499
+ if (res.status === 402) throw new JevError("out-of-credits", "HTTP 402: the account is out of credits");
500
+ // A 404 needs no body to be diagnosed, and the ones seen in the field carry
501
+ // none worth reading: the URL is the diagnosis (see `httpFailureMessage`).
502
+ if (!res.ok) throw new JevError(`http-${res.status}`, httpFailureMessage(res.status, url, "", derivedBase), { retryAfter });
503
+ throw new JevError("malformed", "response body is not JSON");
504
+ }
505
+ if (res.status === 402) {
506
+ // A refusal is only ever recognised FROM the provider's words, so a 402
507
+ // with none is `out-of-credits` and this message fits it. Anything the
508
+ // provider did say is the message, refusal or not.
509
+ const detail = providerErrorDetail(parsed, bearer);
510
+ throw new JevError(paymentRequiredCode(detail), detail || "HTTP 402: the account is out of credits");
511
+ }
512
+ if (!res.ok) {
513
+ throw new JevError(`http-${res.status}`, httpFailureMessage(res.status, url, providerErrorDetail(parsed, bearer), derivedBase), { retryAfter });
514
+ }
515
+ return parsed;
516
+ }
517
+
518
+ /** A model id that names no version and is one of the names this route is known by. */
519
+ function isAliasFor(reported: string, aliases: readonly string[]): boolean {
520
+ return jevModelVersion(reported) === null && aliases.includes(reported);
521
+ }
522
+
523
+ // ── TypeSafe-native transport (typesafe, openrouter, vercel, custom) ─────────
524
+
525
+ export interface NativeTransportOptions {
526
+ /** The full endpoint URL, e.g. `https://api.typesafe.ai/v1/systemone`. */
527
+ url: string;
528
+ apiKey: string;
529
+ /** Sent as the body's `model`, replacing the request's. Omitted: the request's own. */
530
+ model?: string;
531
+ /** Extra top-level body fields — OpenRouter's `provider` routing. */
532
+ extraBody?: Record<string, unknown>;
533
+ /** Unversioned names this route reports for Jev; answering with one marks the response `modelUnverified`. */
534
+ aliases?: readonly string[];
535
+ /** Whether an answer with no `model` at all is accepted (as unverified) rather than refused. */
536
+ allowUnreported?: boolean;
537
+ /** The base URL came from a connection, not from the person (FailproofAI Cloud). */
538
+ derivedBase?: boolean;
539
+ }
540
+
541
+ /**
542
+ * A provider's body → a Jev response, built field by field so nothing but
543
+ * `model`, `answers` and `usage` crosses over — in particular never a
544
+ * `modelUnverified` the server chose to send.
545
+ */
546
+ function normalizeNative(body: unknown, sentModel: string, opts: NativeTransportOptions): JevResponse {
547
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new JevError("malformed", "Jev returned no object");
548
+ const b = body as { model?: unknown; answers?: unknown; usage?: unknown; error?: unknown };
549
+ if (typeof b.answers !== "object" || b.answers === null) {
550
+ // Gateways sometimes report an upstream failure inside a 200.
551
+ if (b.error !== undefined) {
552
+ const code = typeof b.error === "object" && b.error !== null ? (b.error as { code?: unknown }).code : undefined;
553
+ const detail = providerErrorDetail(body, opts.apiKey) || "the provider reported an error";
554
+ // Same two meanings, one layer in; same rule (see `paymentRequiredCode`).
555
+ if (code === 402 || code === "402") throw new JevError(paymentRequiredCode(detail), detail);
556
+ if (typeof code === "number" && (code === 429 || code >= 500)) throw new JevError(`http-${code}`, detail);
557
+ throw new JevError("upstream-error", detail);
558
+ }
559
+ throw new JevError("malformed", "Jev response has no answers object");
560
+ }
561
+ const usage = b.usage && typeof b.usage === "object" ? (b.usage as JevResponse["usage"]) : undefined;
562
+ // The reported id reaches a `model-mismatch` message, so a server echoing the key there must not carry it through.
563
+ const reported = typeof b.model === "string" && b.model.length > 0 ? scrubSecret(b.model, opts.apiKey) : null;
564
+ let model: string;
565
+ let unverified = false;
566
+ if (reported === null) {
567
+ // Nothing to check. Accepted only where the route is known not to report.
568
+ model = opts.allowUnreported ? sentModel : "";
569
+ unverified = opts.allowUnreported === true;
570
+ } else {
571
+ model = reported;
572
+ unverified = isAliasFor(reported, opts.aliases ?? []);
573
+ }
574
+ return {
575
+ model,
576
+ answers: b.answers as JevResponse["answers"],
577
+ ...(usage ? { usage } : {}),
578
+ ...(unverified ? { modelUnverified: true } : {}),
579
+ };
580
+ }
581
+
582
+ export function nativeTransport(opts: NativeTransportOptions): JevTransport {
583
+ return async (request, signal) => {
584
+ const model = opts.model ?? request.model;
585
+ const body = { ...request, model, ...(opts.extraBody ?? {}) };
586
+ return normalizeNative(await postJson(opts.url, opts.apiKey, body, signal, opts.derivedBase), model, opts);
587
+ };
588
+ }
589
+
590
+ /** TypeSafe direct at its fixed endpoint, sending the request's own model. */
591
+ export function httpTransport(apiKey: string): JevTransport {
592
+ return nativeTransport({ url: JEV_ENDPOINT, apiKey });
593
+ }
594
+
595
+ // ── Cloudflare Workers AI ────────────────────────────────────────────────────
596
+
597
+ export interface CloudflareTransportOptions {
598
+ /** Cloudflare's model id. Default `typesafe/jev`. */
599
+ model?: string;
600
+ /** Replaces `https://api.cloudflare.com/client/v4`; a URL already ending in `/ai/run` is used as is. */
601
+ baseUrl?: string;
602
+ }
603
+
604
+ export function cloudflareTransport(token: string, accountId: string, opts: CloudflareTransportOptions = {}): JevTransport {
605
+ if (!CLOUDFLARE_ACCOUNT_ID_RE.test(accountId)) throw new JevError("config", "Cloudflare account id must be 32 hex characters");
606
+ const model = opts.model ?? CLOUDFLARE_JEV_MODEL;
607
+ const endpoint = cloudflareEndpoint(accountId, opts.baseUrl);
608
+ return async (request, signal) => {
609
+ const body = await postJson(endpoint, token, { model, input: { state: request.state, questions: request.questions } }, signal);
610
+ return unwrapCloudflare(body, request, model, token);
611
+ };
612
+ }
613
+
614
+ function cloudflareEndpoint(accountId: string, baseUrl?: string): string {
615
+ if (!baseUrl) return cloudflareRunEndpoint(accountId);
616
+ const url = new URL(baseUrl);
617
+ if (/\/ai\/run$/.test(url.pathname)) return url.toString();
618
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/accounts/${accountId}/ai/run`;
619
+ return url.toString();
620
+ }
621
+
622
+ /**
623
+ * Cloudflare's `{success, errors, result}` envelope → a Jev response.
624
+ *
625
+ * Cloudflare addresses Jev as `typesafe/jev`, without a version. When the
626
+ * result says which Jev answered, `readAnswers` enforces the family exactly as
627
+ * it does for TypeSafe. When it does not — no `model`, or only the alias echoed
628
+ * back — the version cannot be checked from here: the response carries the
629
+ * request's model so the answers are usable, and `modelUnverified` puts that
630
+ * gap in the verdict log instead of hiding it.
631
+ *
632
+ * `secret` is the token the request was sent with. Every piece of provider
633
+ * text that can reach an error message (`errors[].message`, a job state, a
634
+ * reported model id) is scrubbed of it: a 200 `{success: false}` is not
635
+ * scrubbed by `postJson`, which sees only the status.
636
+ */
637
+ export function unwrapCloudflare(
638
+ body: unknown,
639
+ request: JevRequest,
640
+ sentModel: string = CLOUDFLARE_JEV_MODEL,
641
+ secret = "",
642
+ ): JevResponse {
643
+ const envelope = body as { success?: unknown; errors?: Array<{ message?: unknown }>; result?: unknown } | null;
644
+ if (!envelope || typeof envelope !== "object") throw new JevError("malformed", "Cloudflare returned no object");
645
+ if (envelope.success === false) {
646
+ const detail = Array.isArray(envelope.errors)
647
+ ? envelope.errors.map((e) => (typeof e?.message === "string" ? e.message : "")).filter(Boolean).join("; ")
648
+ : "";
649
+ // Not the refusal path: a declined request arrives as HTTP 402 and never
650
+ // reaches here (see `paymentRequiredCode`). A 200 `{success: false}` is a
651
+ // different failure, and `cloudflare-error` blames nobody's billing either.
652
+ throw new JevError("cloudflare-error", scrubSecret(detail, secret).slice(0, MAX_ERROR_DETAIL) || "Cloudflare reported failure");
653
+ }
654
+ let inner: unknown = "result" in envelope ? envelope.result : envelope;
655
+ // Observed live (2026-09-21): partner models add a job layer —
656
+ // `result: {state: "Completed", result: <TypeSafe's own response>, gatewayMetadata}`.
657
+ // Anything but a completed job is not an answer.
658
+ const job = inner as { state?: unknown; result?: unknown } | null;
659
+ if (job && typeof job === "object" && "state" in job && "result" in job) {
660
+ if (job.state !== "Completed") {
661
+ throw new JevError("cloudflare-incomplete", `Cloudflare job state ${scrubSecret(String(job.state), secret).slice(0, MAX_ERROR_DETAIL)}`);
662
+ }
663
+ inner = job.result;
664
+ }
665
+ const result = inner as Partial<JevResponse> | null;
666
+ if (!result || typeof result !== "object" || typeof result.answers !== "object" || result.answers === null) {
667
+ throw new JevError("malformed", "Cloudflare result has no answers");
668
+ }
669
+ // Echoing back the alias is not a version either.
670
+ const reported =
671
+ typeof result.model === "string" && result.model.length > 0 && !isAliasFor(result.model, [CLOUDFLARE_JEV_MODEL, sentModel])
672
+ ? scrubSecret(result.model, secret)
673
+ : null;
674
+ return {
675
+ model: reported ?? request.model,
676
+ answers: result.answers,
677
+ ...(result.usage ? { usage: result.usage } : {}),
678
+ ...(reported === null ? { modelUnverified: true } : {}),
679
+ };
680
+ }
681
+
682
+ // ── Answers ──────────────────────────────────────────────────────────────────
683
+
684
+ /**
685
+ * Probabilities keyed by question id, or a JevError. Every question must come
686
+ * back as a finite number in [0, 1], and the answer must come from the Jev
687
+ * family the thresholds were calibrated against (1.13, any patch or snapshot),
688
+ * or from a route that reports no version at all — which the transport has
689
+ * marked `modelUnverified`. Thresholds mean nothing for another model.
690
+ */
691
+ export function readAnswers(request: JevRequest, response: JevResponse): Record<string, number> {
692
+ if (!response || typeof response !== "object" || typeof response.answers !== "object" || response.answers === null) {
693
+ throw new JevError("malformed", "Jev response has no answers object");
694
+ }
695
+ if (response.modelUnverified !== true && !isCalibratedJevModel(String(response.model))) {
696
+ const got = typeof response.model === "string" && response.model !== "" ? response.model.slice(0, 200) : "no model id";
697
+ throw new JevError("model-mismatch", `asked for ${request.model}, got ${got}`);
698
+ }
699
+ const out: Record<string, number> = {};
700
+ for (const id of Object.keys(request.questions)) {
701
+ const p = response.answers[id]?.noul;
702
+ if (typeof p !== "number" || !Number.isFinite(p) || p < 0 || p > 1) {
703
+ throw new JevError("malformed", `answer for ${id} is missing or out of range`);
704
+ }
705
+ out[id] = p;
706
+ }
707
+ return out;
708
+ }
709
+
710
+ // ── BYOK (T1 contract) ───────────────────────────────────────────────────────
711
+
712
+ export interface JevRoute {
713
+ via: JevProviderKind;
714
+ /** The URL requests are POSTed to. May carry a query string; show it with `displayEndpoint`. */
715
+ endpoint: string;
716
+ /** The model id sent to the provider. */
717
+ model: string;
718
+ modelIsDefault: boolean;
719
+ }
720
+
721
+ export function nativeEndpoint(baseUrl: string): string {
722
+ const url = new URL(baseUrl);
723
+ if (!/\/systemone$/.test(url.pathname)) url.pathname = `${url.pathname.replace(/\/+$/, "")}/systemone`;
724
+ return url.toString();
725
+ }
726
+
727
+ /**
728
+ * A config as the loader produced it, validated again. `validateLoadedJevConfig`
729
+ * rather than `validateJevConfig`, because the FailproofAI Cloud provider's key
730
+ * is filled in from `credentials.json` and would be refused as a file field.
731
+ */
732
+ function validated(cfg: JevConfig): JevConfig {
733
+ const v = validateLoadedJevConfig(cfg);
734
+ if (!v.ok) throw new JevError("config", v.problem);
735
+ return v.value;
736
+ }
737
+
738
+ /** Where a config sends its requests and which model id it names, without building anything. Throws `JevError("config")`. */
739
+ export function jevRoute(input: JevConfig): JevRoute {
740
+ const cfg = validated(input);
741
+ const defaults = JEV_PROVIDER_DEFAULTS[cfg.provider];
742
+ const model = cfg.model ?? defaults.model;
743
+ const modelIsDefault = cfg.model === undefined;
744
+ if (cfg.provider === "cloudflare") {
745
+ return { via: "cloudflare", endpoint: cloudflareEndpoint(cfg.accountId as string, cfg.baseUrl), model, modelIsDefault };
746
+ }
747
+ const base = cfg.baseUrl ?? defaults.baseUrl;
748
+ if (!base) throw new JevError("config", `provider ${cfg.provider} needs a baseUrl`);
749
+ return { via: cfg.provider, endpoint: nativeEndpoint(base), model, modelIsDefault };
750
+ }
751
+
752
+ /** An endpoint for display: the query string (which may hold anything) replaced by `?…`. */
753
+ export function displayEndpoint(endpoint: string): string {
754
+ try {
755
+ const url = new URL(endpoint);
756
+ const shown = `${url.origin}${url.pathname}`;
757
+ return url.search ? `${shown}?…` : shown;
758
+ } catch {
759
+ return "(invalid URL)";
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Why a URL given where a base belongs is refused, in one sentence shared by
765
+ * `jev setup` and the dashboard's save, built from the transport's own URL so
766
+ * neither describes a request it does not make: a `/systemone` URL is used as
767
+ * it is (never doubled); any other would be asked at `<url>/systemone`.
768
+ */
769
+ export function endpointAsBaseReason(given: string): string {
770
+ const asked = nativeEndpoint(given);
771
+ return asked === new URL(given).toString()
772
+ ? "That is already the Jev endpoint itself: this field takes the base it sits under (where <base>/models is read), and failproofai adds /systemone on its own."
773
+ : `A Jev request goes to <base>/systemone, and failproofai appends that itself, so ${displayEndpoint(given)} would be asked at ${displayEndpoint(asked)}.`;
774
+ }
775
+
776
+ // ── The model list ───────────────────────────────────────────────────────────
777
+
778
+ /**
779
+ * Which envelope a model list arrived in. The difference decides what may be
780
+ * DONE with it, not just how it is parsed — see the header.
781
+ */
782
+ export type JevModelListShape =
783
+ /** `{models:[{name, description?, release_date?}]}` — the System One inventory of this base. */
784
+ | "typesafe"
785
+ /** `{data:[{id, name?}]}` — a gateway's chat catalog, which does not enumerate `/systemone`. */
786
+ | "openai";
787
+
788
+ export interface JevModelListRead {
789
+ ok: true;
790
+ shape: JevModelListShape;
791
+ /** Model ids, in the order the provider gave them, deduplicated. */
792
+ models: string[];
793
+ }
794
+
795
+ export interface JevModelListUnread {
796
+ ok: false;
797
+ /** Why, in words meant for a person. Never a code: nothing stores or branches on this. */
798
+ reason: string;
799
+ /** The HTTP status, when the read got that far. */
800
+ status?: number;
801
+ }
802
+
803
+ export type JevModelListResult = JevModelListRead | JevModelListUnread;
804
+
805
+ /**
806
+ * A model list read is a diagnostic, and the whole point of it is to be
807
+ * available BEFORE a config is written — so it gets its own budget rather than
808
+ * the config's `timeoutMs`, which is sized for the hook path (3000 ms, and every
809
+ * millisecond of it lands on a tool call).
810
+ */
811
+ export const JEV_MODEL_LIST_TIMEOUT_MS = 2_500;
812
+
813
+ /**
814
+ * Names kept from one list. OpenRouter's catalog is 458 entries and a proxy's
815
+ * could be anything; this is a terminal, and a bound here is cheaper than a
816
+ * bound at every place that prints one.
817
+ */
818
+ const MAX_LISTED_MODELS = 200;
819
+
820
+ /**
821
+ * `<base>/models`, or null where there is no such thing.
822
+ *
823
+ * Cloudflare is the null: Workers AI has no `<base>/models` — the run endpoint is
824
+ * `/accounts/<id>/ai/run` and its inventory lives behind a different API — so
825
+ * nothing here can read a list for it, and every caller treats that exactly like
826
+ * a list it failed to read.
827
+ *
828
+ * A base that names the full request URL (`…/v1/systemone`, which `jev setup`
829
+ * now refuses but an older file may carry) still has its version root one
830
+ * segment up, and that is where the list is.
831
+ */
832
+ export function jevModelsUrl(input: JevConfig): string | null {
833
+ const cfg = validated(input);
834
+ // FailproofAI Cloud pins the model server-side and serves no list.
835
+ if (cfg.provider === "cloudflare" || cfg.provider === "failproofai") return null;
836
+ const base = cfg.baseUrl ?? JEV_PROVIDER_DEFAULTS[cfg.provider].baseUrl;
837
+ return base ? modelsUrlForBase(base) : null;
838
+ }
839
+
840
+ /** `<base>/models` for a base URL, keeping any query string the base carried. */
841
+ export function modelsUrlForBase(base: string): string | null {
842
+ try {
843
+ const url = new URL(base);
844
+ url.pathname = `${url.pathname.replace(/\/systemone$/i, "").replace(/\/+$/, "")}/models`;
845
+ return url.toString();
846
+ } catch {
847
+ return null;
848
+ }
849
+ }
850
+
851
+ /**
852
+ * The model ids in a list body, or why it could not be read. Never throws: a
853
+ * customer's proxy may serve neither shape, or an HTML error page, and an
854
+ * unreadable list must leave every caller exactly where it was.
855
+ *
856
+ * `secret` is the key the list was fetched with. Names are scrubbed of it and
857
+ * then kept only if they are SHAPED like model ids, because these strings come
858
+ * from a remote endpoint and are about to be printed on a terminal — a name
859
+ * carrying control characters, or a kilobyte of them, is not a name.
860
+ *
861
+ * A shape that is present but yields no usable name is reported as unread rather
862
+ * than as "this endpoint has no models": an empty list is far likelier to be a
863
+ * shape misread than a provider serving nothing, and "unread" is the direction
864
+ * that changes no behaviour.
865
+ */
866
+ export function parseJevModelList(body: unknown, secret = ""): JevModelListResult {
867
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
868
+ return { ok: false, reason: "the list endpoint did not answer with a JSON object" };
869
+ }
870
+ const b = body as { models?: unknown; data?: unknown };
871
+ for (const [shape, entries, keys] of [
872
+ ["typesafe", b.models, ["name"]],
873
+ ["openai", b.data, ["id", "name"]],
874
+ ] as Array<[JevModelListShape, unknown, string[]]>) {
875
+ if (!Array.isArray(entries)) continue;
876
+ const models = listedModelNames(entries, keys, secret);
877
+ return models.length > 0 ? { ok: true, shape, models } : { ok: false, reason: "the list named no models this build could read" };
878
+ }
879
+ return { ok: false, reason: "the list endpoint answered in a shape this build does not know (neither {models:[…]} nor {data:[…]})" };
880
+ }
881
+
882
+ function listedModelNames(entries: unknown[], keys: string[], secret: string): string[] {
883
+ const out: string[] = [];
884
+ for (const entry of entries) {
885
+ if (!entry || typeof entry !== "object") continue;
886
+ const row = entry as Record<string, unknown>;
887
+ for (const key of keys) {
888
+ const raw = row[key];
889
+ if (typeof raw !== "string" || raw === "") continue;
890
+ const name = scrubSecret(raw, secret);
891
+ if (isModelIdShaped(name) && !out.includes(name)) out.push(name);
892
+ break;
893
+ }
894
+ if (out.length >= MAX_LISTED_MODELS) break;
895
+ }
896
+ return out;
897
+ }
898
+
899
+ /**
900
+ * `GET <base>/models`. Returns a reading or a reason, never a throw, because
901
+ * every caller's fallback is to carry on as if the endpoint served no list.
902
+ *
903
+ * `apiKey` may be null: Vercel's and OpenRouter's lists are public, and reading
904
+ * one with no key is better than not reading it. Redirects are not followed, for
905
+ * the reason `postJson` gives — a list read is less dangerous than an answer, but
906
+ * an endpoint that moves this GET somewhere unchecked has not earned the key that
907
+ * would ride along with it.
908
+ */
909
+ export async function readJevModelList(url: string, apiKey: string | null, signal: AbortSignal): Promise<JevModelListResult> {
910
+ const secret = apiKey ?? "";
911
+ let res: Response;
912
+ try {
913
+ res = await fetch(url, {
914
+ method: "GET",
915
+ headers: { Accept: "application/json", ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },
916
+ signal,
917
+ redirect: "manual",
918
+ });
919
+ } catch (err) {
920
+ if (signal.aborted) return { ok: false, reason: `it did not answer within ${JEV_MODEL_LIST_TIMEOUT_MS} ms` };
921
+ return { ok: false, reason: printable(scrubSecret(err instanceof Error ? err.message : String(err), secret)).slice(0, MAX_ERROR_DETAIL) };
922
+ }
923
+ if (isRedirect(res)) {
924
+ try {
925
+ void res.body?.cancel().catch(() => {});
926
+ } catch {
927
+ // The body is irrelevant; freeing it is best effort.
928
+ }
929
+ return { ok: false, reason: "it answered with a redirect, which is never followed" };
930
+ }
931
+ let parsed: unknown;
932
+ try {
933
+ parsed = await res.json();
934
+ } catch {
935
+ if (signal.aborted) return { ok: false, reason: `it did not answer within ${JEV_MODEL_LIST_TIMEOUT_MS} ms` };
936
+ return { ok: false, reason: `HTTP ${res.status}: it answered with no JSON`, status: res.status };
937
+ }
938
+ if (!res.ok) {
939
+ // The provider's own sentence, from whichever envelope — which for a list
940
+ // read is usually "Must supply an API key!", and is the answer.
941
+ const detail = providerErrorDetail(parsed, secret);
942
+ return { ok: false, reason: detail ? `HTTP ${res.status}: ${detail}` : `HTTP ${res.status}`, status: res.status };
943
+ }
944
+ return parseJevModelList(parsed, secret);
945
+ }
946
+
947
+ /**
948
+ * Whether a reading is authoritative about what `<base>/systemone` accepts, and
949
+ * so may be used to REFUSE a model rather than only to show one.
950
+ *
951
+ * Only the TypeSafe shape is. The evidence is in the header: OpenRouter serves an
952
+ * OpenAI-shaped catalog of 458 models with no Jev entry in it, while its
953
+ * `/systemone` exists and is addressed as `typesafe/jev-1.13` — so refusing on an
954
+ * OpenAI-shaped list would refuse a route that works.
955
+ */
956
+ export function listDescribesSystemOne(list: JevModelListRead): boolean {
957
+ return list.shape === "typesafe";
958
+ }
959
+
960
+ /**
961
+ * Whether a list covers a configured model id.
962
+ *
963
+ * Exact match, or the id without an `<owner>/` prefix. That second case is
964
+ * measured, not a convenience: Vercel names one model `typesafe-ai/jev` in its
965
+ * gateway catalog and `jev` in the typesafe-scoped list at the base we POST to,
966
+ * differing by exactly that prefix (see the header).
967
+ *
968
+ * A versioned id (`jev-1.13.0`) always counts: a list names aliases, and cannot
969
+ * prove one absent (see the header).
970
+ */
971
+ export function modelListHasModel(list: JevModelListRead, model: string): boolean {
972
+ if (jevModelVersion(model) !== null) return true;
973
+ if (list.models.includes(model)) return true;
974
+ const slash = model.lastIndexOf("/");
975
+ return slash > 0 && list.models.includes(model.slice(slash + 1));
976
+ }
977
+
978
+ // ── FailproofAI Cloud: Retry-After ───────────────────────────────────────────
979
+ //
980
+ // FailproofAI Cloud rate-limits Jev per org and globally, and its 429 carries
981
+ // `Retry-After: <seconds to the window's end>` (contract §2). Every call that
982
+ // ignores it spends a round trip — up to the hook's whole timeout, on the tool
983
+ // call's critical path — to learn the same 429, and adds to the load that
984
+ // caused it. So after a 429 the Cloud route goes quiet for as long as the
985
+ // server asked: calls fall back at once with the same `http-429`, and send
986
+ // nothing.
987
+ //
988
+ // Module-level, like the throttle's cache and bucket (`jev-throttle.ts`): the
989
+ // daemon's warm worker builds a new transport per hook event and lives for
990
+ // hours, so the process is what has to remember. Keyed by the endpoint, so a
991
+ // reconnect to another Cloud is not held to the old one's window.
992
+ //
993
+ // The Cloud route only. A BYOK provider's 429 keeps doing exactly what it did
994
+ // (the throttle empties its bucket), because nothing here knows what its
995
+ // `Retry-After` means or whether it sends one.
996
+
997
+ /** The longest cool-down a Retry-After can set: a hook never goes quiet for longer on the server's say-so. */
998
+ export const JEV_CLOUD_RETRY_AFTER_CAP_MS = 60_000;
999
+ /** The cool-down after a 429 whose Retry-After is missing or unreadable. */
1000
+ export const JEV_CLOUD_RETRY_AFTER_DEFAULT_MS = 5_000;
1001
+
1002
+ const cloudCooldown = { endpoint: "", until: Number.NEGATIVE_INFINITY, code: "http-429" };
1003
+
1004
+ /** Forget any cool-down. For tests, which share this module's state within a file. */
1005
+ export function resetJevCloudCooldown(): void {
1006
+ cloudCooldown.endpoint = "";
1007
+ cloudCooldown.until = Number.NEGATIVE_INFINITY;
1008
+ cloudCooldown.code = "http-429";
1009
+ }
1010
+
1011
+ /** RFC 9110's IMF-fixdate, the one HTTP-date form a sender generates: `Sun, 06 Nov 1994 08:49:37 GMT`. */
1012
+ const IMF_FIXDATE_RE = /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
1013
+
1014
+ /**
1015
+ * How long a `Retry-After` asks for, in ms, within [0, cap]. Delay-seconds
1016
+ * (what FailproofAI Cloud sends) or an IMF-fixdate; anything else — absent,
1017
+ * negative, fractional, garbage — is the small default. Matched by shape
1018
+ * before `Date.parse` sees it, because that parser reads `-5` or `1.5` as a
1019
+ * date in the past, which would be no cool-down at all.
1020
+ */
1021
+ export function retryAfterMs(header: string | null | undefined, nowEpochMs: number = Date.now()): number {
1022
+ const raw = header?.trim() ?? "";
1023
+ let ms: number;
1024
+ if (/^\d{1,10}$/.test(raw)) {
1025
+ ms = Number(raw) * 1000;
1026
+ } else if (IMF_FIXDATE_RE.test(raw) && Number.isFinite(Date.parse(raw))) {
1027
+ ms = Date.parse(raw) - nowEpochMs;
1028
+ } else {
1029
+ return JEV_CLOUD_RETRY_AFTER_DEFAULT_MS;
1030
+ }
1031
+ return Math.min(JEV_CLOUD_RETRY_AFTER_CAP_MS, Math.max(0, ms));
1032
+ }
1033
+
1034
+ /**
1035
+ * The Cloud transport, quiet for as long as the last 429's Retry-After asked —
1036
+ * and after a 503 too. The server's 503 is an operator state (no model
1037
+ * gateway, an org not provisioned yet, the gateway down) that waiting does not
1038
+ * fix, so without a Retry-After it holds the longest this ever does, and a held
1039
+ * call keeps the code that started it rather than reading as a rate limit.
1040
+ */
1041
+ function cloudRetryAfter(endpoint: string, transport: JevTransport): JevTransport {
1042
+ return async (request, signal) => {
1043
+ const now = performance.now();
1044
+ if (cloudCooldown.endpoint === endpoint && now < cloudCooldown.until) {
1045
+ const seconds = Math.max(1, Math.ceil((cloudCooldown.until - now) / 1000));
1046
+ throw new JevError(
1047
+ cloudCooldown.code,
1048
+ cloudCooldown.code === "http-503"
1049
+ ? `FailproofAI Cloud could not serve Jev; asking again in ${seconds}s, so this one was not sent`
1050
+ : `FailproofAI Cloud asked for no Jev requests for ${seconds}s more (Retry-After), so this one was not sent`,
1051
+ );
1052
+ }
1053
+ try {
1054
+ return await transport(request, signal);
1055
+ } catch (err) {
1056
+ if (err instanceof JevError && (err.code === "http-429" || err.code === "http-503")) {
1057
+ cloudCooldown.endpoint = endpoint;
1058
+ cloudCooldown.code = err.code;
1059
+ cloudCooldown.until =
1060
+ performance.now() +
1061
+ (err.code === "http-503" && err.retryAfter === null ? JEV_CLOUD_RETRY_AFTER_CAP_MS : retryAfterMs(err.retryAfter));
1062
+ }
1063
+ throw err;
1064
+ }
1065
+ };
1066
+ }
1067
+
1068
+ /**
1069
+ * The transport for a customer's own Jev config, which provider it goes
1070
+ * through, and the model id to put in the request (pass it to the evaluator as
1071
+ * `model`). The transport sends the configured model regardless, so nothing in
1072
+ * the environment can change which model a provider is asked for.
1073
+ *
1074
+ * Throws `JevError("config")` for a config that would not pass
1075
+ * `loadJevConfig()`; a caller treats that like any other Jev failure.
1076
+ */
1077
+ export function transportForConfig(input: JevConfig): { transport: JevTransport; via: JevProviderKind; model: string } {
1078
+ const cfg = validated(input);
1079
+ const route = jevRoute(cfg);
1080
+ switch (cfg.provider) {
1081
+ case "cloudflare":
1082
+ return {
1083
+ transport: cloudflareTransport(cfg.apiKey, cfg.accountId as string, { model: route.model, baseUrl: cfg.baseUrl }),
1084
+ via: "cloudflare",
1085
+ model: route.model,
1086
+ };
1087
+ case "typesafe":
1088
+ return {
1089
+ transport: nativeTransport({ url: route.endpoint, apiKey: cfg.apiKey, model: route.model }),
1090
+ via: "typesafe",
1091
+ model: route.model,
1092
+ };
1093
+ case "openrouter":
1094
+ return {
1095
+ transport: nativeTransport({
1096
+ url: route.endpoint,
1097
+ apiKey: cfg.apiKey,
1098
+ model: route.model,
1099
+ extraBody: { provider: { ...OPENROUTER_PROVIDER_PREFS } },
1100
+ }),
1101
+ via: "openrouter",
1102
+ model: route.model,
1103
+ };
1104
+ case "vercel":
1105
+ return {
1106
+ transport: nativeTransport({
1107
+ url: route.endpoint,
1108
+ apiKey: cfg.apiKey,
1109
+ model: route.model,
1110
+ aliases: [VERCEL_JEV_MODEL, route.model],
1111
+ allowUnreported: true,
1112
+ }),
1113
+ via: "vercel",
1114
+ model: route.model,
1115
+ };
1116
+ case "custom":
1117
+ return {
1118
+ transport: nativeTransport({
1119
+ url: route.endpoint,
1120
+ apiKey: cfg.apiKey,
1121
+ model: route.model,
1122
+ // A proxy echoing back an UNVERSIONED name the customer configured for
1123
+ // it (`--model house-jev`) has said nothing about the version, the way
1124
+ // Vercel's alias has not; that answer is used and marked unverified.
1125
+ // One reporting a real Jev version is checked. One reporting no model
1126
+ // at all is refused: unlike Vercel and Cloudflare, nothing is known
1127
+ // about what sits behind a custom URL, so silence is not accepted.
1128
+ aliases: [route.model],
1129
+ allowUnreported: false,
1130
+ }),
1131
+ via: "custom",
1132
+ model: route.model,
1133
+ };
1134
+ case "failproofai":
1135
+ return {
1136
+ transport: cloudRetryAfter(
1137
+ route.endpoint,
1138
+ nativeTransport({
1139
+ url: route.endpoint,
1140
+ apiKey: cfg.apiKey,
1141
+ model: route.model,
1142
+ // FailproofAI Cloud forces `jev-1.13.0` server-side and passes
1143
+ // TypeSafe's answer through untouched, `model` included — so an
1144
+ // answer that names no model, or an alias, is not one this route
1145
+ // produces, and is refused (`model-mismatch`) rather than trusted.
1146
+ // `readAnswers` then holds the reported id to the 1.13 family.
1147
+ allowUnreported: false,
1148
+ derivedBase: true,
1149
+ }),
1150
+ ),
1151
+ via: "failproofai",
1152
+ model: route.model,
1153
+ };
1154
+ }
1155
+ }