@yolk-sdk/emulators 0.1.0-canary.96

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 (333) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2135 -0
  3. package/dist/anthropic.d.mts +269 -0
  4. package/dist/anthropic.d.mts.map +1 -0
  5. package/dist/anthropic.mjs +177 -0
  6. package/dist/anthropic.mjs.map +1 -0
  7. package/dist/chat-completions.d.mts +288 -0
  8. package/dist/chat-completions.d.mts.map +1 -0
  9. package/dist/chat-completions.mjs +451 -0
  10. package/dist/chat-completions.mjs.map +1 -0
  11. package/dist/codex.d.mts +265 -0
  12. package/dist/codex.d.mts.map +1 -0
  13. package/dist/codex.mjs +199 -0
  14. package/dist/codex.mjs.map +1 -0
  15. package/dist/dropbox/api.d.mts +63 -0
  16. package/dist/dropbox/api.d.mts.map +1 -0
  17. package/dist/dropbox/api.mjs +565 -0
  18. package/dist/dropbox/api.mjs.map +1 -0
  19. package/dist/dropbox/state.d.mts +136 -0
  20. package/dist/dropbox/state.d.mts.map +1 -0
  21. package/dist/dropbox/state.mjs +209 -0
  22. package/dist/dropbox/state.mjs.map +1 -0
  23. package/dist/dropbox.d.mts +79 -0
  24. package/dist/dropbox.d.mts.map +1 -0
  25. package/dist/dropbox.mjs +124 -0
  26. package/dist/dropbox.mjs.map +1 -0
  27. package/dist/email-fixtures.d.mts +36 -0
  28. package/dist/email-fixtures.d.mts.map +1 -0
  29. package/dist/email-fixtures.mjs +1080 -0
  30. package/dist/email-fixtures.mjs.map +1 -0
  31. package/dist/email.d.mts +160 -0
  32. package/dist/email.d.mts.map +1 -0
  33. package/dist/email.mjs +608 -0
  34. package/dist/email.mjs.map +1 -0
  35. package/dist/emulator-compose.d.mts +40 -0
  36. package/dist/emulator-compose.d.mts.map +1 -0
  37. package/dist/emulator-compose.mjs +91 -0
  38. package/dist/emulator-compose.mjs.map +1 -0
  39. package/dist/emulator-http.d.mts +53 -0
  40. package/dist/emulator-http.d.mts.map +1 -0
  41. package/dist/emulator-http.mjs +117 -0
  42. package/dist/emulator-http.mjs.map +1 -0
  43. package/dist/emulator-kernel.d.mts +176 -0
  44. package/dist/emulator-kernel.d.mts.map +1 -0
  45. package/dist/emulator-kernel.mjs +413 -0
  46. package/dist/emulator-kernel.mjs.map +1 -0
  47. package/dist/fixture-route.d.mts +129 -0
  48. package/dist/fixture-route.d.mts.map +1 -0
  49. package/dist/fixture-route.mjs +257 -0
  50. package/dist/fixture-route.mjs.map +1 -0
  51. package/dist/fortnox/api.d.mts +92 -0
  52. package/dist/fortnox/api.d.mts.map +1 -0
  53. package/dist/fortnox/api.mjs +750 -0
  54. package/dist/fortnox/api.mjs.map +1 -0
  55. package/dist/fortnox/state.d.mts +533 -0
  56. package/dist/fortnox/state.d.mts.map +1 -0
  57. package/dist/fortnox/state.mjs +612 -0
  58. package/dist/fortnox/state.mjs.map +1 -0
  59. package/dist/fortnox.d.mts +128 -0
  60. package/dist/fortnox.d.mts.map +1 -0
  61. package/dist/fortnox.mjs +403 -0
  62. package/dist/fortnox.mjs.map +1 -0
  63. package/dist/gateway-evaluate-recordings.d.mts +12 -0
  64. package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
  65. package/dist/gateway-evaluate-recordings.mjs +128 -0
  66. package/dist/gateway-evaluate-recordings.mjs.map +1 -0
  67. package/dist/gateway.d.mts +272 -0
  68. package/dist/gateway.d.mts.map +1 -0
  69. package/dist/gateway.mjs +400 -0
  70. package/dist/gateway.mjs.map +1 -0
  71. package/dist/github/api.d.mts +63 -0
  72. package/dist/github/api.d.mts.map +1 -0
  73. package/dist/github/api.mjs +527 -0
  74. package/dist/github/api.mjs.map +1 -0
  75. package/dist/github/state.d.mts +272 -0
  76. package/dist/github/state.d.mts.map +1 -0
  77. package/dist/github/state.mjs +303 -0
  78. package/dist/github/state.mjs.map +1 -0
  79. package/dist/github.d.mts +77 -0
  80. package/dist/github.d.mts.map +1 -0
  81. package/dist/github.mjs +180 -0
  82. package/dist/github.mjs.map +1 -0
  83. package/dist/google/calendar.d.mts +10 -0
  84. package/dist/google/calendar.d.mts.map +1 -0
  85. package/dist/google/calendar.mjs +252 -0
  86. package/dist/google/calendar.mjs.map +1 -0
  87. package/dist/google/drive.d.mts +8 -0
  88. package/dist/google/drive.d.mts.map +1 -0
  89. package/dist/google/drive.mjs +252 -0
  90. package/dist/google/drive.mjs.map +1 -0
  91. package/dist/google/gmail.d.mts +10 -0
  92. package/dist/google/gmail.d.mts.map +1 -0
  93. package/dist/google/gmail.mjs +591 -0
  94. package/dist/google/gmail.mjs.map +1 -0
  95. package/dist/google/shared.d.mts +107 -0
  96. package/dist/google/shared.d.mts.map +1 -0
  97. package/dist/google/shared.mjs +146 -0
  98. package/dist/google/shared.mjs.map +1 -0
  99. package/dist/google/state.d.mts +385 -0
  100. package/dist/google/state.d.mts.map +1 -0
  101. package/dist/google/state.mjs +496 -0
  102. package/dist/google/state.mjs.map +1 -0
  103. package/dist/google.d.mts +75 -0
  104. package/dist/google.d.mts.map +1 -0
  105. package/dist/google.mjs +210 -0
  106. package/dist/google.mjs.map +1 -0
  107. package/dist/linkedin-search/api.d.mts +45 -0
  108. package/dist/linkedin-search/api.d.mts.map +1 -0
  109. package/dist/linkedin-search/api.mjs +181 -0
  110. package/dist/linkedin-search/api.mjs.map +1 -0
  111. package/dist/linkedin-search/state.d.mts +123 -0
  112. package/dist/linkedin-search/state.d.mts.map +1 -0
  113. package/dist/linkedin-search/state.mjs +261 -0
  114. package/dist/linkedin-search/state.mjs.map +1 -0
  115. package/dist/linkedin-search.d.mts +69 -0
  116. package/dist/linkedin-search.d.mts.map +1 -0
  117. package/dist/linkedin-search.mjs +158 -0
  118. package/dist/linkedin-search.mjs.map +1 -0
  119. package/dist/mcp/api.d.mts +44 -0
  120. package/dist/mcp/api.d.mts.map +1 -0
  121. package/dist/mcp/api.mjs +557 -0
  122. package/dist/mcp/api.mjs.map +1 -0
  123. package/dist/mcp/recordings.d.mts +40 -0
  124. package/dist/mcp/recordings.d.mts.map +1 -0
  125. package/dist/mcp/recordings.mjs +2390 -0
  126. package/dist/mcp/recordings.mjs.map +1 -0
  127. package/dist/mcp/state.d.mts +71 -0
  128. package/dist/mcp/state.d.mts.map +1 -0
  129. package/dist/mcp/state.mjs +83 -0
  130. package/dist/mcp/state.mjs.map +1 -0
  131. package/dist/mcp.d.mts +84 -0
  132. package/dist/mcp.d.mts.map +1 -0
  133. package/dist/mcp.mjs +241 -0
  134. package/dist/mcp.mjs.map +1 -0
  135. package/dist/messages.d.mts +183 -0
  136. package/dist/messages.d.mts.map +1 -0
  137. package/dist/messages.mjs +532 -0
  138. package/dist/messages.mjs.map +1 -0
  139. package/dist/microsoft/api.d.mts +47 -0
  140. package/dist/microsoft/api.d.mts.map +1 -0
  141. package/dist/microsoft/api.mjs +178 -0
  142. package/dist/microsoft/api.mjs.map +1 -0
  143. package/dist/microsoft/calendar.d.mts +30 -0
  144. package/dist/microsoft/calendar.d.mts.map +1 -0
  145. package/dist/microsoft/calendar.mjs +271 -0
  146. package/dist/microsoft/calendar.mjs.map +1 -0
  147. package/dist/microsoft/drive.d.mts +36 -0
  148. package/dist/microsoft/drive.d.mts.map +1 -0
  149. package/dist/microsoft/drive.mjs +298 -0
  150. package/dist/microsoft/drive.mjs.map +1 -0
  151. package/dist/microsoft/graph.d.mts +198 -0
  152. package/dist/microsoft/graph.d.mts.map +1 -0
  153. package/dist/microsoft/graph.mjs +264 -0
  154. package/dist/microsoft/graph.mjs.map +1 -0
  155. package/dist/microsoft/mail.d.mts +48 -0
  156. package/dist/microsoft/mail.d.mts.map +1 -0
  157. package/dist/microsoft/mail.mjs +488 -0
  158. package/dist/microsoft/mail.mjs.map +1 -0
  159. package/dist/microsoft/state.d.mts +480 -0
  160. package/dist/microsoft/state.d.mts.map +1 -0
  161. package/dist/microsoft/state.mjs +625 -0
  162. package/dist/microsoft/state.mjs.map +1 -0
  163. package/dist/microsoft.d.mts +168 -0
  164. package/dist/microsoft.d.mts.map +1 -0
  165. package/dist/microsoft.mjs +483 -0
  166. package/dist/microsoft.mjs.map +1 -0
  167. package/dist/node.d.mts +47 -0
  168. package/dist/node.d.mts.map +1 -0
  169. package/dist/node.mjs +178 -0
  170. package/dist/node.mjs.map +1 -0
  171. package/dist/notion/api.d.mts +51 -0
  172. package/dist/notion/api.d.mts.map +1 -0
  173. package/dist/notion/api.mjs +507 -0
  174. package/dist/notion/api.mjs.map +1 -0
  175. package/dist/notion/state.d.mts +329 -0
  176. package/dist/notion/state.d.mts.map +1 -0
  177. package/dist/notion/state.mjs +432 -0
  178. package/dist/notion/state.mjs.map +1 -0
  179. package/dist/notion.d.mts +72 -0
  180. package/dist/notion.d.mts.map +1 -0
  181. package/dist/notion.mjs +127 -0
  182. package/dist/notion.mjs.map +1 -0
  183. package/dist/openai.d.mts +165 -0
  184. package/dist/openai.d.mts.map +1 -0
  185. package/dist/openai.mjs +141 -0
  186. package/dist/openai.mjs.map +1 -0
  187. package/dist/opencode-recordings.d.mts +7 -0
  188. package/dist/opencode-recordings.d.mts.map +1 -0
  189. package/dist/opencode-recordings.mjs +222 -0
  190. package/dist/opencode-recordings.mjs.map +1 -0
  191. package/dist/opencode.d.mts +70 -0
  192. package/dist/opencode.d.mts.map +1 -0
  193. package/dist/opencode.mjs +217 -0
  194. package/dist/opencode.mjs.map +1 -0
  195. package/dist/r2-fixtures.d.mts +36 -0
  196. package/dist/r2-fixtures.d.mts.map +1 -0
  197. package/dist/r2-fixtures.mjs +245 -0
  198. package/dist/r2-fixtures.mjs.map +1 -0
  199. package/dist/r2-guard.d.mts +43 -0
  200. package/dist/r2-guard.d.mts.map +1 -0
  201. package/dist/r2-guard.mjs +215 -0
  202. package/dist/r2-guard.mjs.map +1 -0
  203. package/dist/r2.d.mts +166 -0
  204. package/dist/r2.d.mts.map +1 -0
  205. package/dist/r2.mjs +517 -0
  206. package/dist/r2.mjs.map +1 -0
  207. package/dist/responses.d.mts +231 -0
  208. package/dist/responses.d.mts.map +1 -0
  209. package/dist/responses.mjs +556 -0
  210. package/dist/responses.mjs.map +1 -0
  211. package/dist/route-evidence.d.mts +45 -0
  212. package/dist/route-evidence.d.mts.map +1 -0
  213. package/dist/route-evidence.mjs +56 -0
  214. package/dist/route-evidence.mjs.map +1 -0
  215. package/dist/router.d.mts +93 -0
  216. package/dist/router.d.mts.map +1 -0
  217. package/dist/router.mjs +259 -0
  218. package/dist/router.mjs.map +1 -0
  219. package/dist/stateful-core.d.mts +17 -0
  220. package/dist/stateful-core.d.mts.map +1 -0
  221. package/dist/stateful-core.mjs +41 -0
  222. package/dist/stateful-core.mjs.map +1 -0
  223. package/dist/stateful-emulator.d.mts +565 -0
  224. package/dist/stateful-emulator.d.mts.map +1 -0
  225. package/dist/stateful-emulator.mjs +1228 -0
  226. package/dist/stateful-emulator.mjs.map +1 -0
  227. package/dist/stateful-secrets.d.mts +84 -0
  228. package/dist/stateful-secrets.d.mts.map +1 -0
  229. package/dist/stateful-secrets.mjs +216 -0
  230. package/dist/stateful-secrets.mjs.map +1 -0
  231. package/dist/subscription-usage-recordings.d.mts +9 -0
  232. package/dist/subscription-usage-recordings.d.mts.map +1 -0
  233. package/dist/subscription-usage-recordings.mjs +53 -0
  234. package/dist/subscription-usage-recordings.mjs.map +1 -0
  235. package/dist/subscription-usage.d.mts +53 -0
  236. package/dist/subscription-usage.d.mts.map +1 -0
  237. package/dist/subscription-usage.mjs +21 -0
  238. package/dist/subscription-usage.mjs.map +1 -0
  239. package/dist/telegram/api.d.mts +34 -0
  240. package/dist/telegram/api.d.mts.map +1 -0
  241. package/dist/telegram/api.mjs +257 -0
  242. package/dist/telegram/api.mjs.map +1 -0
  243. package/dist/telegram/state.d.mts +128 -0
  244. package/dist/telegram/state.d.mts.map +1 -0
  245. package/dist/telegram/state.mjs +154 -0
  246. package/dist/telegram/state.mjs.map +1 -0
  247. package/dist/telegram.d.mts +62 -0
  248. package/dist/telegram.d.mts.map +1 -0
  249. package/dist/telegram.mjs +127 -0
  250. package/dist/telegram.mjs.map +1 -0
  251. package/dist/todoist/api.d.mts +55 -0
  252. package/dist/todoist/api.d.mts.map +1 -0
  253. package/dist/todoist/api.mjs +415 -0
  254. package/dist/todoist/api.mjs.map +1 -0
  255. package/dist/todoist/state.d.mts +275 -0
  256. package/dist/todoist/state.d.mts.map +1 -0
  257. package/dist/todoist/state.mjs +345 -0
  258. package/dist/todoist/state.mjs.map +1 -0
  259. package/dist/todoist.d.mts +68 -0
  260. package/dist/todoist.d.mts.map +1 -0
  261. package/dist/todoist.mjs +181 -0
  262. package/dist/todoist.mjs.map +1 -0
  263. package/dist/xai.d.mts +267 -0
  264. package/dist/xai.d.mts.map +1 -0
  265. package/dist/xai.mjs +252 -0
  266. package/dist/xai.mjs.map +1 -0
  267. package/package.json +157 -0
  268. package/src/anthropic.ts +289 -0
  269. package/src/chat-completions.ts +924 -0
  270. package/src/codex.ts +296 -0
  271. package/src/dropbox/api.ts +1084 -0
  272. package/src/dropbox/state.ts +364 -0
  273. package/src/dropbox.ts +203 -0
  274. package/src/email-fixtures.ts +1297 -0
  275. package/src/email.ts +1108 -0
  276. package/src/emulator-compose.ts +147 -0
  277. package/src/emulator-http.ts +184 -0
  278. package/src/emulator-kernel.ts +844 -0
  279. package/src/fixture-route.ts +561 -0
  280. package/src/fortnox/api.ts +1352 -0
  281. package/src/fortnox/state.ts +798 -0
  282. package/src/fortnox.ts +801 -0
  283. package/src/gateway-evaluate-recordings.ts +153 -0
  284. package/src/gateway.ts +530 -0
  285. package/src/github/api.ts +986 -0
  286. package/src/github/state.ts +439 -0
  287. package/src/github.ts +271 -0
  288. package/src/google/calendar.ts +486 -0
  289. package/src/google/drive.ts +471 -0
  290. package/src/google/gmail.ts +1011 -0
  291. package/src/google/shared.ts +321 -0
  292. package/src/google/state.ts +686 -0
  293. package/src/google.ts +298 -0
  294. package/src/linkedin-search/api.ts +363 -0
  295. package/src/linkedin-search/state.ts +373 -0
  296. package/src/linkedin-search.ts +241 -0
  297. package/src/mcp/api.ts +995 -0
  298. package/src/mcp/recordings.ts +2665 -0
  299. package/src/mcp/state.ts +125 -0
  300. package/src/mcp.ts +319 -0
  301. package/src/messages.ts +844 -0
  302. package/src/microsoft/api.ts +426 -0
  303. package/src/microsoft/calendar.ts +491 -0
  304. package/src/microsoft/drive.ts +541 -0
  305. package/src/microsoft/graph.ts +550 -0
  306. package/src/microsoft/mail.ts +833 -0
  307. package/src/microsoft/state.ts +822 -0
  308. package/src/microsoft.ts +982 -0
  309. package/src/node.ts +293 -0
  310. package/src/notion/api.ts +976 -0
  311. package/src/notion/state.ts +512 -0
  312. package/src/notion.ts +210 -0
  313. package/src/openai.ts +198 -0
  314. package/src/opencode-recordings.ts +257 -0
  315. package/src/opencode.ts +271 -0
  316. package/src/r2-fixtures.ts +295 -0
  317. package/src/r2-guard.ts +323 -0
  318. package/src/r2.ts +890 -0
  319. package/src/responses.ts +901 -0
  320. package/src/route-evidence.ts +90 -0
  321. package/src/router.ts +490 -0
  322. package/src/stateful-core.ts +70 -0
  323. package/src/stateful-emulator.ts +2571 -0
  324. package/src/stateful-secrets.ts +299 -0
  325. package/src/subscription-usage-recordings.ts +77 -0
  326. package/src/subscription-usage.ts +68 -0
  327. package/src/telegram/api.ts +437 -0
  328. package/src/telegram/state.ts +229 -0
  329. package/src/telegram.ts +227 -0
  330. package/src/todoist/api.ts +736 -0
  331. package/src/todoist/state.ts +456 -0
  332. package/src/todoist.ts +316 -0
  333. package/src/xai.ts +341 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulator-kernel.mjs","names":[],"sources":["../src/emulator-kernel.ts"],"sourcesContent":["/**\n * Shared emulator kernel (internal; not a package export).\n *\n * The wire-independent half of every fetch-handler emulator: fault and scripted-turn state, the\n * request ledger, pull-driven bodies with chunk faults, evidence tagging, route binding, and the\n * `/_emulate/*` control plane. Each wire core (`chat-completions.ts`, `messages.ts`) supplies its\n * route handlers, fault and turn schemas, ledger entry shape, and error bodies.\n *\n * Also holds small wire-independent helpers both cores use: JSON field access, synthetic values\n * for a tool's JSON Schema, fragment splitting, and a rough token estimate.\n *\n * Runtime-portable Web APIs only (`Request`, `Response`, `ReadableStream`, `TextEncoder`, `URL`);\n * no Node builtins and no SDK imports.\n */\nimport { Predicate, Result } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport { EmulatorHeaderRecord, EmulatorResponseStatus } from './emulator-http.ts'\nimport {\n bindRouteHandlers,\n emulatorEvidenceHeader,\n type EmulatorEvidence,\n type EmulatorRouteEvidence\n} from './route-evidence.ts'\n\nexport const ChunkCount = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))\n\nconst FaultCount = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))\n\n/** Optional fault filter; an omitted field matches every request. `path` ending in `*` is a prefix. */\nexport const EmulatorFaultMatch = Schema.Struct({\n path: Schema.optionalKey(Schema.String),\n model: Schema.optionalKey(Schema.String)\n})\n\nexport type EmulatorFaultMatch = typeof EmulatorFaultMatch.Type\n\n/** Fields every fault kind carries. */\nexport const emulatorFaultFields = {\n match: Schema.optionalKey(EmulatorFaultMatch),\n /** How many matching requests the fault answers. Omitted: every matching request. */\n count: Schema.optionalKey(FaultCount)\n}\n\n/** Answer with this status, headers, and body instead of the planned response. */\nexport const EmulatorStatusFault = Schema.Struct({\n kind: Schema.Literal('status'),\n status: EmulatorResponseStatus,\n headers: Schema.optionalKey(EmulatorHeaderRecord),\n body: Schema.optionalKey(Schema.Json),\n ...emulatorFaultFields\n})\n\n/** Send `chunks` body chunks, then error the body stream (a dropped connection). */\nexport const EmulatorErrorAfterChunksFault = Schema.Struct({\n kind: Schema.Literal('error-after-chunks'),\n chunks: ChunkCount,\n ...emulatorFaultFields\n})\n\n/** Send `chunks` body chunks, then close the body cleanly (a truncated response). */\nexport const EmulatorTruncateAfterChunksFault = Schema.Struct({\n kind: Schema.Literal('truncate-after-chunks'),\n chunks: ChunkCount,\n ...emulatorFaultFields\n})\n\n/** A scripted error response: status, body (a string is sent as is), and optional headers. */\nexport const EmulatorScriptedError = Schema.Struct({\n error: Schema.Struct({\n status: EmulatorResponseStatus,\n body: Schema.Json,\n headers: Schema.optionalKey(EmulatorHeaderRecord)\n })\n})\n\nexport type EmulatorScriptedError = typeof EmulatorScriptedError.Type\n\n/** What the kernel needs from every fault: its kind and optional filter and count. */\nexport type KernelFault = {\n readonly kind: string\n readonly match?: EmulatorFaultMatch | undefined\n readonly count?: number | undefined\n}\n\ntype BodyChunkFault =\n | typeof EmulatorErrorAfterChunksFault.Type\n | typeof EmulatorTruncateAfterChunksFault.Type\n\nexport type EmulatorFaultState<Fault> = {\n readonly id: number\n readonly fault: Fault\n /** Remaining matching requests; `undefined` for an unlimited fault. */\n readonly remaining: number | undefined\n readonly applied: number\n}\n\ntype MutableFaultState<Fault> = {\n readonly id: number\n readonly fault: Fault\n remaining: number | undefined\n applied: number\n}\n\nexport type EmulatorRouteCoverage = EmulatorRouteEvidence & {\n /** Ledger requests answered by this route since the last ledger clear or reset. */\n readonly requests: number\n}\n\nexport type EmulatorCoverage = {\n readonly routes: ReadonlyArray<EmulatorRouteCoverage>\n /** Ledger requests to unknown routes (failed closed). */\n readonly unknownRouteRequests: number\n}\n\n/** Fields an emulator lists first in `/_emulate/state`. */\nexport type EmulatorStateFields = {\n readonly knownModels: ReadonlyArray<string>\n /** Only for emulators that emulate streamed reasoning. */\n readonly reasoningModels?: ReadonlyArray<string>\n}\n\n/** The ledger fields the kernel owns; each wire core extends it with its request fields. */\nexport type KernelLedgerEntry<FaultKind extends string> = {\n seq: number\n method: string\n path: string\n fault?: FaultKind\n faultError?: string\n responseError?: string\n evidence: EmulatorEvidence | 'unknown-route'\n status: number\n bodyChunks: number\n}\n\n/** The public shape of every emulator built on the kernel. */\nexport type EmulatorApi<Turn, Fault, Entry> = {\n /** The fetch handler (emulated API plus the `/_emulate/*` control plane). Never rejects. */\n readonly fetch: (request: Request) => Promise<Response>\n readonly ledger: {\n readonly entries: () => ReadonlyArray<Entry>\n readonly clear: () => void\n }\n /** Clear the ledger, faults, and scripted turns. */\n readonly reset: () => void\n readonly faults: {\n /** Add a fault; throws the emulator's input-invalid error for an invalid fault. */\n readonly add: (fault: Fault) => EmulatorFaultState<Fault>\n readonly list: () => ReadonlyArray<EmulatorFaultState<Fault>>\n readonly clear: () => void\n }\n readonly script: {\n /** Queue a turn for the next emulated request; throws for an invalid turn. */\n readonly enqueue: (turn: Turn) => void\n readonly pending: () => number\n readonly clear: () => void\n }\n readonly coverage: () => EmulatorCoverage\n}\n\nexport type EmulatorKernelConfig<\n Fault extends KernelFault,\n Turn,\n Entry extends KernelLedgerEntry<Fault['kind']>,\n Snapshot\n> = {\n /** Route evidence manifest; `serve` needs exactly one handler per route. */\n readonly routes: ReadonlyArray<EmulatorRouteEvidence>\n /** Fault schema (decoded strictly). */\n readonly faultSchema: Schema.Decoder<Fault>\n /** Scripted-turn schema (decoded strictly). */\n readonly turnSchema: Schema.Decoder<Turn>\n /** Adds the wire core's own ledger fields to a fresh kernel entry. */\n readonly newEntry: (base: KernelLedgerEntry<Fault['kind']>) => Entry\n /** Copies an entry for the ledger API (never shares mutable state). */\n readonly snapshotEntry: (entry: Entry) => Snapshot\n /**\n * Response for an unknown emulated route (fails closed, still written to the ledger with the\n * status it answers); the core may add its own ledger fields to `entry`.\n */\n readonly unknownRoute: (entry: Entry) => Response\n /** Fields listed first in `/_emulate/state`. */\n readonly stateFields: () => EmulatorStateFields\n /** Builds the error thrown by the JS API (`faults.add`, `script.enqueue`) for invalid input. */\n readonly inputInvalid: (input: 'fault' | 'turn', reason: string) => Error\n}\n\nexport type EmulatorRouteHandler<Entry> = (\n request: Request,\n entry: Entry,\n path: string\n) => Promise<Response>\n\n// Control inputs are strict: an unknown key (a typo, or an `error` turn with a bad field) is\n// rejected instead of silently decoding as a different, all-optional shape.\nconst strict = { onExcessProperty: 'error' } as const\n\nconst decodeJsonText = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Json))\n\nconst issueMessage = (issue: Schema.SchemaError['issue']): string =>\n new Schema.SchemaError(issue).message\n\nexport const isJsonObject = (value: Schema.Json | undefined): value is Schema.JsonObject =>\n value !== null && value !== undefined && Predicate.isObject(value) && !Array.isArray(value)\n\nexport const jsonField = (value: Schema.Json | undefined, key: string): Schema.Json | undefined =>\n isJsonObject(value) ? value[key] : undefined\n\nexport const stringField = (value: Schema.Json | undefined, key: string): string | undefined => {\n const field = jsonField(value, key)\n\n return Predicate.isString(field) ? field : undefined\n}\n\nconst textEncoder = new TextEncoder()\n\n/** A JSON response; `body` is any JSON-serializable value (ledger snapshots, fault states, envelopes). */\nexport const jsonResponse = (\n status: number,\n body: unknown,\n headers: HeadersInit = {}\n): Response => {\n const responseHeaders = new Headers(headers)\n\n responseHeaders.set('content-type', 'application/json')\n\n return new Response(JSON.stringify(body), { status, headers: responseHeaders })\n}\n\n/** An emulator (not emulated-service) error: control-plane problems and unbuildable responses. */\nexport const controlError = (\n status: number,\n message: string,\n headers: HeadersInit = {}\n): Response => jsonResponse(status, { error: { message, type: 'emulator_error' } }, headers)\n\n/** A scripted or fault body: a string is sent as is, anything else as JSON. */\nexport const bodyText = (body: Schema.Json): string =>\n Predicate.isString(body) ? body : JSON.stringify(body)\n\n/** Headers for a scripted or fault body, with a `content-type` matching the body when unset. */\nconst bodyHeaders = (headers: Readonly<Record<string, string>> | undefined, body: Schema.Json) => {\n const result = new Headers(headers ?? {})\n\n if (!result.has('content-type')) {\n result.set('content-type', Predicate.isString(body) ? 'text/plain' : 'application/json')\n }\n\n return result\n}\n\nexport const readText = (request: Request): Promise<string | undefined> =>\n request.text().then(\n text => text,\n () => undefined\n )\n\nexport const parseJson = (text: string): Schema.Json | undefined => {\n const result = decodeJsonText(text)\n\n return Result.isSuccess(result) ? result.success : undefined\n}\n\nconst pathMatches = (pattern: string, path: string): boolean =>\n pattern.endsWith('*') ? path.startsWith(pattern.slice(0, -1)) : pattern === path\n\nconst faultMatches = (fault: KernelFault, path: string, model: string | undefined): boolean =>\n (fault.match?.path === undefined || pathMatches(fault.match.path, path)) &&\n (fault.match?.model === undefined || fault.match.model === model)\n\nconst isBodyChunkFault = (fault: KernelFault): fault is BodyChunkFault =>\n fault.kind === 'error-after-chunks' || fault.kind === 'truncate-after-chunks'\n\nconst chunkFaultProblem = (fault: BodyChunkFault, chunks: number): string | undefined => {\n const applies =\n fault.kind === 'error-after-chunks' ? fault.chunks <= chunks : fault.chunks < chunks\n\n return applies\n ? undefined\n : `${fault.kind} after ${fault.chunks} chunk(s) cannot apply to a response with ${chunks} chunk(s)`\n}\n\n/**\n * Strictly pull-driven body: chunk `k` is produced only when the consumer\n * pulls it, and `entry.bodyChunks` counts the chunks handed over. The fault\n * (already validated against the chunk count) applies when the consumer pulls\n * past `fault.chunks` chunks.\n */\nconst chunkedBody = (\n chunks: ReadonlyArray<string>,\n fault: BodyChunkFault | undefined,\n entry: { bodyChunks: number }\n): ReadableStream<Uint8Array> => {\n let next = 0\n\n return new ReadableStream<Uint8Array>(\n {\n pull: controller => {\n if (fault !== undefined && next === fault.chunks) {\n if (fault.kind === 'error-after-chunks') {\n controller.error(new Error(`synthetic mid-stream failure after ${next} chunk(s)`))\n } else {\n controller.close()\n }\n\n return\n }\n\n const chunk = chunks[next]\n\n if (chunk === undefined) {\n controller.close()\n\n return\n }\n\n next += 1\n entry.bodyChunks += 1\n controller.enqueue(textEncoder.encode(chunk))\n }\n },\n { highWaterMark: 0 }\n )\n}\n\nconst withEvidence = (response: Response, evidence: EmulatorEvidence): Response => {\n if (evidence === 'unverified') {\n response.headers.set(emulatorEvidenceHeader, 'unverified')\n }\n\n return response\n}\n\nconst maxSchemaDepth = 8\n\n/**\n * A synthetic value for a JSON Schema: required object properties only,\n * non-empty strings (or the first `enum` / `const` value), the minimum for\n * numbers, `true` for booleans, and `minItems` synthetic items for arrays.\n */\nexport const synthesizeValue = (\n schema: Schema.Json | undefined,\n name: string,\n depth: number\n): Schema.Json => {\n const constant = jsonField(schema, 'const')\n\n if (constant !== undefined) {\n return constant\n }\n\n const choices = jsonField(schema, 'enum')\n\n if (Array.isArray(choices) && choices.length > 0) {\n return choices[0] ?? null\n }\n\n for (const key of ['anyOf', 'oneOf', 'allOf']) {\n const options = jsonField(schema, key)\n\n if (Array.isArray(options) && options.length > 0) {\n const preferred = options.find(option => stringField(option, 'type') !== 'null')\n\n return synthesizeValue(preferred ?? options[0], name, depth + 1)\n }\n }\n\n const declared = jsonField(schema, 'type')\n\n const type = Array.isArray(declared)\n ? declared.find(entry => Predicate.isString(entry) && entry !== 'null')\n : declared\n\n if (depth > maxSchemaDepth) {\n return null\n }\n\n switch (type) {\n case 'object': {\n const properties = jsonField(schema, 'properties')\n const required = jsonField(schema, 'required')\n const value: Record<string, Schema.Json> = {}\n\n for (const key of Array.isArray(required) ? required : []) {\n if (Predicate.isString(key)) {\n value[key] = synthesizeValue(jsonField(properties, key), key, depth + 1)\n }\n }\n\n return value\n }\n\n case 'array': {\n const minItems = jsonField(schema, 'minItems')\n const count = Predicate.isNumber(minItems) && minItems > 0 ? Math.min(minItems, 3) : 0\n\n return Array.from({ length: count }, () =>\n synthesizeValue(jsonField(schema, 'items'), name, depth + 1)\n )\n }\n\n case 'integer':\n case 'number': {\n const minimum = jsonField(schema, 'minimum')\n\n return Predicate.isNumber(minimum) ? minimum : 1\n }\n\n case 'boolean':\n return true\n case 'null':\n return null\n default:\n return `synthetic ${name}`\n }\n}\n\n/** Split text into up to `parts` non-empty fragments (at least one). */\nexport const splitFragments = (text: string, parts: number): ReadonlyArray<string> => {\n if (text.length <= 1) {\n return [text]\n }\n\n const size = Math.ceil(text.length / Math.min(parts, text.length))\n const fragments: Array<string> = []\n\n for (let index = 0; index < text.length; index += size) {\n fragments.push(text.slice(index, index + size))\n }\n\n return fragments\n}\n\n/** A rough synthetic token count (about four characters per token, at least one). */\nexport const approximateTokens = (text: string): number => Math.max(1, Math.ceil(text.length / 4))\n\n/**\n * Create the kernel of an emulator. Each call has independent ledger, fault,\n * and script state. Handlers use the returned helpers, then `serve` binds\n * them to the manifest (throwing `EmulatorRouteUnmapped` on a mismatch) and\n * returns the emulator API.\n */\nexport const makeEmulatorKernel = <\n Fault extends KernelFault,\n Turn,\n Entry extends KernelLedgerEntry<Fault['kind']>,\n Snapshot\n>(\n config: EmulatorKernelConfig<Fault, Turn, Entry, Snapshot>\n) => {\n const decodeFault = Schema.decodeUnknownResult(config.faultSchema, strict)\n\n const decodeFaultList = Schema.decodeUnknownResult(\n Schema.Union([config.faultSchema, Schema.Struct({ faults: Schema.Array(config.faultSchema) })]),\n strict\n )\n\n const decodeTurn = Schema.decodeUnknownResult(config.turnSchema, strict)\n\n const decodeTurnList = Schema.decodeUnknownResult(\n Schema.Union([Schema.Struct({ turns: Schema.Array(config.turnSchema) }), config.turnSchema]),\n strict\n )\n\n let entries: Array<Entry> = []\n let faultStates: Array<MutableFaultState<Fault>> = []\n let turns: Array<Turn> = []\n let nextSeq = 1\n let nextFaultId = 1\n\n const snapshotFault = (state: MutableFaultState<Fault>): EmulatorFaultState<Fault> => ({\n ...state\n })\n\n const clearLedger = () => {\n entries = []\n nextSeq = 1\n }\n\n const addFault = (input: unknown): EmulatorFaultState<Fault> | string => {\n const decoded = decodeFault(input)\n\n if (Result.isFailure(decoded)) {\n return issueMessage(decoded.failure.issue)\n }\n\n const state: MutableFaultState<Fault> = {\n id: nextFaultId++,\n fault: decoded.success,\n remaining: decoded.success.count,\n applied: 0\n }\n\n faultStates.push(state)\n\n return snapshotFault(state)\n }\n\n const enqueueTurn = (input: unknown): string | undefined => {\n const decoded = decodeTurn(input)\n\n if (Result.isFailure(decoded)) {\n return issueMessage(decoded.failure.issue)\n }\n\n turns.push(decoded.success)\n\n return undefined\n }\n\n /** The first matching fault with uses left, in insertion order (not yet consumed). */\n const takeFault = (\n path: string,\n model: string | undefined\n ): MutableFaultState<Fault> | undefined =>\n faultStates.find(\n state =>\n (state.remaining === undefined || state.remaining > 0) &&\n faultMatches(state.fault, path, model)\n )\n\n /** Use up one application of a fault and record its kind in the ledger entry. */\n const consumeFault = (state: MutableFaultState<Fault>, entry: Entry) => {\n state.applied += 1\n entry.fault = state.fault.kind\n\n if (state.remaining !== undefined) {\n state.remaining -= 1\n }\n }\n\n /** The next scripted turn, used up when its request arrives. */\n const nextTurn = (): Turn | undefined => turns.shift()\n\n const coverage = (): EmulatorCoverage => ({\n routes: config.routes.map(route => ({\n ...route,\n requests: entries.filter(\n entry =>\n entry.evidence !== 'unknown-route' &&\n entry.method === route.method &&\n entry.path === route.path\n ).length\n })),\n unknownRouteRequests: entries.filter(entry => entry.evidence === 'unknown-route').length\n })\n\n /**\n * Answer with a pull-driven body. A matching `error-after-chunks` or\n * `truncate-after-chunks` fault shapes it; one that cannot take effect\n * answers 500 and is not consumed. Other fault kinds are left to the caller.\n */\n const respondWithBody = (\n entry: Entry,\n status: number,\n headers: HeadersInit,\n chunks: ReadonlyArray<string>,\n fault: MutableFaultState<Fault> | undefined\n ): Response => {\n const chunkFault =\n fault === undefined || !isBodyChunkFault(fault.fault) ? undefined : fault.fault\n\n if (chunkFault !== undefined) {\n const problem = chunkFaultProblem(chunkFault, chunks.length)\n\n if (problem !== undefined) {\n entry.faultError = problem\n entry.status = 500\n\n return controlError(500, `emulator fault cannot apply: ${problem}`)\n }\n }\n\n // Build the response before using up the fault: a response that cannot be built must not\n // consume it.\n const response = new Response(chunkedBody(chunks, chunkFault, entry), { status, headers })\n\n if (chunkFault !== undefined && fault !== undefined) {\n consumeFault(fault, entry)\n }\n\n entry.status = response.status\n\n return response\n }\n\n /** Answer a `status` fault with its status, headers, and body (or `defaultBody`). */\n const respondWithStatusFault = (\n entry: Entry,\n state: MutableFaultState<Fault>,\n fault: typeof EmulatorStatusFault.Type,\n defaultBody: (status: number) => Schema.Json\n ): Response => {\n const faultBody = fault.body ?? defaultBody(fault.status)\n\n // Built first: a status fault whose response cannot be built is not used up.\n const response = new Response(bodyText(faultBody), {\n status: fault.status,\n headers: bodyHeaders(fault.headers, faultBody)\n })\n\n consumeFault(state, entry)\n entry.status = response.status\n\n return response\n }\n\n /** Answer a scripted error turn; a matching chunk fault still shapes its body. */\n const respondWithScriptedError = (\n entry: Entry,\n error: EmulatorScriptedError['error'],\n fault: MutableFaultState<Fault> | undefined\n ): Response =>\n respondWithBody(\n entry,\n error.status,\n bodyHeaders(error.headers, error.body),\n [bodyText(error.body)],\n fault\n )\n\n const reset = () => {\n clearLedger()\n faultStates = []\n turns = []\n }\n\n const state = () => ({\n ...config.stateFields(),\n pendingTurns: turns.length,\n faults: faultStates.map(snapshotFault),\n ledgerEntries: entries.length\n })\n\n const controlPlane = async (request: Request, path: string): Promise<Response> => {\n const method = request.method\n const allow = (methods: string) => controlError(405, 'method not allowed', { allow: methods })\n\n const jsonBody = async (): Promise<Schema.Json | undefined> => {\n const text = await readText(request)\n\n return text === undefined ? undefined : parseJson(text)\n }\n\n switch (path) {\n case '/_emulate/ledger':\n if (method === 'GET') {\n return jsonResponse(200, { entries: entries.map(config.snapshotEntry) })\n }\n\n if (method === 'DELETE') {\n const cleared = entries.length\n\n clearLedger()\n\n return jsonResponse(200, { cleared })\n }\n\n return allow('GET, DELETE')\n\n case '/_emulate/faults': {\n if (method === 'GET') {\n return jsonResponse(200, { faults: faultStates.map(snapshotFault) })\n }\n\n if (method === 'DELETE') {\n const cleared = faultStates.length\n\n faultStates = []\n\n return jsonResponse(200, { cleared })\n }\n\n if (method !== 'POST') {\n return allow('GET, POST, DELETE')\n }\n\n const decoded = decodeFaultList(await jsonBody())\n\n if (Result.isFailure(decoded)) {\n return controlError(400, `invalid fault: ${issueMessage(decoded.failure.issue)}`)\n }\n\n const added = (\n 'faults' in decoded.success ? decoded.success.faults : [decoded.success]\n ).map(fault => addFault(fault))\n\n return jsonResponse(201, { faults: added })\n }\n\n case '/_emulate/script': {\n if (method !== 'POST') {\n return allow('POST')\n }\n\n const decoded = decodeTurnList(await jsonBody())\n\n if (Result.isFailure(decoded)) {\n return controlError(400, `invalid turn: ${issueMessage(decoded.failure.issue)}`)\n }\n\n const queued =\n Predicate.isObject(decoded.success) && 'turns' in decoded.success\n ? decoded.success.turns\n : [decoded.success]\n\n for (const turn of queued) {\n enqueueTurn(turn)\n }\n\n return jsonResponse(201, { pending: turns.length })\n }\n\n case '/_emulate/reset':\n if (method !== 'POST') {\n return allow('POST')\n }\n\n reset()\n\n return jsonResponse(200, { reset: true })\n\n case '/_emulate/state':\n if (method !== 'GET') {\n return allow('GET')\n }\n\n return jsonResponse(200, state())\n\n case '/_emulate/coverage':\n if (method !== 'GET') {\n return allow('GET')\n }\n\n return jsonResponse(200, coverage())\n\n default:\n return controlError(404, 'unknown control-plane route')\n }\n }\n\n /**\n * Bind one handler per manifest route (keyed by `emulatorRouteKey`) and\n * return the emulator API. Throws `EmulatorRouteUnmapped` when a manifest\n * route has no handler or a handler has no manifest route.\n */\n const serve = (\n handlers: ReadonlyMap<string, EmulatorRouteHandler<Entry>>\n ): EmulatorApi<Turn, Fault, Snapshot> => {\n const routes = bindRouteHandlers(config.routes, handlers)\n\n const emulatedApi = async (request: Request, path: string): Promise<Response> => {\n const bound = routes.find(\n candidate => candidate.route.method === request.method && candidate.route.path === path\n )\n\n const entry = config.newEntry({\n seq: nextSeq++,\n method: request.method,\n path,\n evidence: bound?.route.evidence ?? 'unknown-route',\n status: 0,\n bodyChunks: 0\n })\n\n entries.push(entry)\n\n if (bound === undefined) {\n const response = config.unknownRoute(entry)\n\n entry.status = response.status\n\n return response\n }\n\n // Error recovery still answers through the route: the fallback 500 is evidence-tagged and\n // the ledger records the status actually sent.\n const response = await bound.handler(request, entry, path).catch(() => {\n entry.status = 500\n entry.responseError = 'the emulator could not build the planned response'\n\n return controlError(500, 'emulator could not build the response')\n })\n\n return withEvidence(response, bound.route.evidence)\n }\n\n const handle = (request: Request): Promise<Response> => {\n const path = URL.canParse(request.url) ? new URL(request.url).pathname : '/'\n\n return path === '/_emulate' || path.startsWith('/_emulate/')\n ? controlPlane(request, path)\n : emulatedApi(request, path)\n }\n\n return {\n fetch: request =>\n handle(request).catch(() => controlError(500, 'emulator failed to handle the request')),\n ledger: {\n entries: () => entries.map(config.snapshotEntry),\n clear: clearLedger\n },\n reset,\n faults: {\n add: fault => {\n const added = addFault(fault)\n\n if (Predicate.isString(added)) {\n throw config.inputInvalid('fault', added)\n }\n\n return added\n },\n list: () => faultStates.map(snapshotFault),\n clear: () => {\n faultStates = []\n }\n },\n script: {\n enqueue: turn => {\n const problem = enqueueTurn(turn)\n\n if (problem !== undefined) {\n throw config.inputInvalid('turn', problem)\n }\n },\n pending: () => turns.length,\n clear: () => {\n turns = []\n }\n },\n coverage\n }\n }\n\n return {\n takeFault,\n consumeFault,\n nextTurn,\n respondWithBody,\n respondWithStatusFault,\n respondWithScriptedError,\n serve\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwBA,MAAa,aAAa,OAAO,IAAI,MAAM,OAAO,uBAAuB,CAAC,CAAC;AAE3E,MAAM,aAAa,OAAO,IAAI,MAAM,OAAO,uBAAuB,CAAC,CAAC;;AAGpE,MAAa,qBAAqB,OAAO,OAAO;CAC9C,MAAM,OAAO,YAAY,OAAO,MAAM;CACtC,OAAO,OAAO,YAAY,OAAO,MAAM;AACzC,CAAC;;AAKD,MAAa,sBAAsB;CACjC,OAAO,OAAO,YAAY,kBAAkB;;CAE5C,OAAO,OAAO,YAAY,UAAU;AACtC;;AAGA,MAAa,sBAAsB,OAAO,OAAO;CAC/C,MAAM,OAAO,QAAQ,QAAQ;CAC7B,QAAQ;CACR,SAAS,OAAO,YAAY,oBAAoB;CAChD,MAAM,OAAO,YAAY,OAAO,IAAI;CACpC,GAAG;AACL,CAAC;;AAGD,MAAa,gCAAgC,OAAO,OAAO;CACzD,MAAM,OAAO,QAAQ,oBAAoB;CACzC,QAAQ;CACR,GAAG;AACL,CAAC;;AAGD,MAAa,mCAAmC,OAAO,OAAO;CAC5D,MAAM,OAAO,QAAQ,uBAAuB;CAC5C,QAAQ;CACR,GAAG;AACL,CAAC;;AAGD,MAAa,wBAAwB,OAAO,OAAO,EACjD,OAAO,OAAO,OAAO;CACnB,QAAQ;CACR,MAAM,OAAO;CACb,SAAS,OAAO,YAAY,oBAAoB;AAClD,CAAC,EACH,CAAC;AAyHD,MAAM,SAAS,EAAE,kBAAkB,QAAQ;AAE3C,MAAM,iBAAiB,OAAO,oBAAoB,OAAO,eAAe,OAAO,IAAI,CAAC;AAEpF,MAAM,gBAAgB,UACpB,IAAI,OAAO,YAAY,KAAK,EAAE;AAEhC,MAAa,gBAAgB,UAC3B,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK;AAE5F,MAAa,aAAa,OAAgC,QACxD,aAAa,KAAK,IAAI,MAAM,OAAO,KAAA;AAErC,MAAa,eAAe,OAAgC,QAAoC;CAC9F,MAAM,QAAQ,UAAU,OAAO,GAAG;CAElC,OAAO,UAAU,SAAS,KAAK,IAAI,QAAQ,KAAA;AAC7C;AAEA,MAAM,cAAc,IAAI,YAAY;;AAGpC,MAAa,gBACX,QACA,MACA,UAAuB,CAAC,MACX;CACb,MAAM,kBAAkB,IAAI,QAAQ,OAAO;CAE3C,gBAAgB,IAAI,gBAAgB,kBAAkB;CAEtD,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EAAE;EAAQ,SAAS;CAAgB,CAAC;AAChF;;AAGA,MAAa,gBACX,QACA,SACA,UAAuB,CAAC,MACX,aAAa,QAAQ,EAAE,OAAO;CAAE;CAAS,MAAM;AAAiB,EAAE,GAAG,OAAO;;AAG3F,MAAa,YAAY,SACvB,UAAU,SAAS,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI;;AAGvD,MAAM,eAAe,SAAuD,SAAsB;CAChG,MAAM,SAAS,IAAI,QAAQ,WAAW,CAAC,CAAC;CAExC,IAAI,CAAC,OAAO,IAAI,cAAc,GAC5B,OAAO,IAAI,gBAAgB,UAAU,SAAS,IAAI,IAAI,eAAe,kBAAkB;CAGzF,OAAO;AACT;AAEA,MAAa,YAAY,YACvB,QAAQ,KAAK,EAAE,MACb,SAAQ,YACF,KAAA,CACR;AAEF,MAAa,aAAa,SAA0C;CAClE,MAAM,SAAS,eAAe,IAAI;CAElC,OAAO,OAAO,UAAU,MAAM,IAAI,OAAO,UAAU,KAAA;AACrD;AAEA,MAAM,eAAe,SAAiB,SACpC,QAAQ,SAAS,GAAG,IAAI,KAAK,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC,IAAI,YAAY;AAE9E,MAAM,gBAAgB,OAAoB,MAAc,WACrD,MAAM,OAAO,SAAS,KAAA,KAAa,YAAY,MAAM,MAAM,MAAM,IAAI,OACrE,MAAM,OAAO,UAAU,KAAA,KAAa,MAAM,MAAM,UAAU;AAE7D,MAAM,oBAAoB,UACxB,MAAM,SAAS,wBAAwB,MAAM,SAAS;AAExD,MAAM,qBAAqB,OAAuB,WAAuC;CAIvF,QAFE,MAAM,SAAS,uBAAuB,MAAM,UAAU,SAAS,MAAM,SAAS,UAG5E,KAAA,IACA,GAAG,MAAM,KAAK,SAAS,MAAM,OAAO,4CAA4C,OAAO;AAC7F;;;;;;;AAQA,MAAM,eACJ,QACA,OACA,UAC+B;CAC/B,IAAI,OAAO;CAEX,OAAO,IAAI,eACT,EACE,OAAM,eAAc;EAClB,IAAI,UAAU,KAAA,KAAa,SAAS,MAAM,QAAQ;GAChD,IAAI,MAAM,SAAS,sBACjB,WAAW,sBAAM,IAAI,MAAM,sCAAsC,KAAK,UAAU,CAAC;QAEjF,WAAW,MAAM;GAGnB;EACF;EAEA,MAAM,QAAQ,OAAO;EAErB,IAAI,UAAU,KAAA,GAAW;GACvB,WAAW,MAAM;GAEjB;EACF;EAEA,QAAQ;EACR,MAAM,cAAc;EACpB,WAAW,QAAQ,YAAY,OAAO,KAAK,CAAC;CAC9C,EACF,GACA,EAAE,eAAe,EAAE,CACrB;AACF;AAEA,MAAM,gBAAgB,UAAoB,aAAyC;CACjF,IAAI,aAAa,cACf,SAAS,QAAQ,IAAI,wBAAwB,YAAY;CAG3D,OAAO;AACT;AAEA,MAAM,iBAAiB;;;;;;AAOvB,MAAa,mBACX,QACA,MACA,UACgB;CAChB,MAAM,WAAW,UAAU,QAAQ,OAAO;CAE1C,IAAI,aAAa,KAAA,GACf,OAAO;CAGT,MAAM,UAAU,UAAU,QAAQ,MAAM;CAExC,IAAI,MAAM,QAAQ,OAAO,KAAK,QAAQ,SAAS,GAC7C,OAAO,QAAQ,MAAM;CAGvB,KAAK,MAAM,OAAO;EAAC;EAAS;EAAS;CAAO,GAAG;EAC7C,MAAM,UAAU,UAAU,QAAQ,GAAG;EAErC,IAAI,MAAM,QAAQ,OAAO,KAAK,QAAQ,SAAS,GAG7C,OAAO,gBAFW,QAAQ,MAAK,WAAU,YAAY,QAAQ,MAAM,MAAM,MAE1C,KAAK,QAAQ,IAAI,MAAM,QAAQ,CAAC;CAEnE;CAEA,MAAM,WAAW,UAAU,QAAQ,MAAM;CAEzC,MAAM,OAAO,MAAM,QAAQ,QAAQ,IAC/B,SAAS,MAAK,UAAS,UAAU,SAAS,KAAK,KAAK,UAAU,MAAM,IACpE;CAEJ,IAAI,QAAQ,gBACV,OAAO;CAGT,QAAQ,MAAR;EACE,KAAK,UAAU;GACb,MAAM,aAAa,UAAU,QAAQ,YAAY;GACjD,MAAM,WAAW,UAAU,QAAQ,UAAU;GAC7C,MAAM,QAAqC,CAAC;GAE5C,KAAK,MAAM,OAAO,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,GACtD,IAAI,UAAU,SAAS,GAAG,GACxB,MAAM,OAAO,gBAAgB,UAAU,YAAY,GAAG,GAAG,KAAK,QAAQ,CAAC;GAI3E,OAAO;EACT;EAEA,KAAK,SAAS;GACZ,MAAM,WAAW,UAAU,QAAQ,UAAU;GAC7C,MAAM,QAAQ,UAAU,SAAS,QAAQ,KAAK,WAAW,IAAI,KAAK,IAAI,UAAU,CAAC,IAAI;GAErF,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,SAChC,gBAAgB,UAAU,QAAQ,OAAO,GAAG,MAAM,QAAQ,CAAC,CAC7D;EACF;EAEA,KAAK;EACL,KAAK,UAAU;GACb,MAAM,UAAU,UAAU,QAAQ,SAAS;GAE3C,OAAO,UAAU,SAAS,OAAO,IAAI,UAAU;EACjD;EAEA,KAAK,WACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,SACE,OAAO,aAAa;CACxB;AACF;;AAGA,MAAa,kBAAkB,MAAc,UAAyC;CACpF,IAAI,KAAK,UAAU,GACjB,OAAO,CAAC,IAAI;CAGd,MAAM,OAAO,KAAK,KAAK,KAAK,SAAS,KAAK,IAAI,OAAO,KAAK,MAAM,CAAC;CACjE,MAAM,YAA2B,CAAC;CAElC,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,MAChD,UAAU,KAAK,KAAK,MAAM,OAAO,QAAQ,IAAI,CAAC;CAGhD,OAAO;AACT;;AAGA,MAAa,qBAAqB,SAAyB,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,SAAS,CAAC,CAAC;;;;;;;AAQjG,MAAa,sBAMX,WACG;CACH,MAAM,cAAc,OAAO,oBAAoB,OAAO,aAAa,MAAM;CAEzE,MAAM,kBAAkB,OAAO,oBAC7B,OAAO,MAAM,CAAC,OAAO,aAAa,OAAO,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,WAAW,EAAE,CAAC,CAAC,CAAC,GAC9F,MACF;CAEA,MAAM,aAAa,OAAO,oBAAoB,OAAO,YAAY,MAAM;CAEvE,MAAM,iBAAiB,OAAO,oBAC5B,OAAO,MAAM,CAAC,OAAO,OAAO,EAAE,OAAO,OAAO,MAAM,OAAO,UAAU,EAAE,CAAC,GAAG,OAAO,UAAU,CAAC,GAC3F,MACF;CAEA,IAAI,UAAwB,CAAC;CAC7B,IAAI,cAA+C,CAAC;CACpD,IAAI,QAAqB,CAAC;CAC1B,IAAI,UAAU;CACd,IAAI,cAAc;CAElB,MAAM,iBAAiB,WAAgE,EACrF,GAAG,MACL;CAEA,MAAM,oBAAoB;EACxB,UAAU,CAAC;EACX,UAAU;CACZ;CAEA,MAAM,YAAY,UAAuD;EACvE,MAAM,UAAU,YAAY,KAAK;EAEjC,IAAI,OAAO,UAAU,OAAO,GAC1B,OAAO,aAAa,QAAQ,QAAQ,KAAK;EAG3C,MAAM,QAAkC;GACtC,IAAI;GACJ,OAAO,QAAQ;GACf,WAAW,QAAQ,QAAQ;GAC3B,SAAS;EACX;EAEA,YAAY,KAAK,KAAK;EAEtB,OAAO,cAAc,KAAK;CAC5B;CAEA,MAAM,eAAe,UAAuC;EAC1D,MAAM,UAAU,WAAW,KAAK;EAEhC,IAAI,OAAO,UAAU,OAAO,GAC1B,OAAO,aAAa,QAAQ,QAAQ,KAAK;EAG3C,MAAM,KAAK,QAAQ,OAAO;CAG5B;;CAGA,MAAM,aACJ,MACA,UAEA,YAAY,MACV,WACG,MAAM,cAAc,KAAA,KAAa,MAAM,YAAY,MACpD,aAAa,MAAM,OAAO,MAAM,KAAK,CACzC;;CAGF,MAAM,gBAAgB,OAAiC,UAAiB;EACtE,MAAM,WAAW;EACjB,MAAM,QAAQ,MAAM,MAAM;EAE1B,IAAI,MAAM,cAAc,KAAA,GACtB,MAAM,aAAa;CAEvB;;CAGA,MAAM,iBAAmC,MAAM,MAAM;CAErD,MAAM,kBAAoC;EACxC,QAAQ,OAAO,OAAO,KAAI,WAAU;GAClC,GAAG;GACH,UAAU,QAAQ,QAChB,UACE,MAAM,aAAa,mBACnB,MAAM,WAAW,MAAM,UACvB,MAAM,SAAS,MAAM,IACzB,EAAE;EACJ,EAAE;EACF,sBAAsB,QAAQ,QAAO,UAAS,MAAM,aAAa,eAAe,EAAE;CACpF;;;;;;CAOA,MAAM,mBACJ,OACA,QACA,SACA,QACA,UACa;EACb,MAAM,aACJ,UAAU,KAAA,KAAa,CAAC,iBAAiB,MAAM,KAAK,IAAI,KAAA,IAAY,MAAM;EAE5E,IAAI,eAAe,KAAA,GAAW;GAC5B,MAAM,UAAU,kBAAkB,YAAY,OAAO,MAAM;GAE3D,IAAI,YAAY,KAAA,GAAW;IACzB,MAAM,aAAa;IACnB,MAAM,SAAS;IAEf,OAAO,aAAa,KAAK,gCAAgC,SAAS;GACpE;EACF;EAIA,MAAM,WAAW,IAAI,SAAS,YAAY,QAAQ,YAAY,KAAK,GAAG;GAAE;GAAQ;EAAQ,CAAC;EAEzF,IAAI,eAAe,KAAA,KAAa,UAAU,KAAA,GACxC,aAAa,OAAO,KAAK;EAG3B,MAAM,SAAS,SAAS;EAExB,OAAO;CACT;;CAGA,MAAM,0BACJ,OACA,OACA,OACA,gBACa;EACb,MAAM,YAAY,MAAM,QAAQ,YAAY,MAAM,MAAM;EAGxD,MAAM,WAAW,IAAI,SAAS,SAAS,SAAS,GAAG;GACjD,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,SAAS,SAAS;EAC/C,CAAC;EAED,aAAa,OAAO,KAAK;EACzB,MAAM,SAAS,SAAS;EAExB,OAAO;CACT;;CAGA,MAAM,4BACJ,OACA,OACA,UAEA,gBACE,OACA,MAAM,QACN,YAAY,MAAM,SAAS,MAAM,IAAI,GACrC,CAAC,SAAS,MAAM,IAAI,CAAC,GACrB,KACF;CAEF,MAAM,cAAc;EAClB,YAAY;EACZ,cAAc,CAAC;EACf,QAAQ,CAAC;CACX;CAEA,MAAM,eAAe;EACnB,GAAG,OAAO,YAAY;EACtB,cAAc,MAAM;EACpB,QAAQ,YAAY,IAAI,aAAa;EACrC,eAAe,QAAQ;CACzB;CAEA,MAAM,eAAe,OAAO,SAAkB,SAAoC;EAChF,MAAM,SAAS,QAAQ;EACvB,MAAM,SAAS,YAAoB,aAAa,KAAK,sBAAsB,EAAE,OAAO,QAAQ,CAAC;EAE7F,MAAM,WAAW,YAA8C;GAC7D,MAAM,OAAO,MAAM,SAAS,OAAO;GAEnC,OAAO,SAAS,KAAA,IAAY,KAAA,IAAY,UAAU,IAAI;EACxD;EAEA,QAAQ,MAAR;GACE,KAAK;IACH,IAAI,WAAW,OACb,OAAO,aAAa,KAAK,EAAE,SAAS,QAAQ,IAAI,OAAO,aAAa,EAAE,CAAC;IAGzE,IAAI,WAAW,UAAU;KACvB,MAAM,UAAU,QAAQ;KAExB,YAAY;KAEZ,OAAO,aAAa,KAAK,EAAE,QAAQ,CAAC;IACtC;IAEA,OAAO,MAAM,aAAa;GAE5B,KAAK,oBAAoB;IACvB,IAAI,WAAW,OACb,OAAO,aAAa,KAAK,EAAE,QAAQ,YAAY,IAAI,aAAa,EAAE,CAAC;IAGrE,IAAI,WAAW,UAAU;KACvB,MAAM,UAAU,YAAY;KAE5B,cAAc,CAAC;KAEf,OAAO,aAAa,KAAK,EAAE,QAAQ,CAAC;IACtC;IAEA,IAAI,WAAW,QACb,OAAO,MAAM,mBAAmB;IAGlC,MAAM,UAAU,gBAAgB,MAAM,SAAS,CAAC;IAEhD,IAAI,OAAO,UAAU,OAAO,GAC1B,OAAO,aAAa,KAAK,kBAAkB,aAAa,QAAQ,QAAQ,KAAK,GAAG;IAOlF,OAAO,aAAa,KAAK,EAAE,SAHzB,YAAY,QAAQ,UAAU,QAAQ,QAAQ,SAAS,CAAC,QAAQ,OAAO,GACvE,KAAI,UAAS,SAAS,KAAK,CAEU,EAAE,CAAC;GAC5C;GAEA,KAAK,oBAAoB;IACvB,IAAI,WAAW,QACb,OAAO,MAAM,MAAM;IAGrB,MAAM,UAAU,eAAe,MAAM,SAAS,CAAC;IAE/C,IAAI,OAAO,UAAU,OAAO,GAC1B,OAAO,aAAa,KAAK,iBAAiB,aAAa,QAAQ,QAAQ,KAAK,GAAG;IAGjF,MAAM,SACJ,UAAU,SAAS,QAAQ,OAAO,KAAK,WAAW,QAAQ,UACtD,QAAQ,QAAQ,QAChB,CAAC,QAAQ,OAAO;IAEtB,KAAK,MAAM,QAAQ,QACjB,YAAY,IAAI;IAGlB,OAAO,aAAa,KAAK,EAAE,SAAS,MAAM,OAAO,CAAC;GACpD;GAEA,KAAK;IACH,IAAI,WAAW,QACb,OAAO,MAAM,MAAM;IAGrB,MAAM;IAEN,OAAO,aAAa,KAAK,EAAE,OAAO,KAAK,CAAC;GAE1C,KAAK;IACH,IAAI,WAAW,OACb,OAAO,MAAM,KAAK;IAGpB,OAAO,aAAa,KAAK,MAAM,CAAC;GAElC,KAAK;IACH,IAAI,WAAW,OACb,OAAO,MAAM,KAAK;IAGpB,OAAO,aAAa,KAAK,SAAS,CAAC;GAErC,SACE,OAAO,aAAa,KAAK,6BAA6B;EAC1D;CACF;;;;;;CAOA,MAAM,SACJ,aACuC;EACvC,MAAM,SAAS,kBAAkB,OAAO,QAAQ,QAAQ;EAExD,MAAM,cAAc,OAAO,SAAkB,SAAoC;GAC/E,MAAM,QAAQ,OAAO,MACnB,cAAa,UAAU,MAAM,WAAW,QAAQ,UAAU,UAAU,MAAM,SAAS,IACrF;GAEA,MAAM,QAAQ,OAAO,SAAS;IAC5B,KAAK;IACL,QAAQ,QAAQ;IAChB;IACA,UAAU,OAAO,MAAM,YAAY;IACnC,QAAQ;IACR,YAAY;GACd,CAAC;GAED,QAAQ,KAAK,KAAK;GAElB,IAAI,UAAU,KAAA,GAAW;IACvB,MAAM,WAAW,OAAO,aAAa,KAAK;IAE1C,MAAM,SAAS,SAAS;IAExB,OAAO;GACT;GAWA,OAAO,aAAa,MAPG,MAAM,QAAQ,SAAS,OAAO,IAAI,EAAE,YAAY;IACrE,MAAM,SAAS;IACf,MAAM,gBAAgB;IAEtB,OAAO,aAAa,KAAK,uCAAuC;GAClE,CAAC,GAE6B,MAAM,MAAM,QAAQ;EACpD;EAEA,MAAM,UAAU,YAAwC;GACtD,MAAM,OAAO,IAAI,SAAS,QAAQ,GAAG,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,WAAW;GAEzE,OAAO,SAAS,eAAe,KAAK,WAAW,YAAY,IACvD,aAAa,SAAS,IAAI,IAC1B,YAAY,SAAS,IAAI;EAC/B;EAEA,OAAO;GACL,QAAO,YACL,OAAO,OAAO,EAAE,YAAY,aAAa,KAAK,uCAAuC,CAAC;GACxF,QAAQ;IACN,eAAe,QAAQ,IAAI,OAAO,aAAa;IAC/C,OAAO;GACT;GACA;GACA,QAAQ;IACN,MAAK,UAAS;KACZ,MAAM,QAAQ,SAAS,KAAK;KAE5B,IAAI,UAAU,SAAS,KAAK,GAC1B,MAAM,OAAO,aAAa,SAAS,KAAK;KAG1C,OAAO;IACT;IACA,YAAY,YAAY,IAAI,aAAa;IACzC,aAAa;KACX,cAAc,CAAC;IACjB;GACF;GACA,QAAQ;IACN,UAAS,SAAQ;KACf,MAAM,UAAU,YAAY,IAAI;KAEhC,IAAI,YAAY,KAAA,GACd,MAAM,OAAO,aAAa,QAAQ,OAAO;IAE7C;IACA,eAAe,MAAM;IACrB,aAAa;KACX,QAAQ,CAAC;IACX;GACF;GACA;EACF;CACF;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -0,0 +1,129 @@
1
+ import { EmulatorEvidence, EmulatorRouteEvidence } from "./route-evidence.mjs";
2
+ import { EmulatorApi, EmulatorScriptedError } from "./emulator-kernel.mjs";
3
+ import * as Schema from "effect/Schema";
4
+
5
+ //#region src/fixture-route.d.ts
6
+ /** One committed fixture exchange, copied as data. */
7
+ type FixtureRecording = {
8
+ readonly fixtureId: string;
9
+ readonly caseId: string;
10
+ readonly request: {
11
+ readonly method: string;
12
+ readonly path: string; /** The recorded query string (with `?`), or empty. */
13
+ readonly query: string; /** The recorded (non-credential) request headers, lower-case. */
14
+ readonly headers: Readonly<Record<string, string>>;
15
+ readonly body?: Schema.Json;
16
+ };
17
+ readonly response: {
18
+ readonly status: number;
19
+ readonly headers: Readonly<Record<string, string>>; /** Whether the fixture recorded network chunks (a stream) or one body. */
20
+ readonly streamed: boolean; /** Network chunks as recorded; a body is one chunk. */
21
+ readonly chunks: ReadonlyArray<string>;
22
+ };
23
+ };
24
+ /**
25
+ * Faults: the shared kernel kinds only, with `status` limited to 400-599. `status` without a
26
+ * `body` answers an emulator-fault body (`{ error: { type: 'emulator_fault', message } }`), never
27
+ * a guessed provider envelope.
28
+ */
29
+ declare const FixtureRouteFault: Schema.Union<readonly [Schema.Struct<{
30
+ readonly status: Schema.Int;
31
+ readonly match: Schema.optionalKey<Schema.Struct<{
32
+ readonly path: Schema.optionalKey<Schema.String>;
33
+ readonly model: Schema.optionalKey<Schema.String>;
34
+ }>>;
35
+ readonly count: Schema.optionalKey<Schema.Int>;
36
+ readonly kind: Schema.Literal<"status">;
37
+ readonly headers: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
38
+ readonly body: Schema.optionalKey<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
39
+ }>, Schema.Struct<{
40
+ readonly match: Schema.optionalKey<Schema.Struct<{
41
+ readonly path: Schema.optionalKey<Schema.String>;
42
+ readonly model: Schema.optionalKey<Schema.String>;
43
+ }>>;
44
+ readonly count: Schema.optionalKey<Schema.Int>;
45
+ readonly kind: Schema.Literal<"error-after-chunks">;
46
+ readonly chunks: Schema.Int;
47
+ }>, Schema.Struct<{
48
+ readonly match: Schema.optionalKey<Schema.Struct<{
49
+ readonly path: Schema.optionalKey<Schema.String>;
50
+ readonly model: Schema.optionalKey<Schema.String>;
51
+ }>>;
52
+ readonly count: Schema.optionalKey<Schema.Int>;
53
+ readonly kind: Schema.Literal<"truncate-after-chunks">;
54
+ readonly chunks: Schema.Int;
55
+ }>]>;
56
+ type FixtureRouteFault = typeof FixtureRouteFault.Type;
57
+ type FixtureRouteFaultKind = FixtureRouteFault['kind'];
58
+ /** A scripted replacement body (replaceable-body routes only), shape-checked against the recording. */
59
+ type FixtureRouteScriptedBody = {
60
+ readonly usage: Schema.Json;
61
+ };
62
+ /** A scripted turn: an error response, or (replaceable-body routes only) a replacement body. */
63
+ type FixtureRouteScriptedTurn = EmulatorScriptedError | FixtureRouteScriptedBody;
64
+ type FixtureRouteLedgerEntry = {
65
+ /** 1-based arrival order since the last ledger clear or reset. */readonly seq: number;
66
+ readonly method: string;
67
+ readonly path: string; /** The request's query string (with `?`), when it has one. */
68
+ readonly query?: string; /** Parsed JSON request body, when it was valid JSON. */
69
+ readonly body?: Schema.Json;
70
+ readonly model?: string; /** Which credential header carried a non-empty value (the value is never recorded). */
71
+ readonly credentialHeader?: 'authorization' | 'x-api-key'; /** Values of the non-credential headers the route records (lower-case names). */
72
+ readonly headers: Readonly<Record<string, string>>; /** The fixture id whose recorded response answered the request. */
73
+ readonly recording?: string; /** Why the request was answered 400 not-emulated. */
74
+ readonly notEmulated?: string; /** Kind of the fault that shaped the response. */
75
+ readonly fault?: FixtureRouteFaultKind; /** Why a matching fault could not take effect (the response was a 500 emulator error). */
76
+ readonly faultError?: string; /** Set when the emulator could not build the planned response (answered 500). */
77
+ readonly responseError?: string; /** Set when a scripted turn answered the request. */
78
+ readonly scripted?: 'usage' | 'error'; /** Evidence of the matched route; `unknown-route` for requests on no route. */
79
+ readonly evidence: EmulatorEvidence | 'unknown-route';
80
+ readonly status: number; /** Body chunks handed to the transport so far (each produced when pulled). */
81
+ readonly bodyChunks: number;
82
+ };
83
+ type FixtureRouteEmulator = EmulatorApi<FixtureRouteScriptedTurn, FixtureRouteFault, FixtureRouteLedgerEntry>;
84
+ /** A header every request must carry (non-empty and, when set, accepted by `accepts`). */
85
+ type FixtureRouteHeaderRule = {
86
+ /** Lower-case header name. */readonly name: string; /** Keep the value in the ledger (never for credential or account headers). */
87
+ readonly record: boolean;
88
+ readonly accepts?: (value: string) => boolean;
89
+ };
90
+ type FixtureRouteConfig = {
91
+ readonly method: 'GET' | 'POST';
92
+ readonly path: string; /** Route evidence manifest; must list exactly this route. */
93
+ readonly routes: ReadonlyArray<EmulatorRouteEvidence>; /** The recordings this route answers from (at least one), tried in order. */
94
+ readonly recordings: ReadonlyArray<FixtureRecording>; /** The credential header the SDK sends: a non-empty Bearer, or a non-empty `x-api-key`. */
95
+ readonly credential: 'bearer' | 'x-api-key'; /** Headers the SDK sends besides the credential and the recorded ones, required in order. */
96
+ readonly headers: ReadonlyArray<FixtureRouteHeaderRule>;
97
+ /**
98
+ * Replaceable JSON body (usage routes): enables the scripted `{ usage }` turn, and
99
+ * `defaultBody` replaces the recorded body; both must have the recorded body's JSON shape.
100
+ */
101
+ readonly replaceableBody?: {
102
+ readonly defaultBody: Schema.Json | undefined;
103
+ }; /** Builds the error thrown by the JS API (and at construction) for invalid input. */
104
+ readonly inputInvalid: (input: 'fault' | 'turn', reason: string) => Error;
105
+ };
106
+ /** The one answer for everything no fixture records. */
107
+ declare const notEmulatedResponse: (reason: string) => Response;
108
+ /**
109
+ * Where `actual` leaves the recorded request shape (the documented latitude applied), or
110
+ * undefined when it matches.
111
+ */
112
+ declare const recordedRequestMismatch: (actual: Schema.Json | undefined, recorded: Schema.Json, path?: string, key?: string) => string | undefined;
113
+ /** Where `actual` leaves the recorded JSON shape (object keys and value kinds), or undefined. */
114
+ declare const recordedBodyMismatch: (actual: Schema.Json, recorded: Schema.Json, path?: string) => string | undefined;
115
+ /**
116
+ * Create a fixture-only route. Each call has independent ledger, fault, and script state. Throws
117
+ * `inputInvalid('turn', ...)` when a replaceable body's `defaultBody` does not have the recorded
118
+ * shape.
119
+ *
120
+ * Precedence per request: the credential, the header rules in order, the recorded headers and
121
+ * query, the recorded request shape (one of `recordings`); a request failing any of these answers
122
+ * 400 not-emulated and uses up no fault or turn. Then the first matching fault if it is a `status`
123
+ * fault, then the next scripted turn, then the recorded response; a first matching body fault then
124
+ * shapes the body.
125
+ */
126
+ declare const makeFixtureRouteEmulator: (config: FixtureRouteConfig) => FixtureRouteEmulator;
127
+ //#endregion
128
+ export { FixtureRecording, FixtureRouteConfig, FixtureRouteEmulator, FixtureRouteFault, FixtureRouteFaultKind, FixtureRouteHeaderRule, FixtureRouteLedgerEntry, FixtureRouteScriptedBody, FixtureRouteScriptedTurn, makeFixtureRouteEmulator, notEmulatedResponse, recordedBodyMismatch, recordedRequestMismatch };
129
+ //# sourceMappingURL=fixture-route.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixture-route.d.mts","names":[],"sources":["../src/fixture-route.ts"],"mappings":";;;;;;KAsDY,gBAAA;EAAA,SACD,SAAA;EAAA,SACA,MAAA;EAAA,SACA,OAAA;IAAA,SACE,MAAA;IAAA,SACA,IAAA,UAKO;IAAA,SAHP,KAAA,UAOS;IAAA,SALT,OAAA,EAAS,QAAA,CAAS,MAAA;IAAA,SAClB,IAAA,GAAO,MAAA,CAAO,IAAA;EAAA;EAAA,SAEhB,QAAA;IAAA,SACE,MAAA;IAAA,SACA,OAAA,EAAS,QAAA,CAAS,MAAA,mBAVlB;IAAA,SAYA,QAAA,WATA;IAAA,SAWA,MAAA,EAAQ,aAAA;EAAA;AAAA;;;;;;cA0BR,iBAAA,EAAiB,MAAA,CAAA,KAAA,WAAA,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;KAMlB,iBAAA,UAA2B,iBAAA,CAAkB,IAAI;AAAA,KAEjD,qBAAA,GAAwB,iBAAiB;;KAGzC,wBAAA;EAAA,SAAsC,KAAA,EAAO,MAAA,CAAO,IAAI;AAAA;;KAGxD,wBAAA,GAA2B,qBAAA,GAAwB,wBAAwB;AAAA,KAE3E,uBAAA;6EAED,GAAA;EAAA,SACA,MAAA;EAAA,SACA,IAAA;WAEA,KAAA;WAEA,IAAA,GAAO,MAAA,CAAO,IAAA;EAAA,SACd,KAAA;WAEA,gBAAA;WAEA,OAAA,EAAS,QAAA,CAAS,MAAA;WAElB,SAAA;WAEA,WAAA,WAjCmB;EAAA,SAmCnB,KAAA,GAAQ,qBAAA,EAnCW;EAAA,SAqCnB,UAAA,WArCmB;EAAA,SAuCnB,aAAA;WAEA,QAAA;WAEA,QAAA,EAAU,gBAAA;EAAA,SACV,MAAA;WAEA,UAAA;AAAA;AAAA,KAGC,oBAAA,GAAuB,WAAA,CACjC,wBAAA,EACA,iBAAA,EACA,uBAAA;;KAIU,sBAAA;yCAED,IAAA;WAEA,MAAA;EAAA,SACA,OAAA,IAAW,KAAA;AAAA;AAAA,KAGV,kBAAA;EAAA,SACD,MAAA;EAAA,SACA,IAAA;WAEA,MAAA,EAAQ,aAAA,CAAc,qBAAA;WAEtB,UAAA,EAAY,aAAA,CAAc,gBAAA;WAE1B,UAAA;WAEA,OAAA,EAAS,aAAA,CAAc,sBAAA;;;;;WAKvB,eAAA;IAAA,SAA6B,WAAA,EAAa,MAAA,CAAO,IAAA;EAAA;WAEjD,YAAA,GAAe,KAAA,oBAAyB,MAAA,aAAmB,KAAA;AAAA;;cAIzD,mBAAA,GAAuB,MAAA,aAAiB,QACuC;;;;;cAwB/E,uBAAA,GACX,MAAA,EAAQ,MAAA,CAAO,IAAA,cACf,QAAA,EAAU,MAAA,CAAO,IAAI,EACrB,IAAA,WACA,GAAA;;cAgEW,oBAAA,GACX,MAAA,EAAQ,MAAA,CAAO,IAAA,EACf,QAAA,EAAU,MAAA,CAAO,IAAI,EACrB,IAAA;;;;;;;;;;;;cA0EW,wBAAA,GAA4B,MAAA,EAAQ,kBAAA,KAAqB,oBA+MrE"}
@@ -0,0 +1,257 @@
1
+ import { emulatorRouteKey } from "./route-evidence.mjs";
2
+ import { EmulatorErrorAfterChunksFault, EmulatorScriptedError, EmulatorStatusFault, EmulatorTruncateAfterChunksFault, isJsonObject, jsonResponse, makeEmulatorKernel, parseJson, readText, stringField } from "./emulator-kernel.mjs";
3
+ import { Predicate } from "effect";
4
+ import * as Schema from "effect/Schema";
5
+ //#region src/fixture-route.ts
6
+ /**
7
+ * Fixture-only route core (internal; not a package export).
8
+ *
9
+ * The owner rule for every route added after the model-route emulators (the four OpenCode Go
10
+ * routes and the Claude, Codex, and Grok subscription-usage routes): response behaviour comes only
11
+ * from the committed conformance fixtures.
12
+ *
13
+ * - A request that matches a recorded request's shape (within the documented request-shape
14
+ * latitude below) gets that recording's response, copied as data (`*-recordings.ts`; never an
15
+ * SDK import).
16
+ * - Everything else answers one ledgered 400 not-emulated: unknown routes, other methods, missing
17
+ * or invalid credentials, missing or other headers and query parameters, unknown models, other
18
+ * request shapes (non-streamed modes, tools, reasoning, other message kinds, extra fields).
19
+ * - Test controls, as far as the other emulators allow them: the shared kernel faults (`status`,
20
+ * `error-after-chunks`, `truncate-after-chunks`) and scripted error turns; a route with a
21
+ * replaceable JSON body (the usage routes) also takes a scripted `{ usage }` body (or a default
22
+ * override) whose JSON shape (object keys and value kinds) equals the recording's. Fault and
23
+ * scripted-error statuses must be 400-599, so no control can produce a success status or
24
+ * a body other than the recorded one (or a prefix of it cut by the truncation faults).
25
+ *
26
+ * Request-shape latitude (accepted, harmless): any credential value (never checked or stored);
27
+ * extra request headers; key order; every string value except the discriminators `model`, `role`,
28
+ * `type`, and `phase` (which must equal the recording); any positive integer where the recording
29
+ * has a number (the output-token limit). Array lengths, object keys, booleans (`stream`, `store`,
30
+ * `include_usage`, `parallel_tool_calls`, `additionalProperties`), the recorded `accept` value,
31
+ * the `content-type` media type, and the query string (exactly, byte for byte; a bare `?` counts as no query) must equal the
32
+ * recording. Route-specific header values (for example Grok's `x-grok-client-version`) are
33
+ * documented by the route that declares them.
34
+ *
35
+ * Runtime-portable Web APIs only; no Node builtins and no SDK imports.
36
+ */
37
+ const FixtureRouteErrorStatus = Schema.Int.check(Schema.isBetween({
38
+ minimum: 400,
39
+ maximum: 599
40
+ }));
41
+ const FixtureRouteStatusFault = Schema.Struct({
42
+ ...EmulatorStatusFault.fields,
43
+ status: FixtureRouteErrorStatus
44
+ });
45
+ const FixtureRouteScriptedError = Schema.Struct({ error: Schema.Struct({
46
+ ...EmulatorScriptedError.fields.error.fields,
47
+ status: FixtureRouteErrorStatus
48
+ }) });
49
+ /**
50
+ * Faults: the shared kernel kinds only, with `status` limited to 400-599. `status` without a
51
+ * `body` answers an emulator-fault body (`{ error: { type: 'emulator_fault', message } }`), never
52
+ * a guessed provider envelope.
53
+ */
54
+ const FixtureRouteFault = Schema.Union([
55
+ FixtureRouteStatusFault,
56
+ EmulatorErrorAfterChunksFault,
57
+ EmulatorTruncateAfterChunksFault
58
+ ]);
59
+ /** The one answer for everything no fixture records. */
60
+ const notEmulatedResponse = (reason) => jsonResponse(400, { error: {
61
+ type: "not_emulated",
62
+ message: `Not emulated: ${reason}`
63
+ } });
64
+ const faultBody = (status) => ({ error: {
65
+ type: "emulator_fault",
66
+ message: `Emulator fault: status ${status}.`
67
+ } });
68
+ const discriminators = [
69
+ "model",
70
+ "role",
71
+ "type",
72
+ "phase"
73
+ ];
74
+ const kindOf = (value) => {
75
+ if (value === null) return "null";
76
+ if (Array.isArray(value)) return "array";
77
+ if (Predicate.isString(value)) return "string";
78
+ if (Predicate.isNumber(value)) return "number";
79
+ return Predicate.isBoolean(value) ? "boolean" : "object";
80
+ };
81
+ /**
82
+ * Where `actual` leaves the recorded request shape (the documented latitude applied), or
83
+ * undefined when it matches.
84
+ */
85
+ const recordedRequestMismatch = (actual, recorded, path = "$", key = "") => {
86
+ if (actual === void 0) return `${path} is missing`;
87
+ if (isJsonObject(recorded)) {
88
+ if (!isJsonObject(actual)) return `${path} is not an object`;
89
+ const recordedKeys = Object.keys(recorded).sort();
90
+ const actualKeys = Object.keys(actual).sort();
91
+ const extra = actualKeys.filter((name) => !recordedKeys.includes(name));
92
+ const missing = recordedKeys.filter((name) => !actualKeys.includes(name));
93
+ if (extra.length > 0) return `${path} has unrecorded field(s) ${extra.join(", ")}`;
94
+ if (missing.length > 0) return `${path} lacks recorded field(s) ${missing.join(", ")}`;
95
+ for (const name of recordedKeys) {
96
+ const field = recorded[name];
97
+ if (field === void 0) continue;
98
+ const mismatch = recordedRequestMismatch(actual[name], field, `${path}.${name}`, name);
99
+ if (mismatch !== void 0) return mismatch;
100
+ }
101
+ return;
102
+ }
103
+ if (Array.isArray(recorded)) {
104
+ if (!Array.isArray(actual)) return `${path} is not an array`;
105
+ if (actual.length !== recorded.length) return `${path} has ${actual.length} item(s), the recording ${recorded.length}`;
106
+ for (const [index, item] of recorded.entries()) {
107
+ const mismatch = recordedRequestMismatch(actual[index], item, `${path}[${index}]`, key);
108
+ if (mismatch !== void 0) return mismatch;
109
+ }
110
+ return;
111
+ }
112
+ if (kindOf(actual) !== kindOf(recorded)) return `${path} is not a ${kindOf(recorded)}`;
113
+ if (Predicate.isString(recorded)) return discriminators.includes(key) && actual !== recorded ? `${path} is not the recorded ${key}` : void 0;
114
+ if (Predicate.isNumber(recorded)) return Predicate.isNumber(actual) && Number.isSafeInteger(actual) && actual > 0 ? void 0 : `${path} is not a positive integer`;
115
+ return actual === recorded ? void 0 : `${path} is not the recorded value`;
116
+ };
117
+ /** Where `actual` leaves the recorded JSON shape (object keys and value kinds), or undefined. */
118
+ const recordedBodyMismatch = (actual, recorded, path = "$") => {
119
+ if (isJsonObject(recorded)) {
120
+ if (!isJsonObject(actual)) return `${path} is not an object`;
121
+ const recordedKeys = Object.keys(recorded).sort();
122
+ if (JSON.stringify(Object.keys(actual).sort()) !== JSON.stringify(recordedKeys)) return `${path} does not have the recorded fields ${recordedKeys.join(", ")}`;
123
+ for (const name of recordedKeys) {
124
+ const actualField = actual[name];
125
+ const recordedField = recorded[name];
126
+ if (actualField === void 0 || recordedField === void 0) continue;
127
+ const mismatch = recordedBodyMismatch(actualField, recordedField, `${path}.${name}`);
128
+ if (mismatch !== void 0) return mismatch;
129
+ }
130
+ return;
131
+ }
132
+ if (Array.isArray(recorded)) {
133
+ if (!Array.isArray(actual) || actual.length !== recorded.length) return `${path} is not a ${recorded.length}-item array`;
134
+ for (const [index, item] of recorded.entries()) {
135
+ const actualItem = actual[index];
136
+ if (actualItem === void 0) return `${path}[${index}] is missing`;
137
+ const mismatch = recordedBodyMismatch(actualItem, item, `${path}[${index}]`);
138
+ if (mismatch !== void 0) return mismatch;
139
+ }
140
+ return;
141
+ }
142
+ return kindOf(actual) === kindOf(recorded) ? void 0 : `${path} is not a ${kindOf(recorded)}`;
143
+ };
144
+ const bearerPattern = /^bearer\s+\S+/i;
145
+ const mediaType = (value) => value?.split(";")[0]?.trim().toLowerCase();
146
+ /**
147
+ * Create a fixture-only route. Each call has independent ledger, fault, and script state. Throws
148
+ * `inputInvalid('turn', ...)` when a replaceable body's `defaultBody` does not have the recorded
149
+ * shape.
150
+ *
151
+ * Precedence per request: the credential, the header rules in order, the recorded headers and
152
+ * query, the recorded request shape (one of `recordings`); a request failing any of these answers
153
+ * 400 not-emulated and uses up no fault or turn. Then the first matching fault if it is a `status`
154
+ * fault, then the next scripted turn, then the recorded response; a first matching body fault then
155
+ * shapes the body.
156
+ */
157
+ const makeFixtureRouteEmulator = (config) => {
158
+ const [primary] = config.recordings;
159
+ if (primary === void 0) throw config.inputInvalid("turn", "a fixture route needs at least one recording");
160
+ const recordedBody = config.replaceableBody === void 0 ? void 0 : parseJson(primary.response.chunks.join(""));
161
+ const bodyMismatch = (value) => recordedBody === void 0 ? "this route has no replaceable body" : recordedBodyMismatch(value, recordedBody);
162
+ const defaultBody = config.replaceableBody?.defaultBody;
163
+ if (defaultBody !== void 0) {
164
+ const mismatch = bodyMismatch(defaultBody);
165
+ if (mismatch !== void 0) throw config.inputInvalid("turn", `the default body must have the recorded shape: ${mismatch}`);
166
+ }
167
+ const ScriptedBody = Schema.Struct({ usage: Schema.Json.check(Schema.makeFilter((value) => {
168
+ const mismatch = bodyMismatch(value);
169
+ return mismatch === void 0 ? true : `must have the recorded shape: ${mismatch}`;
170
+ })) });
171
+ const turnSchema = config.replaceableBody === void 0 ? FixtureRouteScriptedError : Schema.Union([FixtureRouteScriptedError, ScriptedBody]);
172
+ const models = config.recordings.flatMap((recording) => {
173
+ const model = stringField(recording.request.body, "model");
174
+ return model === void 0 ? [] : [model];
175
+ });
176
+ const kernel = makeEmulatorKernel({
177
+ routes: config.routes,
178
+ faultSchema: FixtureRouteFault,
179
+ turnSchema,
180
+ newEntry: (base) => ({
181
+ ...base,
182
+ headers: {}
183
+ }),
184
+ snapshotEntry: (entry) => ({
185
+ ...entry,
186
+ headers: { ...entry.headers }
187
+ }),
188
+ unknownRoute: (entry) => {
189
+ entry.notEmulated = "no recorded route for this method and path";
190
+ return notEmulatedResponse(entry.notEmulated);
191
+ },
192
+ stateFields: () => ({ knownModels: [...new Set(models)] }),
193
+ inputInvalid: config.inputInvalid
194
+ });
195
+ const recordingMismatch = (recording, request, url, text, json) => {
196
+ for (const [name, value] of Object.entries(recording.request.headers)) {
197
+ const actual = request.headers.get(name);
198
+ if (!(name === "content-type" ? mediaType(actual) === mediaType(value) : actual === value)) return `the ${name} header is not the recorded ${value}`;
199
+ }
200
+ if (url.search !== recording.request.query) return recording.request.query.length === 0 ? "the recording has no query parameters" : `the query is not the recorded ${recording.request.query}`;
201
+ if (recording.request.body === void 0) return text.length === 0 ? void 0 : "the recording has no request body";
202
+ if (json === void 0) return "the request body is not JSON";
203
+ return recordedRequestMismatch(json, recording.request.body);
204
+ };
205
+ const admit = async (request, entry) => {
206
+ if (!(config.credential === "bearer" ? bearerPattern.test(request.headers.get("authorization") ?? "") : (request.headers.get("x-api-key") ?? "").trim().length > 0)) return { reason: config.credential === "bearer" ? "a non-empty Authorization: Bearer credential is required" : "a non-empty x-api-key credential is required" };
207
+ entry.credentialHeader = config.credential === "bearer" ? "authorization" : "x-api-key";
208
+ for (const rule of config.headers) {
209
+ const value = (request.headers.get(rule.name) ?? "").trim();
210
+ if (value.length === 0 || rule.accepts !== void 0 && !rule.accepts(value)) return { reason: `the ${rule.name} header is missing or not the one the SDK sends` };
211
+ }
212
+ const url = new URL(request.url);
213
+ const text = await readText(request) ?? "";
214
+ const json = config.method === "POST" ? parseJson(text) : void 0;
215
+ if (json !== void 0) entry.body = json;
216
+ const model = stringField(json, "model");
217
+ if (model !== void 0) entry.model = model;
218
+ const reasons = [];
219
+ for (const recording of config.recordings) {
220
+ const reason = recordingMismatch(recording, request, url, text, json);
221
+ if (reason === void 0) return { recording };
222
+ reasons.push(config.recordings.length === 1 ? reason : `${recording.caseId}: ${reason}`);
223
+ }
224
+ return { reason: reasons.join("; ") };
225
+ };
226
+ const handle = async (request, entry, path) => {
227
+ for (const rule of config.headers) {
228
+ const value = request.headers.get(rule.name);
229
+ if (rule.record && value !== null) entry.headers[rule.name] = value;
230
+ }
231
+ const search = new URL(request.url).search;
232
+ if (search.length > 0) entry.query = search;
233
+ const admitted = await admit(request, entry);
234
+ if ("reason" in admitted) {
235
+ entry.notEmulated = admitted.reason;
236
+ entry.status = 400;
237
+ return notEmulatedResponse(admitted.reason);
238
+ }
239
+ const recording = admitted.recording;
240
+ const fault = kernel.takeFault(path, entry.model);
241
+ if (fault !== void 0 && fault.fault.kind === "status") return kernel.respondWithStatusFault(entry, fault, fault.fault, faultBody);
242
+ const turn = kernel.nextTurn();
243
+ if (turn !== void 0 && "error" in turn) {
244
+ entry.scripted = "error";
245
+ return kernel.respondWithScriptedError(entry, turn.error, fault);
246
+ }
247
+ const body = turn === void 0 ? defaultBody : turn.usage;
248
+ if (turn !== void 0) entry.scripted = "usage";
249
+ entry.recording = recording.fixtureId;
250
+ return kernel.respondWithBody(entry, recording.response.status, { ...recording.response.headers }, body === void 0 ? recording.response.chunks : [JSON.stringify(body)], fault);
251
+ };
252
+ return kernel.serve(new Map([[emulatorRouteKey(config.method, config.path), handle]]));
253
+ };
254
+ //#endregion
255
+ export { FixtureRouteFault, makeFixtureRouteEmulator, notEmulatedResponse, recordedBodyMismatch, recordedRequestMismatch };
256
+
257
+ //# sourceMappingURL=fixture-route.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixture-route.mjs","names":[],"sources":["../src/fixture-route.ts"],"sourcesContent":["/**\n * Fixture-only route core (internal; not a package export).\n *\n * The owner rule for every route added after the model-route emulators (the four OpenCode Go\n * routes and the Claude, Codex, and Grok subscription-usage routes): response behaviour comes only\n * from the committed conformance fixtures.\n *\n * - A request that matches a recorded request's shape (within the documented request-shape\n * latitude below) gets that recording's response, copied as data (`*-recordings.ts`; never an\n * SDK import).\n * - Everything else answers one ledgered 400 not-emulated: unknown routes, other methods, missing\n * or invalid credentials, missing or other headers and query parameters, unknown models, other\n * request shapes (non-streamed modes, tools, reasoning, other message kinds, extra fields).\n * - Test controls, as far as the other emulators allow them: the shared kernel faults (`status`,\n * `error-after-chunks`, `truncate-after-chunks`) and scripted error turns; a route with a\n * replaceable JSON body (the usage routes) also takes a scripted `{ usage }` body (or a default\n * override) whose JSON shape (object keys and value kinds) equals the recording's. Fault and\n * scripted-error statuses must be 400-599, so no control can produce a success status or\n * a body other than the recorded one (or a prefix of it cut by the truncation faults).\n *\n * Request-shape latitude (accepted, harmless): any credential value (never checked or stored);\n * extra request headers; key order; every string value except the discriminators `model`, `role`,\n * `type`, and `phase` (which must equal the recording); any positive integer where the recording\n * has a number (the output-token limit). Array lengths, object keys, booleans (`stream`, `store`,\n * `include_usage`, `parallel_tool_calls`, `additionalProperties`), the recorded `accept` value,\n * the `content-type` media type, and the query string (exactly, byte for byte; a bare `?` counts as no query) must equal the\n * recording. Route-specific header values (for example Grok's `x-grok-client-version`) are\n * documented by the route that declares them.\n *\n * Runtime-portable Web APIs only; no Node builtins and no SDK imports.\n */\nimport { Predicate } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport {\n EmulatorErrorAfterChunksFault,\n EmulatorScriptedError,\n EmulatorStatusFault,\n EmulatorTruncateAfterChunksFault,\n isJsonObject,\n jsonResponse,\n makeEmulatorKernel,\n parseJson,\n readText,\n stringField,\n type EmulatorApi,\n type KernelLedgerEntry\n} from './emulator-kernel.ts'\nimport {\n emulatorRouteKey,\n type EmulatorEvidence,\n type EmulatorRouteEvidence\n} from './route-evidence.ts'\n\n/** One committed fixture exchange, copied as data. */\nexport type FixtureRecording = {\n readonly fixtureId: string\n readonly caseId: string\n readonly request: {\n readonly method: string\n readonly path: string\n /** The recorded query string (with `?`), or empty. */\n readonly query: string\n /** The recorded (non-credential) request headers, lower-case. */\n readonly headers: Readonly<Record<string, string>>\n readonly body?: Schema.Json\n }\n readonly response: {\n readonly status: number\n readonly headers: Readonly<Record<string, string>>\n /** Whether the fixture recorded network chunks (a stream) or one body. */\n readonly streamed: boolean\n /** Network chunks as recorded; a body is one chunk. */\n readonly chunks: ReadonlyArray<string>\n }\n}\n\n// Fault and scripted-error statuses on fixture-only routes: errors only (400-599), so no control\n// answers a success that no fixture records. One range check, so a rejection names the range\n// actually accepted (400-599 holds no 204, 205, or 3xx, which the shared schema excludes).\nconst FixtureRouteErrorStatus = Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 }))\n\nconst FixtureRouteStatusFault = Schema.Struct({\n ...EmulatorStatusFault.fields,\n status: FixtureRouteErrorStatus\n})\n\nconst FixtureRouteScriptedError = Schema.Struct({\n error: Schema.Struct({\n ...EmulatorScriptedError.fields.error.fields,\n status: FixtureRouteErrorStatus\n })\n})\n\n/**\n * Faults: the shared kernel kinds only, with `status` limited to 400-599. `status` without a\n * `body` answers an emulator-fault body (`{ error: { type: 'emulator_fault', message } }`), never\n * a guessed provider envelope.\n */\nexport const FixtureRouteFault = Schema.Union([\n FixtureRouteStatusFault,\n EmulatorErrorAfterChunksFault,\n EmulatorTruncateAfterChunksFault\n])\n\nexport type FixtureRouteFault = typeof FixtureRouteFault.Type\n\nexport type FixtureRouteFaultKind = FixtureRouteFault['kind']\n\n/** A scripted replacement body (replaceable-body routes only), shape-checked against the recording. */\nexport type FixtureRouteScriptedBody = { readonly usage: Schema.Json }\n\n/** A scripted turn: an error response, or (replaceable-body routes only) a replacement body. */\nexport type FixtureRouteScriptedTurn = EmulatorScriptedError | FixtureRouteScriptedBody\n\nexport type FixtureRouteLedgerEntry = {\n /** 1-based arrival order since the last ledger clear or reset. */\n readonly seq: number\n readonly method: string\n readonly path: string\n /** The request's query string (with `?`), when it has one. */\n readonly query?: string\n /** Parsed JSON request body, when it was valid JSON. */\n readonly body?: Schema.Json\n readonly model?: string\n /** Which credential header carried a non-empty value (the value is never recorded). */\n readonly credentialHeader?: 'authorization' | 'x-api-key'\n /** Values of the non-credential headers the route records (lower-case names). */\n readonly headers: Readonly<Record<string, string>>\n /** The fixture id whose recorded response answered the request. */\n readonly recording?: string\n /** Why the request was answered 400 not-emulated. */\n readonly notEmulated?: string\n /** Kind of the fault that shaped the response. */\n readonly fault?: FixtureRouteFaultKind\n /** Why a matching fault could not take effect (the response was a 500 emulator error). */\n readonly faultError?: string\n /** Set when the emulator could not build the planned response (answered 500). */\n readonly responseError?: string\n /** Set when a scripted turn answered the request. */\n readonly scripted?: 'usage' | 'error'\n /** Evidence of the matched route; `unknown-route` for requests on no route. */\n readonly evidence: EmulatorEvidence | 'unknown-route'\n readonly status: number\n /** Body chunks handed to the transport so far (each produced when pulled). */\n readonly bodyChunks: number\n}\n\nexport type FixtureRouteEmulator = EmulatorApi<\n FixtureRouteScriptedTurn,\n FixtureRouteFault,\n FixtureRouteLedgerEntry\n>\n\n/** A header every request must carry (non-empty and, when set, accepted by `accepts`). */\nexport type FixtureRouteHeaderRule = {\n /** Lower-case header name. */\n readonly name: string\n /** Keep the value in the ledger (never for credential or account headers). */\n readonly record: boolean\n readonly accepts?: (value: string) => boolean\n}\n\nexport type FixtureRouteConfig = {\n readonly method: 'GET' | 'POST'\n readonly path: string\n /** Route evidence manifest; must list exactly this route. */\n readonly routes: ReadonlyArray<EmulatorRouteEvidence>\n /** The recordings this route answers from (at least one), tried in order. */\n readonly recordings: ReadonlyArray<FixtureRecording>\n /** The credential header the SDK sends: a non-empty Bearer, or a non-empty `x-api-key`. */\n readonly credential: 'bearer' | 'x-api-key'\n /** Headers the SDK sends besides the credential and the recorded ones, required in order. */\n readonly headers: ReadonlyArray<FixtureRouteHeaderRule>\n /**\n * Replaceable JSON body (usage routes): enables the scripted `{ usage }` turn, and\n * `defaultBody` replaces the recorded body; both must have the recorded body's JSON shape.\n */\n readonly replaceableBody?: { readonly defaultBody: Schema.Json | undefined }\n /** Builds the error thrown by the JS API (and at construction) for invalid input. */\n readonly inputInvalid: (input: 'fault' | 'turn', reason: string) => Error\n}\n\n/** The one answer for everything no fixture records. */\nexport const notEmulatedResponse = (reason: string): Response =>\n jsonResponse(400, { error: { type: 'not_emulated', message: `Not emulated: ${reason}` } })\n\nconst faultBody = (status: number): Schema.Json => ({\n error: { type: 'emulator_fault', message: `Emulator fault: status ${status}.` }\n})\n\nconst discriminators: ReadonlyArray<string> = ['model', 'role', 'type', 'phase']\n\nconst kindOf = (value: Schema.Json): string => {\n if (value === null) return 'null'\n\n if (Array.isArray(value)) return 'array'\n\n if (Predicate.isString(value)) return 'string'\n\n if (Predicate.isNumber(value)) return 'number'\n\n return Predicate.isBoolean(value) ? 'boolean' : 'object'\n}\n\n/**\n * Where `actual` leaves the recorded request shape (the documented latitude applied), or\n * undefined when it matches.\n */\nexport const recordedRequestMismatch = (\n actual: Schema.Json | undefined,\n recorded: Schema.Json,\n path = '$',\n key = ''\n): string | undefined => {\n if (actual === undefined) return `${path} is missing`\n\n if (isJsonObject(recorded)) {\n if (!isJsonObject(actual)) return `${path} is not an object`\n\n const recordedKeys = Object.keys(recorded).sort()\n const actualKeys = Object.keys(actual).sort()\n\n const extra = actualKeys.filter(name => !recordedKeys.includes(name))\n const missing = recordedKeys.filter(name => !actualKeys.includes(name))\n\n if (extra.length > 0) return `${path} has unrecorded field(s) ${extra.join(', ')}`\n\n if (missing.length > 0) return `${path} lacks recorded field(s) ${missing.join(', ')}`\n\n for (const name of recordedKeys) {\n const field = recorded[name]\n\n if (field === undefined) continue\n\n const mismatch = recordedRequestMismatch(actual[name], field, `${path}.${name}`, name)\n\n if (mismatch !== undefined) return mismatch\n }\n\n return undefined\n }\n\n if (Array.isArray(recorded)) {\n if (!Array.isArray(actual)) return `${path} is not an array`\n\n if (actual.length !== recorded.length) {\n return `${path} has ${actual.length} item(s), the recording ${recorded.length}`\n }\n\n for (const [index, item] of recorded.entries()) {\n const mismatch = recordedRequestMismatch(actual[index], item, `${path}[${index}]`, key)\n\n if (mismatch !== undefined) return mismatch\n }\n\n return undefined\n }\n\n if (kindOf(actual) !== kindOf(recorded)) return `${path} is not a ${kindOf(recorded)}`\n\n if (Predicate.isString(recorded)) {\n return discriminators.includes(key) && actual !== recorded\n ? `${path} is not the recorded ${key}`\n : undefined\n }\n\n if (Predicate.isNumber(recorded)) {\n return Predicate.isNumber(actual) && Number.isSafeInteger(actual) && actual > 0\n ? undefined\n : `${path} is not a positive integer`\n }\n\n return actual === recorded ? undefined : `${path} is not the recorded value`\n}\n\n/** Where `actual` leaves the recorded JSON shape (object keys and value kinds), or undefined. */\nexport const recordedBodyMismatch = (\n actual: Schema.Json,\n recorded: Schema.Json,\n path = '$'\n): string | undefined => {\n if (isJsonObject(recorded)) {\n if (!isJsonObject(actual)) return `${path} is not an object`\n\n const recordedKeys = Object.keys(recorded).sort()\n\n if (JSON.stringify(Object.keys(actual).sort()) !== JSON.stringify(recordedKeys)) {\n return `${path} does not have the recorded fields ${recordedKeys.join(', ')}`\n }\n\n for (const name of recordedKeys) {\n const actualField = actual[name]\n const recordedField = recorded[name]\n\n if (actualField === undefined || recordedField === undefined) continue\n\n const mismatch = recordedBodyMismatch(actualField, recordedField, `${path}.${name}`)\n\n if (mismatch !== undefined) return mismatch\n }\n\n return undefined\n }\n\n if (Array.isArray(recorded)) {\n if (!Array.isArray(actual) || actual.length !== recorded.length) {\n return `${path} is not a ${recorded.length}-item array`\n }\n\n for (const [index, item] of recorded.entries()) {\n const actualItem = actual[index]\n\n if (actualItem === undefined) return `${path}[${index}] is missing`\n\n const mismatch = recordedBodyMismatch(actualItem, item, `${path}[${index}]`)\n\n if (mismatch !== undefined) return mismatch\n }\n\n return undefined\n }\n\n return kindOf(actual) === kindOf(recorded) ? undefined : `${path} is not a ${kindOf(recorded)}`\n}\n\n// A non-empty bearer credential. The value is never checked or stored.\nconst bearerPattern = /^bearer\\s+\\S+/i\n\nconst mediaType = (value: string | null): string | undefined =>\n value?.split(';')[0]?.trim().toLowerCase()\n\ntype MutableLedgerEntry = KernelLedgerEntry<FixtureRouteFaultKind> & {\n query?: string\n body?: Schema.Json\n model?: string\n credentialHeader?: 'authorization' | 'x-api-key'\n headers: Record<string, string>\n recording?: string\n notEmulated?: string\n scripted?: 'usage' | 'error'\n}\n\n/**\n * Create a fixture-only route. Each call has independent ledger, fault, and script state. Throws\n * `inputInvalid('turn', ...)` when a replaceable body's `defaultBody` does not have the recorded\n * shape.\n *\n * Precedence per request: the credential, the header rules in order, the recorded headers and\n * query, the recorded request shape (one of `recordings`); a request failing any of these answers\n * 400 not-emulated and uses up no fault or turn. Then the first matching fault if it is a `status`\n * fault, then the next scripted turn, then the recorded response; a first matching body fault then\n * shapes the body.\n */\nexport const makeFixtureRouteEmulator = (config: FixtureRouteConfig): FixtureRouteEmulator => {\n const [primary] = config.recordings\n\n if (primary === undefined) {\n throw config.inputInvalid('turn', 'a fixture route needs at least one recording')\n }\n\n const recordedBody =\n config.replaceableBody === undefined ? undefined : parseJson(primary.response.chunks.join(''))\n\n const bodyMismatch = (value: Schema.Json): string | undefined =>\n recordedBody === undefined\n ? 'this route has no replaceable body'\n : recordedBodyMismatch(value, recordedBody)\n\n const defaultBody = config.replaceableBody?.defaultBody\n\n if (defaultBody !== undefined) {\n const mismatch = bodyMismatch(defaultBody)\n\n if (mismatch !== undefined) {\n throw config.inputInvalid(\n 'turn',\n `the default body must have the recorded shape: ${mismatch}`\n )\n }\n }\n\n const ScriptedBody = Schema.Struct({\n usage: Schema.Json.check(\n Schema.makeFilter(value => {\n const mismatch = bodyMismatch(value)\n\n return mismatch === undefined ? true : `must have the recorded shape: ${mismatch}`\n })\n )\n })\n\n const turnSchema: Schema.Decoder<FixtureRouteScriptedTurn> =\n config.replaceableBody === undefined\n ? FixtureRouteScriptedError\n : Schema.Union([FixtureRouteScriptedError, ScriptedBody])\n\n const models = config.recordings.flatMap(recording => {\n const model = stringField(recording.request.body, 'model')\n\n return model === undefined ? [] : [model]\n })\n\n const kernel = makeEmulatorKernel({\n routes: config.routes,\n faultSchema: FixtureRouteFault,\n turnSchema,\n newEntry: (base): MutableLedgerEntry => ({ ...base, headers: {} }),\n snapshotEntry: (entry): FixtureRouteLedgerEntry => ({\n ...entry,\n headers: { ...entry.headers }\n }),\n unknownRoute: entry => {\n entry.notEmulated = 'no recorded route for this method and path'\n\n return notEmulatedResponse(entry.notEmulated)\n },\n stateFields: () => ({ knownModels: [...new Set(models)] }),\n inputInvalid: config.inputInvalid\n })\n\n const recordingMismatch = (\n recording: FixtureRecording,\n request: Request,\n url: URL,\n text: string,\n json: Schema.Json | undefined\n ): string | undefined => {\n for (const [name, value] of Object.entries(recording.request.headers)) {\n const actual = request.headers.get(name)\n\n const matches =\n name === 'content-type' ? mediaType(actual) === mediaType(value) : actual === value\n\n if (!matches) return `the ${name} header is not the recorded ${value}`\n }\n\n if (url.search !== recording.request.query) {\n return recording.request.query.length === 0\n ? 'the recording has no query parameters'\n : `the query is not the recorded ${recording.request.query}`\n }\n\n if (recording.request.body === undefined) {\n return text.length === 0 ? undefined : 'the recording has no request body'\n }\n\n if (json === undefined) return 'the request body is not JSON'\n\n return recordedRequestMismatch(json, recording.request.body)\n }\n\n const admit = async (\n request: Request,\n entry: MutableLedgerEntry\n ): Promise<{ readonly recording: FixtureRecording } | { readonly reason: string }> => {\n const credential =\n config.credential === 'bearer'\n ? bearerPattern.test(request.headers.get('authorization') ?? '')\n : (request.headers.get('x-api-key') ?? '').trim().length > 0\n\n if (!credential) {\n return {\n reason:\n config.credential === 'bearer'\n ? 'a non-empty Authorization: Bearer credential is required'\n : 'a non-empty x-api-key credential is required'\n }\n }\n\n entry.credentialHeader = config.credential === 'bearer' ? 'authorization' : 'x-api-key'\n\n for (const rule of config.headers) {\n const value = (request.headers.get(rule.name) ?? '').trim()\n\n if (value.length === 0 || (rule.accepts !== undefined && !rule.accepts(value))) {\n return { reason: `the ${rule.name} header is missing or not the one the SDK sends` }\n }\n }\n\n const url = new URL(request.url)\n const text = (await readText(request)) ?? ''\n\n const json = config.method === 'POST' ? parseJson(text) : undefined\n\n if (json !== undefined) entry.body = json\n\n const model = stringField(json, 'model')\n\n if (model !== undefined) entry.model = model\n\n const reasons: Array<string> = []\n\n for (const recording of config.recordings) {\n const reason = recordingMismatch(recording, request, url, text, json)\n\n if (reason === undefined) return { recording }\n\n reasons.push(config.recordings.length === 1 ? reason : `${recording.caseId}: ${reason}`)\n }\n\n return { reason: reasons.join('; ') }\n }\n\n const handle = async (\n request: Request,\n entry: MutableLedgerEntry,\n path: string\n ): Promise<Response> => {\n for (const rule of config.headers) {\n const value = request.headers.get(rule.name)\n\n if (rule.record && value !== null) entry.headers[rule.name] = value\n }\n\n const search = new URL(request.url).search\n\n if (search.length > 0) entry.query = search\n\n const admitted = await admit(request, entry)\n\n if ('reason' in admitted) {\n entry.notEmulated = admitted.reason\n entry.status = 400\n\n return notEmulatedResponse(admitted.reason)\n }\n\n const recording = admitted.recording\n const fault = kernel.takeFault(path, entry.model)\n\n if (fault !== undefined && fault.fault.kind === 'status') {\n return kernel.respondWithStatusFault(entry, fault, fault.fault, faultBody)\n }\n\n const turn = kernel.nextTurn()\n\n if (turn !== undefined && 'error' in turn) {\n entry.scripted = 'error'\n\n return kernel.respondWithScriptedError(entry, turn.error, fault)\n }\n\n const body = turn === undefined ? defaultBody : turn.usage\n\n if (turn !== undefined) entry.scripted = 'usage'\n\n entry.recording = recording.fixtureId\n\n return kernel.respondWithBody(\n entry,\n recording.response.status,\n { ...recording.response.headers },\n body === undefined ? recording.response.chunks : [JSON.stringify(body)],\n fault\n )\n }\n\n // Every manifest route maps to its own handler; construction throws `EmulatorRouteUnmapped`\n // when a manifest route has no handler (or a handler has no manifest route).\n return kernel.serve(new Map([[emulatorRouteKey(config.method, config.path), handle]]))\n}\n"],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import { EmulatorRouteEvidence } from "../route-evidence.mjs";
2
+ import { FortnoxEmulatorState } from "./state.mjs";
3
+ import * as Schema from "effect/Schema";
4
+ import { Hono } from "@emulators/core";
5
+
6
+ //#region src/fortnox/api.d.ts
7
+ /**
8
+ * Drill knobs: each flips one observed quirk to the documented-but-wrong behavior, ONLY to prove
9
+ * that the conformance cases catch a disagreement. Defaults follow the observed behavior.
10
+ */
11
+ type FortnoxEmulatorQuirks = {
12
+ /** `false`: a positionally matched row that omits `Discount`/`DiscountType` resets them. */readonly stickyRowDiscount?: boolean; /** `true`: a customer update with `""` clears the stored string instead of keeping it. */
13
+ readonly emptyStringClears?: boolean; /** `true`: `unpaid`/`unpaidoverdue`/`fullypaid` also match unbooked invoices. */
14
+ readonly paymentFiltersIncludeUnbooked?: boolean;
15
+ };
16
+ type FortnoxApiEnv = {
17
+ /** Clock in epoch milliseconds (the `unpaidoverdue` "today", new invoice dates, the outbox). */readonly now: () => number; /** Origin used in `@url` links, for example `https://api.fortnox.se`. */
18
+ readonly linkOrigin: string;
19
+ readonly quirks: Required<FortnoxEmulatorQuirks>;
20
+ };
21
+ /**
22
+ * `ErrorInformation` codes the emulator answers with. The two customer-not-found codes are copied
23
+ * from the rejection fixture; the `2999xxx` codes are synthetic emulator codes.
24
+ */
25
+ declare const fortnoxEmulatorErrorCodes: {
26
+ readonly customerNotFound: 2000204;
27
+ readonly invoiceCustomerNotFound: 2000433;
28
+ readonly unauthorized: 2999001;
29
+ readonly invalidBody: 2999002;
30
+ readonly invalidField: 2999003;
31
+ readonly readOnlyField: 2999004;
32
+ readonly notEditable: 2999005;
33
+ readonly invoiceNotFound: 2999006;
34
+ readonly unknownRoute: 2999007;
35
+ readonly unsupportedQuery: 2999008;
36
+ readonly rateLimited: 2999009;
37
+ readonly upstreamError: 2999010;
38
+ };
39
+ /** Fortnox API base path. */
40
+ declare const fortnoxEmulatorBasePath = "/3";
41
+ /** A Fortnox `ErrorInformation` response with the lowercase fields of the rejection fixture. */
42
+ declare const errorInformation: (status: number, code: number, message: string) => Response;
43
+ type RouteRequest = {
44
+ readonly params: Readonly<Record<string, string>>;
45
+ readonly query: URLSearchParams; /** Parsed JSON body; `undefined` when absent or not JSON. */
46
+ readonly body: Schema.Json | undefined;
47
+ };
48
+ type RouteHandler = (state: FortnoxEmulatorState, request: RouteRequest, env: FortnoxApiEnv) => Response;
49
+ type FortnoxApiRoute = EmulatorRouteEvidence & {
50
+ /**
51
+ * Query parameters the route emulates; any other key answers 400 before the handler runs, so
52
+ * a rejected request never writes. Empty by default.
53
+ */
54
+ readonly queryKeys: ReadonlyArray<string>;
55
+ readonly handler: RouteHandler;
56
+ };
57
+ /** The route table: evidence plus handler. `fortnoxEmulatorRoutes` is its evidence part. */
58
+ declare const fortnoxApiRoutes: ReadonlyArray<FortnoxApiRoute>;
59
+ /** One observed Fortnox quirk the emulator implements, and the case that claims it. */
60
+ type FortnoxEmulatorQuirk = {
61
+ readonly id: string; /** `METHOD path` of the route that implements it. */
62
+ readonly route: string; /** The conformance case whose claim it follows; `undefined` when no case covers it yet. */
63
+ readonly caseId: string | undefined; /** The drill knob that flips it, if any. */
64
+ readonly knob: keyof FortnoxEmulatorQuirks | undefined;
65
+ readonly summary: string;
66
+ };
67
+ /** The observed quirks, each tied to its conformance case id. */
68
+ declare const fortnoxEmulatorQuirks: ReadonlyArray<FortnoxEmulatorQuirk>;
69
+ /** The route answering `method` + concrete `path`, or `undefined` (fail closed). */
70
+ declare const matchFortnoxRoute: (method: string, path: string) => FortnoxApiRoute | undefined;
71
+ /** Parsed JSON, or `undefined` for invalid JSON. */
72
+ declare const parseJsonText: (text: string) => Schema.Json | undefined;
73
+ /**
74
+ * The wrapper's fault decision for the core request of job `jobId` (its `emulatorJobHeader`),
75
+ * asked only once the route would answer successfully: `true` when a fault answers instead (it is
76
+ * used up, and the wrapper sends its answer), `false` when none applies.
77
+ */
78
+ type FortnoxFaultDecision = (jobId: string | undefined) => boolean;
79
+ /**
80
+ * Register every route of the table on the core app, over the generation's state. Each request is
81
+ * planned, then faulted, then committed: the handler runs on a draft of the state (handlers replace
82
+ * whole lists and counters, never edit them in place, so a shallow copy keeps every write off the
83
+ * state); a refusal (any answer that is not 2xx) is sent as is and uses up no fault; a successful
84
+ * answer asks `decideFault`, and only an unfaulted one commits the draft. The three steps run
85
+ * synchronously together, so no other request interleaves. A handler that throws answers
86
+ * `handlerFailedResponse()` (nothing written), which the wrapper turns into its `ErrorInformation`
87
+ * 500 with `responseError` in the ledger.
88
+ */
89
+ declare const registerFortnoxApi: (app: Hono, state: FortnoxEmulatorState, env: FortnoxApiEnv, decideFault: FortnoxFaultDecision) => void;
90
+ //#endregion
91
+ export { FortnoxApiEnv, FortnoxEmulatorQuirk, FortnoxEmulatorQuirks, FortnoxFaultDecision, errorInformation, fortnoxApiRoutes, fortnoxEmulatorBasePath, fortnoxEmulatorErrorCodes, fortnoxEmulatorQuirks, matchFortnoxRoute, parseJsonText, registerFortnoxApi };
92
+ //# sourceMappingURL=api.d.mts.map