@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,844 @@
1
+ /**
2
+ * Shared emulator kernel (internal; not a package export).
3
+ *
4
+ * The wire-independent half of every fetch-handler emulator: fault and scripted-turn state, the
5
+ * request ledger, pull-driven bodies with chunk faults, evidence tagging, route binding, and the
6
+ * `/_emulate/*` control plane. Each wire core (`chat-completions.ts`, `messages.ts`) supplies its
7
+ * route handlers, fault and turn schemas, ledger entry shape, and error bodies.
8
+ *
9
+ * Also holds small wire-independent helpers both cores use: JSON field access, synthetic values
10
+ * for a tool's JSON Schema, fragment splitting, and a rough token estimate.
11
+ *
12
+ * Runtime-portable Web APIs only (`Request`, `Response`, `ReadableStream`, `TextEncoder`, `URL`);
13
+ * no Node builtins and no SDK imports.
14
+ */
15
+ import { Predicate, Result } from 'effect'
16
+ import * as Schema from 'effect/Schema'
17
+ import { EmulatorHeaderRecord, EmulatorResponseStatus } from './emulator-http.ts'
18
+ import {
19
+ bindRouteHandlers,
20
+ emulatorEvidenceHeader,
21
+ type EmulatorEvidence,
22
+ type EmulatorRouteEvidence
23
+ } from './route-evidence.ts'
24
+
25
+ export const ChunkCount = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
26
+
27
+ const FaultCount = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
28
+
29
+ /** Optional fault filter; an omitted field matches every request. `path` ending in `*` is a prefix. */
30
+ export const EmulatorFaultMatch = Schema.Struct({
31
+ path: Schema.optionalKey(Schema.String),
32
+ model: Schema.optionalKey(Schema.String)
33
+ })
34
+
35
+ export type EmulatorFaultMatch = typeof EmulatorFaultMatch.Type
36
+
37
+ /** Fields every fault kind carries. */
38
+ export const emulatorFaultFields = {
39
+ match: Schema.optionalKey(EmulatorFaultMatch),
40
+ /** How many matching requests the fault answers. Omitted: every matching request. */
41
+ count: Schema.optionalKey(FaultCount)
42
+ }
43
+
44
+ /** Answer with this status, headers, and body instead of the planned response. */
45
+ export const EmulatorStatusFault = Schema.Struct({
46
+ kind: Schema.Literal('status'),
47
+ status: EmulatorResponseStatus,
48
+ headers: Schema.optionalKey(EmulatorHeaderRecord),
49
+ body: Schema.optionalKey(Schema.Json),
50
+ ...emulatorFaultFields
51
+ })
52
+
53
+ /** Send `chunks` body chunks, then error the body stream (a dropped connection). */
54
+ export const EmulatorErrorAfterChunksFault = Schema.Struct({
55
+ kind: Schema.Literal('error-after-chunks'),
56
+ chunks: ChunkCount,
57
+ ...emulatorFaultFields
58
+ })
59
+
60
+ /** Send `chunks` body chunks, then close the body cleanly (a truncated response). */
61
+ export const EmulatorTruncateAfterChunksFault = Schema.Struct({
62
+ kind: Schema.Literal('truncate-after-chunks'),
63
+ chunks: ChunkCount,
64
+ ...emulatorFaultFields
65
+ })
66
+
67
+ /** A scripted error response: status, body (a string is sent as is), and optional headers. */
68
+ export const EmulatorScriptedError = Schema.Struct({
69
+ error: Schema.Struct({
70
+ status: EmulatorResponseStatus,
71
+ body: Schema.Json,
72
+ headers: Schema.optionalKey(EmulatorHeaderRecord)
73
+ })
74
+ })
75
+
76
+ export type EmulatorScriptedError = typeof EmulatorScriptedError.Type
77
+
78
+ /** What the kernel needs from every fault: its kind and optional filter and count. */
79
+ export type KernelFault = {
80
+ readonly kind: string
81
+ readonly match?: EmulatorFaultMatch | undefined
82
+ readonly count?: number | undefined
83
+ }
84
+
85
+ type BodyChunkFault =
86
+ | typeof EmulatorErrorAfterChunksFault.Type
87
+ | typeof EmulatorTruncateAfterChunksFault.Type
88
+
89
+ export type EmulatorFaultState<Fault> = {
90
+ readonly id: number
91
+ readonly fault: Fault
92
+ /** Remaining matching requests; `undefined` for an unlimited fault. */
93
+ readonly remaining: number | undefined
94
+ readonly applied: number
95
+ }
96
+
97
+ type MutableFaultState<Fault> = {
98
+ readonly id: number
99
+ readonly fault: Fault
100
+ remaining: number | undefined
101
+ applied: number
102
+ }
103
+
104
+ export type EmulatorRouteCoverage = EmulatorRouteEvidence & {
105
+ /** Ledger requests answered by this route since the last ledger clear or reset. */
106
+ readonly requests: number
107
+ }
108
+
109
+ export type EmulatorCoverage = {
110
+ readonly routes: ReadonlyArray<EmulatorRouteCoverage>
111
+ /** Ledger requests to unknown routes (failed closed). */
112
+ readonly unknownRouteRequests: number
113
+ }
114
+
115
+ /** Fields an emulator lists first in `/_emulate/state`. */
116
+ export type EmulatorStateFields = {
117
+ readonly knownModels: ReadonlyArray<string>
118
+ /** Only for emulators that emulate streamed reasoning. */
119
+ readonly reasoningModels?: ReadonlyArray<string>
120
+ }
121
+
122
+ /** The ledger fields the kernel owns; each wire core extends it with its request fields. */
123
+ export type KernelLedgerEntry<FaultKind extends string> = {
124
+ seq: number
125
+ method: string
126
+ path: string
127
+ fault?: FaultKind
128
+ faultError?: string
129
+ responseError?: string
130
+ evidence: EmulatorEvidence | 'unknown-route'
131
+ status: number
132
+ bodyChunks: number
133
+ }
134
+
135
+ /** The public shape of every emulator built on the kernel. */
136
+ export type EmulatorApi<Turn, Fault, Entry> = {
137
+ /** The fetch handler (emulated API plus the `/_emulate/*` control plane). Never rejects. */
138
+ readonly fetch: (request: Request) => Promise<Response>
139
+ readonly ledger: {
140
+ readonly entries: () => ReadonlyArray<Entry>
141
+ readonly clear: () => void
142
+ }
143
+ /** Clear the ledger, faults, and scripted turns. */
144
+ readonly reset: () => void
145
+ readonly faults: {
146
+ /** Add a fault; throws the emulator's input-invalid error for an invalid fault. */
147
+ readonly add: (fault: Fault) => EmulatorFaultState<Fault>
148
+ readonly list: () => ReadonlyArray<EmulatorFaultState<Fault>>
149
+ readonly clear: () => void
150
+ }
151
+ readonly script: {
152
+ /** Queue a turn for the next emulated request; throws for an invalid turn. */
153
+ readonly enqueue: (turn: Turn) => void
154
+ readonly pending: () => number
155
+ readonly clear: () => void
156
+ }
157
+ readonly coverage: () => EmulatorCoverage
158
+ }
159
+
160
+ export type EmulatorKernelConfig<
161
+ Fault extends KernelFault,
162
+ Turn,
163
+ Entry extends KernelLedgerEntry<Fault['kind']>,
164
+ Snapshot
165
+ > = {
166
+ /** Route evidence manifest; `serve` needs exactly one handler per route. */
167
+ readonly routes: ReadonlyArray<EmulatorRouteEvidence>
168
+ /** Fault schema (decoded strictly). */
169
+ readonly faultSchema: Schema.Decoder<Fault>
170
+ /** Scripted-turn schema (decoded strictly). */
171
+ readonly turnSchema: Schema.Decoder<Turn>
172
+ /** Adds the wire core's own ledger fields to a fresh kernel entry. */
173
+ readonly newEntry: (base: KernelLedgerEntry<Fault['kind']>) => Entry
174
+ /** Copies an entry for the ledger API (never shares mutable state). */
175
+ readonly snapshotEntry: (entry: Entry) => Snapshot
176
+ /**
177
+ * Response for an unknown emulated route (fails closed, still written to the ledger with the
178
+ * status it answers); the core may add its own ledger fields to `entry`.
179
+ */
180
+ readonly unknownRoute: (entry: Entry) => Response
181
+ /** Fields listed first in `/_emulate/state`. */
182
+ readonly stateFields: () => EmulatorStateFields
183
+ /** Builds the error thrown by the JS API (`faults.add`, `script.enqueue`) for invalid input. */
184
+ readonly inputInvalid: (input: 'fault' | 'turn', reason: string) => Error
185
+ }
186
+
187
+ export type EmulatorRouteHandler<Entry> = (
188
+ request: Request,
189
+ entry: Entry,
190
+ path: string
191
+ ) => Promise<Response>
192
+
193
+ // Control inputs are strict: an unknown key (a typo, or an `error` turn with a bad field) is
194
+ // rejected instead of silently decoding as a different, all-optional shape.
195
+ const strict = { onExcessProperty: 'error' } as const
196
+
197
+ const decodeJsonText = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Json))
198
+
199
+ const issueMessage = (issue: Schema.SchemaError['issue']): string =>
200
+ new Schema.SchemaError(issue).message
201
+
202
+ export const isJsonObject = (value: Schema.Json | undefined): value is Schema.JsonObject =>
203
+ value !== null && value !== undefined && Predicate.isObject(value) && !Array.isArray(value)
204
+
205
+ export const jsonField = (value: Schema.Json | undefined, key: string): Schema.Json | undefined =>
206
+ isJsonObject(value) ? value[key] : undefined
207
+
208
+ export const stringField = (value: Schema.Json | undefined, key: string): string | undefined => {
209
+ const field = jsonField(value, key)
210
+
211
+ return Predicate.isString(field) ? field : undefined
212
+ }
213
+
214
+ const textEncoder = new TextEncoder()
215
+
216
+ /** A JSON response; `body` is any JSON-serializable value (ledger snapshots, fault states, envelopes). */
217
+ export const jsonResponse = (
218
+ status: number,
219
+ body: unknown,
220
+ headers: HeadersInit = {}
221
+ ): Response => {
222
+ const responseHeaders = new Headers(headers)
223
+
224
+ responseHeaders.set('content-type', 'application/json')
225
+
226
+ return new Response(JSON.stringify(body), { status, headers: responseHeaders })
227
+ }
228
+
229
+ /** An emulator (not emulated-service) error: control-plane problems and unbuildable responses. */
230
+ export const controlError = (
231
+ status: number,
232
+ message: string,
233
+ headers: HeadersInit = {}
234
+ ): Response => jsonResponse(status, { error: { message, type: 'emulator_error' } }, headers)
235
+
236
+ /** A scripted or fault body: a string is sent as is, anything else as JSON. */
237
+ export const bodyText = (body: Schema.Json): string =>
238
+ Predicate.isString(body) ? body : JSON.stringify(body)
239
+
240
+ /** Headers for a scripted or fault body, with a `content-type` matching the body when unset. */
241
+ const bodyHeaders = (headers: Readonly<Record<string, string>> | undefined, body: Schema.Json) => {
242
+ const result = new Headers(headers ?? {})
243
+
244
+ if (!result.has('content-type')) {
245
+ result.set('content-type', Predicate.isString(body) ? 'text/plain' : 'application/json')
246
+ }
247
+
248
+ return result
249
+ }
250
+
251
+ export const readText = (request: Request): Promise<string | undefined> =>
252
+ request.text().then(
253
+ text => text,
254
+ () => undefined
255
+ )
256
+
257
+ export const parseJson = (text: string): Schema.Json | undefined => {
258
+ const result = decodeJsonText(text)
259
+
260
+ return Result.isSuccess(result) ? result.success : undefined
261
+ }
262
+
263
+ const pathMatches = (pattern: string, path: string): boolean =>
264
+ pattern.endsWith('*') ? path.startsWith(pattern.slice(0, -1)) : pattern === path
265
+
266
+ const faultMatches = (fault: KernelFault, path: string, model: string | undefined): boolean =>
267
+ (fault.match?.path === undefined || pathMatches(fault.match.path, path)) &&
268
+ (fault.match?.model === undefined || fault.match.model === model)
269
+
270
+ const isBodyChunkFault = (fault: KernelFault): fault is BodyChunkFault =>
271
+ fault.kind === 'error-after-chunks' || fault.kind === 'truncate-after-chunks'
272
+
273
+ const chunkFaultProblem = (fault: BodyChunkFault, chunks: number): string | undefined => {
274
+ const applies =
275
+ fault.kind === 'error-after-chunks' ? fault.chunks <= chunks : fault.chunks < chunks
276
+
277
+ return applies
278
+ ? undefined
279
+ : `${fault.kind} after ${fault.chunks} chunk(s) cannot apply to a response with ${chunks} chunk(s)`
280
+ }
281
+
282
+ /**
283
+ * Strictly pull-driven body: chunk `k` is produced only when the consumer
284
+ * pulls it, and `entry.bodyChunks` counts the chunks handed over. The fault
285
+ * (already validated against the chunk count) applies when the consumer pulls
286
+ * past `fault.chunks` chunks.
287
+ */
288
+ const chunkedBody = (
289
+ chunks: ReadonlyArray<string>,
290
+ fault: BodyChunkFault | undefined,
291
+ entry: { bodyChunks: number }
292
+ ): ReadableStream<Uint8Array> => {
293
+ let next = 0
294
+
295
+ return new ReadableStream<Uint8Array>(
296
+ {
297
+ pull: controller => {
298
+ if (fault !== undefined && next === fault.chunks) {
299
+ if (fault.kind === 'error-after-chunks') {
300
+ controller.error(new Error(`synthetic mid-stream failure after ${next} chunk(s)`))
301
+ } else {
302
+ controller.close()
303
+ }
304
+
305
+ return
306
+ }
307
+
308
+ const chunk = chunks[next]
309
+
310
+ if (chunk === undefined) {
311
+ controller.close()
312
+
313
+ return
314
+ }
315
+
316
+ next += 1
317
+ entry.bodyChunks += 1
318
+ controller.enqueue(textEncoder.encode(chunk))
319
+ }
320
+ },
321
+ { highWaterMark: 0 }
322
+ )
323
+ }
324
+
325
+ const withEvidence = (response: Response, evidence: EmulatorEvidence): Response => {
326
+ if (evidence === 'unverified') {
327
+ response.headers.set(emulatorEvidenceHeader, 'unverified')
328
+ }
329
+
330
+ return response
331
+ }
332
+
333
+ const maxSchemaDepth = 8
334
+
335
+ /**
336
+ * A synthetic value for a JSON Schema: required object properties only,
337
+ * non-empty strings (or the first `enum` / `const` value), the minimum for
338
+ * numbers, `true` for booleans, and `minItems` synthetic items for arrays.
339
+ */
340
+ export const synthesizeValue = (
341
+ schema: Schema.Json | undefined,
342
+ name: string,
343
+ depth: number
344
+ ): Schema.Json => {
345
+ const constant = jsonField(schema, 'const')
346
+
347
+ if (constant !== undefined) {
348
+ return constant
349
+ }
350
+
351
+ const choices = jsonField(schema, 'enum')
352
+
353
+ if (Array.isArray(choices) && choices.length > 0) {
354
+ return choices[0] ?? null
355
+ }
356
+
357
+ for (const key of ['anyOf', 'oneOf', 'allOf']) {
358
+ const options = jsonField(schema, key)
359
+
360
+ if (Array.isArray(options) && options.length > 0) {
361
+ const preferred = options.find(option => stringField(option, 'type') !== 'null')
362
+
363
+ return synthesizeValue(preferred ?? options[0], name, depth + 1)
364
+ }
365
+ }
366
+
367
+ const declared = jsonField(schema, 'type')
368
+
369
+ const type = Array.isArray(declared)
370
+ ? declared.find(entry => Predicate.isString(entry) && entry !== 'null')
371
+ : declared
372
+
373
+ if (depth > maxSchemaDepth) {
374
+ return null
375
+ }
376
+
377
+ switch (type) {
378
+ case 'object': {
379
+ const properties = jsonField(schema, 'properties')
380
+ const required = jsonField(schema, 'required')
381
+ const value: Record<string, Schema.Json> = {}
382
+
383
+ for (const key of Array.isArray(required) ? required : []) {
384
+ if (Predicate.isString(key)) {
385
+ value[key] = synthesizeValue(jsonField(properties, key), key, depth + 1)
386
+ }
387
+ }
388
+
389
+ return value
390
+ }
391
+
392
+ case 'array': {
393
+ const minItems = jsonField(schema, 'minItems')
394
+ const count = Predicate.isNumber(minItems) && minItems > 0 ? Math.min(minItems, 3) : 0
395
+
396
+ return Array.from({ length: count }, () =>
397
+ synthesizeValue(jsonField(schema, 'items'), name, depth + 1)
398
+ )
399
+ }
400
+
401
+ case 'integer':
402
+ case 'number': {
403
+ const minimum = jsonField(schema, 'minimum')
404
+
405
+ return Predicate.isNumber(minimum) ? minimum : 1
406
+ }
407
+
408
+ case 'boolean':
409
+ return true
410
+ case 'null':
411
+ return null
412
+ default:
413
+ return `synthetic ${name}`
414
+ }
415
+ }
416
+
417
+ /** Split text into up to `parts` non-empty fragments (at least one). */
418
+ export const splitFragments = (text: string, parts: number): ReadonlyArray<string> => {
419
+ if (text.length <= 1) {
420
+ return [text]
421
+ }
422
+
423
+ const size = Math.ceil(text.length / Math.min(parts, text.length))
424
+ const fragments: Array<string> = []
425
+
426
+ for (let index = 0; index < text.length; index += size) {
427
+ fragments.push(text.slice(index, index + size))
428
+ }
429
+
430
+ return fragments
431
+ }
432
+
433
+ /** A rough synthetic token count (about four characters per token, at least one). */
434
+ export const approximateTokens = (text: string): number => Math.max(1, Math.ceil(text.length / 4))
435
+
436
+ /**
437
+ * Create the kernel of an emulator. Each call has independent ledger, fault,
438
+ * and script state. Handlers use the returned helpers, then `serve` binds
439
+ * them to the manifest (throwing `EmulatorRouteUnmapped` on a mismatch) and
440
+ * returns the emulator API.
441
+ */
442
+ export const makeEmulatorKernel = <
443
+ Fault extends KernelFault,
444
+ Turn,
445
+ Entry extends KernelLedgerEntry<Fault['kind']>,
446
+ Snapshot
447
+ >(
448
+ config: EmulatorKernelConfig<Fault, Turn, Entry, Snapshot>
449
+ ) => {
450
+ const decodeFault = Schema.decodeUnknownResult(config.faultSchema, strict)
451
+
452
+ const decodeFaultList = Schema.decodeUnknownResult(
453
+ Schema.Union([config.faultSchema, Schema.Struct({ faults: Schema.Array(config.faultSchema) })]),
454
+ strict
455
+ )
456
+
457
+ const decodeTurn = Schema.decodeUnknownResult(config.turnSchema, strict)
458
+
459
+ const decodeTurnList = Schema.decodeUnknownResult(
460
+ Schema.Union([Schema.Struct({ turns: Schema.Array(config.turnSchema) }), config.turnSchema]),
461
+ strict
462
+ )
463
+
464
+ let entries: Array<Entry> = []
465
+ let faultStates: Array<MutableFaultState<Fault>> = []
466
+ let turns: Array<Turn> = []
467
+ let nextSeq = 1
468
+ let nextFaultId = 1
469
+
470
+ const snapshotFault = (state: MutableFaultState<Fault>): EmulatorFaultState<Fault> => ({
471
+ ...state
472
+ })
473
+
474
+ const clearLedger = () => {
475
+ entries = []
476
+ nextSeq = 1
477
+ }
478
+
479
+ const addFault = (input: unknown): EmulatorFaultState<Fault> | string => {
480
+ const decoded = decodeFault(input)
481
+
482
+ if (Result.isFailure(decoded)) {
483
+ return issueMessage(decoded.failure.issue)
484
+ }
485
+
486
+ const state: MutableFaultState<Fault> = {
487
+ id: nextFaultId++,
488
+ fault: decoded.success,
489
+ remaining: decoded.success.count,
490
+ applied: 0
491
+ }
492
+
493
+ faultStates.push(state)
494
+
495
+ return snapshotFault(state)
496
+ }
497
+
498
+ const enqueueTurn = (input: unknown): string | undefined => {
499
+ const decoded = decodeTurn(input)
500
+
501
+ if (Result.isFailure(decoded)) {
502
+ return issueMessage(decoded.failure.issue)
503
+ }
504
+
505
+ turns.push(decoded.success)
506
+
507
+ return undefined
508
+ }
509
+
510
+ /** The first matching fault with uses left, in insertion order (not yet consumed). */
511
+ const takeFault = (
512
+ path: string,
513
+ model: string | undefined
514
+ ): MutableFaultState<Fault> | undefined =>
515
+ faultStates.find(
516
+ state =>
517
+ (state.remaining === undefined || state.remaining > 0) &&
518
+ faultMatches(state.fault, path, model)
519
+ )
520
+
521
+ /** Use up one application of a fault and record its kind in the ledger entry. */
522
+ const consumeFault = (state: MutableFaultState<Fault>, entry: Entry) => {
523
+ state.applied += 1
524
+ entry.fault = state.fault.kind
525
+
526
+ if (state.remaining !== undefined) {
527
+ state.remaining -= 1
528
+ }
529
+ }
530
+
531
+ /** The next scripted turn, used up when its request arrives. */
532
+ const nextTurn = (): Turn | undefined => turns.shift()
533
+
534
+ const coverage = (): EmulatorCoverage => ({
535
+ routes: config.routes.map(route => ({
536
+ ...route,
537
+ requests: entries.filter(
538
+ entry =>
539
+ entry.evidence !== 'unknown-route' &&
540
+ entry.method === route.method &&
541
+ entry.path === route.path
542
+ ).length
543
+ })),
544
+ unknownRouteRequests: entries.filter(entry => entry.evidence === 'unknown-route').length
545
+ })
546
+
547
+ /**
548
+ * Answer with a pull-driven body. A matching `error-after-chunks` or
549
+ * `truncate-after-chunks` fault shapes it; one that cannot take effect
550
+ * answers 500 and is not consumed. Other fault kinds are left to the caller.
551
+ */
552
+ const respondWithBody = (
553
+ entry: Entry,
554
+ status: number,
555
+ headers: HeadersInit,
556
+ chunks: ReadonlyArray<string>,
557
+ fault: MutableFaultState<Fault> | undefined
558
+ ): Response => {
559
+ const chunkFault =
560
+ fault === undefined || !isBodyChunkFault(fault.fault) ? undefined : fault.fault
561
+
562
+ if (chunkFault !== undefined) {
563
+ const problem = chunkFaultProblem(chunkFault, chunks.length)
564
+
565
+ if (problem !== undefined) {
566
+ entry.faultError = problem
567
+ entry.status = 500
568
+
569
+ return controlError(500, `emulator fault cannot apply: ${problem}`)
570
+ }
571
+ }
572
+
573
+ // Build the response before using up the fault: a response that cannot be built must not
574
+ // consume it.
575
+ const response = new Response(chunkedBody(chunks, chunkFault, entry), { status, headers })
576
+
577
+ if (chunkFault !== undefined && fault !== undefined) {
578
+ consumeFault(fault, entry)
579
+ }
580
+
581
+ entry.status = response.status
582
+
583
+ return response
584
+ }
585
+
586
+ /** Answer a `status` fault with its status, headers, and body (or `defaultBody`). */
587
+ const respondWithStatusFault = (
588
+ entry: Entry,
589
+ state: MutableFaultState<Fault>,
590
+ fault: typeof EmulatorStatusFault.Type,
591
+ defaultBody: (status: number) => Schema.Json
592
+ ): Response => {
593
+ const faultBody = fault.body ?? defaultBody(fault.status)
594
+
595
+ // Built first: a status fault whose response cannot be built is not used up.
596
+ const response = new Response(bodyText(faultBody), {
597
+ status: fault.status,
598
+ headers: bodyHeaders(fault.headers, faultBody)
599
+ })
600
+
601
+ consumeFault(state, entry)
602
+ entry.status = response.status
603
+
604
+ return response
605
+ }
606
+
607
+ /** Answer a scripted error turn; a matching chunk fault still shapes its body. */
608
+ const respondWithScriptedError = (
609
+ entry: Entry,
610
+ error: EmulatorScriptedError['error'],
611
+ fault: MutableFaultState<Fault> | undefined
612
+ ): Response =>
613
+ respondWithBody(
614
+ entry,
615
+ error.status,
616
+ bodyHeaders(error.headers, error.body),
617
+ [bodyText(error.body)],
618
+ fault
619
+ )
620
+
621
+ const reset = () => {
622
+ clearLedger()
623
+ faultStates = []
624
+ turns = []
625
+ }
626
+
627
+ const state = () => ({
628
+ ...config.stateFields(),
629
+ pendingTurns: turns.length,
630
+ faults: faultStates.map(snapshotFault),
631
+ ledgerEntries: entries.length
632
+ })
633
+
634
+ const controlPlane = async (request: Request, path: string): Promise<Response> => {
635
+ const method = request.method
636
+ const allow = (methods: string) => controlError(405, 'method not allowed', { allow: methods })
637
+
638
+ const jsonBody = async (): Promise<Schema.Json | undefined> => {
639
+ const text = await readText(request)
640
+
641
+ return text === undefined ? undefined : parseJson(text)
642
+ }
643
+
644
+ switch (path) {
645
+ case '/_emulate/ledger':
646
+ if (method === 'GET') {
647
+ return jsonResponse(200, { entries: entries.map(config.snapshotEntry) })
648
+ }
649
+
650
+ if (method === 'DELETE') {
651
+ const cleared = entries.length
652
+
653
+ clearLedger()
654
+
655
+ return jsonResponse(200, { cleared })
656
+ }
657
+
658
+ return allow('GET, DELETE')
659
+
660
+ case '/_emulate/faults': {
661
+ if (method === 'GET') {
662
+ return jsonResponse(200, { faults: faultStates.map(snapshotFault) })
663
+ }
664
+
665
+ if (method === 'DELETE') {
666
+ const cleared = faultStates.length
667
+
668
+ faultStates = []
669
+
670
+ return jsonResponse(200, { cleared })
671
+ }
672
+
673
+ if (method !== 'POST') {
674
+ return allow('GET, POST, DELETE')
675
+ }
676
+
677
+ const decoded = decodeFaultList(await jsonBody())
678
+
679
+ if (Result.isFailure(decoded)) {
680
+ return controlError(400, `invalid fault: ${issueMessage(decoded.failure.issue)}`)
681
+ }
682
+
683
+ const added = (
684
+ 'faults' in decoded.success ? decoded.success.faults : [decoded.success]
685
+ ).map(fault => addFault(fault))
686
+
687
+ return jsonResponse(201, { faults: added })
688
+ }
689
+
690
+ case '/_emulate/script': {
691
+ if (method !== 'POST') {
692
+ return allow('POST')
693
+ }
694
+
695
+ const decoded = decodeTurnList(await jsonBody())
696
+
697
+ if (Result.isFailure(decoded)) {
698
+ return controlError(400, `invalid turn: ${issueMessage(decoded.failure.issue)}`)
699
+ }
700
+
701
+ const queued =
702
+ Predicate.isObject(decoded.success) && 'turns' in decoded.success
703
+ ? decoded.success.turns
704
+ : [decoded.success]
705
+
706
+ for (const turn of queued) {
707
+ enqueueTurn(turn)
708
+ }
709
+
710
+ return jsonResponse(201, { pending: turns.length })
711
+ }
712
+
713
+ case '/_emulate/reset':
714
+ if (method !== 'POST') {
715
+ return allow('POST')
716
+ }
717
+
718
+ reset()
719
+
720
+ return jsonResponse(200, { reset: true })
721
+
722
+ case '/_emulate/state':
723
+ if (method !== 'GET') {
724
+ return allow('GET')
725
+ }
726
+
727
+ return jsonResponse(200, state())
728
+
729
+ case '/_emulate/coverage':
730
+ if (method !== 'GET') {
731
+ return allow('GET')
732
+ }
733
+
734
+ return jsonResponse(200, coverage())
735
+
736
+ default:
737
+ return controlError(404, 'unknown control-plane route')
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Bind one handler per manifest route (keyed by `emulatorRouteKey`) and
743
+ * return the emulator API. Throws `EmulatorRouteUnmapped` when a manifest
744
+ * route has no handler or a handler has no manifest route.
745
+ */
746
+ const serve = (
747
+ handlers: ReadonlyMap<string, EmulatorRouteHandler<Entry>>
748
+ ): EmulatorApi<Turn, Fault, Snapshot> => {
749
+ const routes = bindRouteHandlers(config.routes, handlers)
750
+
751
+ const emulatedApi = async (request: Request, path: string): Promise<Response> => {
752
+ const bound = routes.find(
753
+ candidate => candidate.route.method === request.method && candidate.route.path === path
754
+ )
755
+
756
+ const entry = config.newEntry({
757
+ seq: nextSeq++,
758
+ method: request.method,
759
+ path,
760
+ evidence: bound?.route.evidence ?? 'unknown-route',
761
+ status: 0,
762
+ bodyChunks: 0
763
+ })
764
+
765
+ entries.push(entry)
766
+
767
+ if (bound === undefined) {
768
+ const response = config.unknownRoute(entry)
769
+
770
+ entry.status = response.status
771
+
772
+ return response
773
+ }
774
+
775
+ // Error recovery still answers through the route: the fallback 500 is evidence-tagged and
776
+ // the ledger records the status actually sent.
777
+ const response = await bound.handler(request, entry, path).catch(() => {
778
+ entry.status = 500
779
+ entry.responseError = 'the emulator could not build the planned response'
780
+
781
+ return controlError(500, 'emulator could not build the response')
782
+ })
783
+
784
+ return withEvidence(response, bound.route.evidence)
785
+ }
786
+
787
+ const handle = (request: Request): Promise<Response> => {
788
+ const path = URL.canParse(request.url) ? new URL(request.url).pathname : '/'
789
+
790
+ return path === '/_emulate' || path.startsWith('/_emulate/')
791
+ ? controlPlane(request, path)
792
+ : emulatedApi(request, path)
793
+ }
794
+
795
+ return {
796
+ fetch: request =>
797
+ handle(request).catch(() => controlError(500, 'emulator failed to handle the request')),
798
+ ledger: {
799
+ entries: () => entries.map(config.snapshotEntry),
800
+ clear: clearLedger
801
+ },
802
+ reset,
803
+ faults: {
804
+ add: fault => {
805
+ const added = addFault(fault)
806
+
807
+ if (Predicate.isString(added)) {
808
+ throw config.inputInvalid('fault', added)
809
+ }
810
+
811
+ return added
812
+ },
813
+ list: () => faultStates.map(snapshotFault),
814
+ clear: () => {
815
+ faultStates = []
816
+ }
817
+ },
818
+ script: {
819
+ enqueue: turn => {
820
+ const problem = enqueueTurn(turn)
821
+
822
+ if (problem !== undefined) {
823
+ throw config.inputInvalid('turn', problem)
824
+ }
825
+ },
826
+ pending: () => turns.length,
827
+ clear: () => {
828
+ turns = []
829
+ }
830
+ },
831
+ coverage
832
+ }
833
+ }
834
+
835
+ return {
836
+ takeFault,
837
+ consumeFault,
838
+ nextTurn,
839
+ respondWithBody,
840
+ respondWithStatusFault,
841
+ respondWithScriptedError,
842
+ serve
843
+ }
844
+ }