@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
package/dist/email.mjs ADDED
@@ -0,0 +1,608 @@
1
+ import { bindRouteHandlers, emulatorRouteKey } from "./route-evidence.mjs";
2
+ import { emailEmulatorFixtures } from "./email-fixtures.mjs";
3
+ import { Data, Equal, Predicate, Result } from "effect";
4
+ import * as Schema from "effect/Schema";
5
+ //#region src/email.ts
6
+ /**
7
+ * Email emulator: a fixture-driven, in-memory fake backend for the generic `EmailClient` port.
8
+ *
9
+ * Yolk never speaks IMAP, POP3, or SMTP; hosts implement `EmailClient`. This emulator stands in for
10
+ * such a host at the port level: `call(method, request)` takes one port call as plain JSON (the
11
+ * credential-free request) and answers `{ response }`, `{ failure }`, or a fail-closed
12
+ * `{ notEmulated }`. There is no socket, TLS, MIME, or mail library here, no `node:` builtin, and
13
+ * no SDK import: it is a plain structural object, and
14
+ * `emailClientLayerFromBackend` in `@yolk-sdk/connectors/email/conformance` turns it into the
15
+ * `EmailClient` layer.
16
+ *
17
+ * Response behaviour comes ONLY from the email conformance fixtures (copied as data in
18
+ * `email-fixtures.ts`). The emulator keeps an in-memory mailbox (folders, messages, flags) seeded
19
+ * with the mailbox the fixtures describe; the state only selects which fixture answers (the first
20
+ * matching fixture consistent with the mailbox) and is updated with what that fixture says
21
+ * happened (flags set, a message moved to the destination id the fixture names, a draft appended,
22
+ * a Sent copy saved). It never invents an id, a flag, or a response.
23
+ *
24
+ * Request-shape latitude (the only one): credential fields are never compared or recorded, and
25
+ * `connection.host` is not compared (the practice host comes from seeds). Every other connection
26
+ * field (`protocol`, `port`, `security`) is compared. Everything else must equal a fixture request
27
+ * exactly. Anything that does not
28
+ * match (an unknown method, no matching fixture, or no fixture consistent with the mailbox state)
29
+ * fails closed with a ledgered `notEmulated` answer, the port analogue of HTTP 400.
30
+ *
31
+ * @experimental
32
+ */
33
+ /** The port every email fixture and manifest route names. */
34
+ const emailEmulatorPort = "EmailClient";
35
+ /** Manifest routes of port emulators use this pseudo method; `path` is `<Port>.<method>`. */
36
+ const emailEmulatorRouteMethod = "PORT";
37
+ const portRoute = (method, write, caseIds) => ({
38
+ method: emailEmulatorRouteMethod,
39
+ path: `${emailEmulatorPort}.${method}`,
40
+ kind: "connector",
41
+ write,
42
+ caseIds,
43
+ evidence: "unverified",
44
+ observedAt: void 0
45
+ });
46
+ const listAndGet = "email.imap.list-and-get-headers";
47
+ const filteredList = "email.imap.filtered-list-no-fallback";
48
+ const draftsDiscovery = "email.imap.draft-drafts-discovery";
49
+ const setReadAndFlag = "email.imap.set-read-and-flag";
50
+ const trashUntrash = "email.imap.trash-untrash-to-inbox";
51
+ const moveIds = "email.imap.move-destination-ids";
52
+ /**
53
+ * Route evidence manifest: every emulated `EmailClient` method (`PORT EmailClient.<method>`) and the
54
+ * email conformance cases whose (synthetic, unverified) fixtures it follows. Write methods are
55
+ * unverified connector writes: they need a pending entry in the repo's evidence check until an
56
+ * owner-approved live run against a practice mailbox verifies them.
57
+ */
58
+ const emailEmulatorRoutes = [
59
+ portRoute("listMessages", false, [listAndGet, "email.pop3.rejects-folders-drafts-mutations"]),
60
+ portRoute("listMessagesFiltered", false, [filteredList]),
61
+ portRoute("getMessage", false, [
62
+ listAndGet,
63
+ draftsDiscovery,
64
+ setReadAndFlag,
65
+ trashUntrash,
66
+ moveIds
67
+ ]),
68
+ portRoute("createDraft", true, [
69
+ draftsDiscovery,
70
+ trashUntrash,
71
+ moveIds
72
+ ]),
73
+ portRoute("sendMessage", true, [
74
+ "email.smtp.sent-copy-statuses",
75
+ "email.smtp.legacy-host-sent-copy",
76
+ "email.smtp.acceptance-not-delivery"
77
+ ]),
78
+ portRoute("setRead", true, [setReadAndFlag]),
79
+ portRoute("setFlag", true, [setReadAndFlag]),
80
+ portRoute("trash", true, [trashUntrash]),
81
+ portRoute("untrash", true, [trashUntrash]),
82
+ portRoute("move", true, [moveIds]),
83
+ portRoute("deletePermanently", true, [
84
+ draftsDiscovery,
85
+ trashUntrash,
86
+ moveIds
87
+ ])
88
+ ];
89
+ const NonEmpty = Schema.String.check(Schema.isNonEmpty());
90
+ /** SPECIAL-USE attributes (RFC 6154) a seeded folder may advertise. */
91
+ const EmailEmulatorSpecialUse = Schema.Literals([
92
+ "\\All",
93
+ "\\Archive",
94
+ "\\Drafts",
95
+ "\\Flagged",
96
+ "\\Junk",
97
+ "\\Sent",
98
+ "\\Trash"
99
+ ]);
100
+ /**
101
+ * One message in the emulated mailbox. `id` is the opaque IMAP id (absent for a Sent copy whose
102
+ * id no fixture named), `uidl` the POP3 id of an INBOX message; unknown flags stay absent until a
103
+ * fixture reports them.
104
+ */
105
+ const EmailEmulatorMessage = Schema.Struct({
106
+ id: Schema.optionalKey(NonEmpty),
107
+ uidl: Schema.optionalKey(NonEmpty),
108
+ subject: Schema.optionalKey(Schema.String),
109
+ isRead: Schema.optionalKey(Schema.Boolean),
110
+ isFlagged: Schema.optionalKey(Schema.Boolean)
111
+ });
112
+ const EmailEmulatorFolder = Schema.Struct({
113
+ name: NonEmpty,
114
+ specialUse: Schema.optionalKey(EmailEmulatorSpecialUse),
115
+ messages: Schema.Array(EmailEmulatorMessage)
116
+ });
117
+ /** The emulated mailbox: plain JSON, used both as the seed and as the `state()` snapshot. */
118
+ const EmailEmulatorSeed = Schema.Struct({ folders: Schema.Array(EmailEmulatorFolder) });
119
+ /** The synthetic practice mailbox the email fixtures describe (the default seed). */
120
+ const emailEmulatorDefaultSeed = { folders: [
121
+ {
122
+ name: "INBOX",
123
+ messages: [{
124
+ id: "1700000001:41",
125
+ uidl: "uidl-synthetic-0041",
126
+ subject: "Synthetic welcome",
127
+ isRead: false,
128
+ isFlagged: false
129
+ }, {
130
+ id: "1700000001:42",
131
+ uidl: "uidl-synthetic-0042",
132
+ subject: "Synthetic weekly summary",
133
+ isRead: true,
134
+ isFlagged: false
135
+ }]
136
+ },
137
+ {
138
+ name: "Saved Drafts",
139
+ specialUse: "\\Drafts",
140
+ messages: []
141
+ },
142
+ {
143
+ name: "Sent Items",
144
+ specialUse: "\\Sent",
145
+ messages: []
146
+ },
147
+ {
148
+ name: "Deleted Items",
149
+ specialUse: "\\Trash",
150
+ messages: []
151
+ },
152
+ {
153
+ name: "Archive",
154
+ messages: []
155
+ }
156
+ ] };
157
+ const Failure = Schema.Struct({
158
+ kind: Schema.Literals(["expected", "error"]),
159
+ code: NonEmpty,
160
+ message: Schema.String,
161
+ status: Schema.optionalKey(Schema.Int)
162
+ });
163
+ /**
164
+ * A fault: answer the next `count` calls (every one when omitted) of `method` whose credential-free
165
+ * request contains `match` (a deep subset: objects by key, arrays element by element, other values
166
+ * exactly) with `failure` instead of a fixture. Faults change no mailbox state.
167
+ */
168
+ const EmailEmulatorFault = Schema.Struct({
169
+ kind: Schema.Literal("failure"),
170
+ method: NonEmpty,
171
+ match: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)),
172
+ count: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
173
+ failure: Failure
174
+ });
175
+ /** Thrown by `makeEmailEmulator` (invalid seed) and `faults.add` (invalid fault). */
176
+ var EmailEmulatorInputInvalid = class extends Data.TaggedError("EmailEmulatorInputInvalid") {
177
+ get message() {
178
+ return `Invalid email emulator ${this.input}: ${this.reason}`;
179
+ }
180
+ };
181
+ const notEmulatedText = {
182
+ "unknown-method": "unknown-method: the method has no emulated route",
183
+ "invalid-request": "invalid-request: the request is not a JSON object",
184
+ "no-matching-fixture": "no-matching-fixture: no fixture matches this request",
185
+ "state-conflict": "state-conflict: no matching fixture is consistent with the emulated mailbox"
186
+ };
187
+ const isJsonObject = (value) => value !== void 0 && value !== null && Predicate.isObject(value) && !Array.isArray(value);
188
+ const objectField = (value, key) => {
189
+ const field = isJsonObject(value) ? value[key] : void 0;
190
+ return isJsonObject(field) ? field : void 0;
191
+ };
192
+ const stringField = (value, key) => {
193
+ const field = isJsonObject(value) ? value[key] : void 0;
194
+ return Predicate.isString(field) ? field : void 0;
195
+ };
196
+ const booleanField = (value, key) => {
197
+ const field = isJsonObject(value) ? value[key] : void 0;
198
+ return Predicate.isBoolean(field) ? field : void 0;
199
+ };
200
+ const arrayField = (value, key) => {
201
+ const field = isJsonObject(value) ? value[key] : void 0;
202
+ return Array.isArray(field) ? field : void 0;
203
+ };
204
+ const credentialKeyPattern = /^(?:credentials?|(?:access|refresh|id|auth|api|session|private|bearer|oauth)[_-]?token|token|client[_-]?secret|secret(?:[_-]?key)?|private[_-]?key|password|passwd|api[_-]?key|authorization)$/i;
205
+ /** A copy without credential fields; `ignoreConnectionHost` applies the request-shape latitude. */
206
+ const scrub = (value, ignoreConnectionHost) => {
207
+ if (Array.isArray(value)) return value.map((item) => scrub(item, ignoreConnectionHost));
208
+ if (!isJsonObject(value)) return value;
209
+ const copy = {};
210
+ for (const [key, item] of Object.entries(value)) {
211
+ if (credentialKeyPattern.test(key)) continue;
212
+ if (ignoreConnectionHost && key === "connection" && isJsonObject(item)) {
213
+ const { host: _host, ...connection } = item;
214
+ copy[key] = scrub(connection, ignoreConnectionHost);
215
+ continue;
216
+ }
217
+ copy[key] = scrub(item, ignoreConnectionHost);
218
+ }
219
+ return copy;
220
+ };
221
+ /** Deep subset: objects by key, arrays element by element (same length), other values exactly. */
222
+ const containsSubset = (value, pattern) => {
223
+ if (value === void 0) return false;
224
+ if (Array.isArray(pattern)) return Array.isArray(value) && value.length === pattern.length && pattern.every((item, index) => containsSubset(value[index], item));
225
+ if (isJsonObject(pattern)) return isJsonObject(value) && Object.entries(pattern).every(([key, item]) => containsSubset(value[key], item));
226
+ return Equal.equals(value, pattern);
227
+ };
228
+ const copyMessage = (message) => {
229
+ const copy = {};
230
+ if (message.id !== void 0) copy.id = message.id;
231
+ if (message.uidl !== void 0) copy.uidl = message.uidl;
232
+ if (message.subject !== void 0) copy.subject = message.subject;
233
+ if (message.isRead !== void 0) copy.isRead = message.isRead;
234
+ if (message.isFlagged !== void 0) copy.isFlagged = message.isFlagged;
235
+ return copy;
236
+ };
237
+ const mailboxFrom = (seed) => ({ folders: seed.folders.map((folder) => folder.specialUse === void 0 ? {
238
+ name: folder.name,
239
+ messages: folder.messages.map(copyMessage)
240
+ } : {
241
+ name: folder.name,
242
+ specialUse: folder.specialUse,
243
+ messages: folder.messages.map(copyMessage)
244
+ }) });
245
+ const snapshotOf = (state) => ({ folders: state.folders.map((folder) => folder.specialUse === void 0 ? {
246
+ name: folder.name,
247
+ messages: folder.messages.map(copyMessage)
248
+ } : {
249
+ name: folder.name,
250
+ specialUse: folder.specialUse,
251
+ messages: folder.messages.map(copyMessage)
252
+ }) });
253
+ const folderNamed = (state, name) => name === void 0 ? void 0 : state.folders.find((folder) => folder.name === name);
254
+ const specialUseFolder = (state, use) => state.folders.find((folder) => folder.specialUse === use);
255
+ const isPop3 = (request) => stringField(objectField(request, "connection"), "protocol") === "pop3";
256
+ const messageIn = (folder, id, pop3 = false) => folder === void 0 || id === void 0 ? void 0 : folder.messages.find((message) => (pop3 ? message.uidl : message.id) === id);
257
+ const flagKeys = ["isRead", "isFlagged"];
258
+ /** Reported flags and subject agree with what the mailbox knows (unknown state agrees). */
259
+ const agrees = (message, reported) => {
260
+ const subject = stringField(reported, "subject");
261
+ return flagKeys.every((key) => {
262
+ const value = booleanField(reported, key);
263
+ return value === void 0 || message[key] === void 0 || message[key] === value;
264
+ }) && (subject === void 0 || message.subject === void 0 || message.subject === subject);
265
+ };
266
+ /** Remember flags and subject the mailbox did not know yet. */
267
+ const learn = (message, reported) => {
268
+ for (const key of flagKeys) {
269
+ const value = booleanField(reported, key);
270
+ if (message[key] === void 0 && value !== void 0) message[key] = value;
271
+ }
272
+ const subject = stringField(reported, "subject");
273
+ if (message.subject === void 0 && subject !== void 0) message.subject = subject;
274
+ };
275
+ const requestFolder = (state, request) => folderNamed(state, stringField(request, "folder") ?? "INBOX");
276
+ /** Every known message that certainly passes the request's read/flagged filters. */
277
+ const certainlyListed = (message, request) => flagKeys.every((key) => {
278
+ const filter = booleanField(request, key);
279
+ return filter === void 0 || message[key] === filter;
280
+ });
281
+ const listModel = {
282
+ consistent: (state, request, fixture) => {
283
+ if (fixture.failure !== void 0) return true;
284
+ const folder = requestFolder(state, request);
285
+ const listed = arrayField(fixture.response, "messages");
286
+ const pop3 = isPop3(request);
287
+ if (folder === void 0 || listed === void 0) return false;
288
+ if (!listed.every((summary) => {
289
+ const message = messageIn(folder, stringField(summary, "id"), pop3);
290
+ return message !== void 0 && agrees(message, summary);
291
+ })) return false;
292
+ if (stringField(fixture.response, "nextCursor") !== void 0) return true;
293
+ const listedIds = new Set(listed.map((summary) => stringField(summary, "id")));
294
+ return folder.messages.every((message) => {
295
+ const id = pop3 ? message.uidl : message.id;
296
+ return id === void 0 || !certainlyListed(message, request) || listedIds.has(id);
297
+ });
298
+ },
299
+ apply: (state, request, fixture) => {
300
+ const folder = requestFolder(state, request);
301
+ const pop3 = isPop3(request);
302
+ for (const summary of arrayField(fixture.response, "messages") ?? []) {
303
+ const message = messageIn(folder, stringField(summary, "id"), pop3);
304
+ if (message !== void 0) learn(message, summary);
305
+ }
306
+ }
307
+ };
308
+ const getModel = {
309
+ consistent: (state, request, fixture) => {
310
+ const id = stringField(request, "messageId");
311
+ const message = messageIn(requestFolder(state, request), id, isPop3(request));
312
+ if (fixture.failure !== void 0) return message === void 0;
313
+ const reported = objectField(fixture.response, "message");
314
+ return message !== void 0 && stringField(reported, "id") === id && agrees(message, reported);
315
+ },
316
+ apply: (state, request, fixture) => {
317
+ const message = messageIn(requestFolder(state, request), stringField(request, "messageId"), isPop3(request));
318
+ if (message !== void 0 && fixture.failure === void 0) learn(message, objectField(fixture.response, "message"));
319
+ }
320
+ };
321
+ const flagModel = (key) => ({
322
+ consistent: (state, request, fixture) => {
323
+ if (fixture.failure !== void 0) return true;
324
+ const id = stringField(request, "messageId");
325
+ return messageIn(requestFolder(state, request), id) !== void 0 && stringField(fixture.response, "messageId") === id && booleanField(fixture.response, key) === booleanField(request, key);
326
+ },
327
+ apply: (state, request, fixture) => {
328
+ const message = messageIn(requestFolder(state, request), stringField(request, "messageId"));
329
+ const value = booleanField(request, key);
330
+ if (fixture.failure === void 0 && message !== void 0 && value !== void 0) message[key] = value;
331
+ }
332
+ });
333
+ /** A move-shaped method: where the message comes from and which destination is acceptable. */
334
+ const moveModel = (source, destinationAllowed) => {
335
+ const plan = (state, request, fixture) => {
336
+ const from = source(state, request);
337
+ return {
338
+ from,
339
+ message: messageIn(from, stringField(request, "messageId")),
340
+ destination: folderNamed(state, stringField(fixture.response, "folder")),
341
+ movedId: stringField(fixture.response, "messageId")
342
+ };
343
+ };
344
+ return {
345
+ consistent: (state, request, fixture) => {
346
+ if (fixture.failure !== void 0) return true;
347
+ const { from, message, destination, movedId } = plan(state, request, fixture);
348
+ return booleanField(fixture.response, "moved") === true && from !== void 0 && message !== void 0 && destination !== void 0 && destination !== from && destinationAllowed(state, request, destination) && messageIn(destination, movedId) === void 0;
349
+ },
350
+ apply: (state, request, fixture) => {
351
+ if (fixture.failure !== void 0) return;
352
+ const { from, message, destination, movedId } = plan(state, request, fixture);
353
+ if (from === void 0 || message === void 0 || destination === void 0) return;
354
+ from.messages = from.messages.filter((candidate) => candidate !== message);
355
+ const moved = { ...message };
356
+ delete moved.uidl;
357
+ if (movedId === void 0) delete moved.id;
358
+ else moved.id = movedId;
359
+ destination.messages.push(moved);
360
+ }
361
+ };
362
+ };
363
+ const createDraftModel = {
364
+ consistent: (state, request, fixture) => {
365
+ if (fixture.failure !== void 0) return true;
366
+ const destination = folderNamed(state, stringField(fixture.response, "folder"));
367
+ const requested = stringField(request, "folder");
368
+ return booleanField(fixture.response, "saved") === true && destination !== void 0 && (requested === void 0 ? destination.specialUse === "\\Drafts" : destination.name === requested) && messageIn(destination, stringField(fixture.response, "draftId")) === void 0;
369
+ },
370
+ apply: (state, request, fixture) => {
371
+ const destination = folderNamed(state, stringField(fixture.response, "folder"));
372
+ if (fixture.failure !== void 0 || destination === void 0) return;
373
+ const draft = {};
374
+ const draftId = stringField(fixture.response, "draftId");
375
+ const subject = stringField(objectField(request, "message"), "subject");
376
+ if (draftId !== void 0) draft.id = draftId;
377
+ if (subject !== void 0) draft.subject = subject;
378
+ destination.messages.push(draft);
379
+ }
380
+ };
381
+ const deletePermanentlyModel = {
382
+ consistent: (state, request, fixture) => {
383
+ if (fixture.failure !== void 0) return true;
384
+ const folder = requestFolder(state, request);
385
+ const results = arrayField(fixture.response, "results");
386
+ return folder !== void 0 && results !== void 0 && results.every((result) => {
387
+ const present = messageIn(folder, stringField(result, "messageId")) !== void 0;
388
+ const status = stringField(result, "status");
389
+ return status === "succeeded" ? present : status !== "failed" || stringField(result, "code") !== "not_found" || !present;
390
+ });
391
+ },
392
+ apply: (state, request, fixture) => {
393
+ const folder = requestFolder(state, request);
394
+ if (fixture.failure !== void 0 || folder === void 0) return;
395
+ const deleted = new Set((arrayField(fixture.response, "results") ?? []).flatMap((result) => stringField(result, "status") === "succeeded" ? [stringField(result, "messageId")] : []));
396
+ folder.messages = folder.messages.filter((message) => message.id === void 0 || !deleted.has(message.id));
397
+ }
398
+ };
399
+ /** The mailbox a saved Sent copy lands in, or `undefined` when none is consistent. */
400
+ const savedCopyFolder = (state, request, fixture) => {
401
+ const requestedCopy = objectField(request, "sentCopy");
402
+ if (requestedCopy === void 0) return;
403
+ const requestedFolder = stringField(requestedCopy, "folder");
404
+ const reportedFolder = stringField(objectField(fixture.response, "sentCopy"), "folder");
405
+ const folder = requestedFolder === void 0 ? reportedFolder === void 0 ? specialUseFolder(state, "\\Sent") : folderNamed(state, reportedFolder) : folderNamed(state, requestedFolder);
406
+ if (folder === void 0 || reportedFolder !== void 0 && reportedFolder !== folder.name) return;
407
+ return requestedFolder !== void 0 || folder.specialUse === "\\Sent" ? folder : void 0;
408
+ };
409
+ const sentCopyStatus = (fixture) => stringField(objectField(fixture.response, "sentCopy"), "status");
410
+ const sendMessageModel = {
411
+ consistent: (state, request, fixture) => {
412
+ if (fixture.failure !== void 0) return true;
413
+ const status = sentCopyStatus(fixture);
414
+ const requestedCopy = objectField(request, "sentCopy");
415
+ const requestedFolder = stringField(requestedCopy, "folder");
416
+ const failedConsistent = requestedCopy !== void 0 && (requestedFolder === void 0 || folderNamed(state, requestedFolder) === void 0);
417
+ return booleanField(fixture.response, "accepted") === true && (status === "saved" ? savedCopyFolder(state, request, fixture) !== void 0 : status !== "failed" || failedConsistent);
418
+ },
419
+ apply: (state, request, fixture) => {
420
+ if (fixture.failure !== void 0 || sentCopyStatus(fixture) !== "saved") return;
421
+ const folder = savedCopyFolder(state, request, fixture);
422
+ const subject = stringField(objectField(request, "message"), "subject");
423
+ folder?.messages.push(subject === void 0 ? {} : { subject });
424
+ }
425
+ };
426
+ const routeKey = (method) => emulatorRouteKey(emailEmulatorRouteMethod, `${emailEmulatorPort}.${method}`);
427
+ const methodModels = new Map([
428
+ [routeKey("listMessages"), listModel],
429
+ [routeKey("listMessagesFiltered"), listModel],
430
+ [routeKey("getMessage"), getModel],
431
+ [routeKey("createDraft"), createDraftModel],
432
+ [routeKey("sendMessage"), sendMessageModel],
433
+ [routeKey("setRead"), flagModel("isRead")],
434
+ [routeKey("setFlag"), flagModel("isFlagged")],
435
+ [routeKey("trash"), moveModel((state, request) => requestFolder(state, request), (_state, request, destination) => {
436
+ const trashFolder = stringField(request, "trashFolder");
437
+ return trashFolder === void 0 ? destination.specialUse === "\\Trash" : destination.name === trashFolder;
438
+ })],
439
+ [routeKey("untrash"), moveModel((state, request) => {
440
+ const folder = stringField(request, "folder");
441
+ return folder === void 0 ? specialUseFolder(state, "\\Trash") : folderNamed(state, folder);
442
+ }, (_state, request, destination) => destination.name === stringField(request, "destinationFolder"))],
443
+ [routeKey("move"), moveModel((state, request) => requestFolder(state, request), (_state, request, destination) => destination.name === stringField(request, "destinationFolder"))],
444
+ [routeKey("deletePermanently"), deletePermanentlyModel]
445
+ ]);
446
+ const strict = { onExcessProperty: "error" };
447
+ const decodeSeed = Schema.decodeUnknownResult(EmailEmulatorSeed, strict);
448
+ const decodeFault = Schema.decodeUnknownResult(EmailEmulatorFault, strict);
449
+ const issueText = (error) => error.message;
450
+ /** Folder names and message ids (and POP3 ids) must be unique so fixtures address one message. */
451
+ const seedProblem = (seed) => {
452
+ const names = seed.folders.map((folder) => folder.name);
453
+ if (new Set(names).size !== names.length) return "folder names must be unique";
454
+ for (const folder of seed.folders) {
455
+ const ids = folder.messages.flatMap((message) => message.id === void 0 ? [] : [message.id]);
456
+ const uidls = folder.messages.flatMap((message) => message.uidl === void 0 ? [] : [message.uidl]);
457
+ if (new Set(ids).size !== ids.length || new Set(uidls).size !== uidls.length) return `message ids in ${folder.name} must be unique`;
458
+ }
459
+ };
460
+ /**
461
+ * Create an email emulator. Each call has its own mailbox, ledger, faults, and fixture use.
462
+ *
463
+ * `call(method, request)` answers in this order: an unknown method or a non-object request fails
464
+ * closed; the first active matching fault answers its failure; otherwise the fixtures whose method
465
+ * and request match (after the request-shape latitude) are checked against the mailbox, and the
466
+ * first consistent one (preferring one not used since the last reset) answers and updates the
467
+ * mailbox; no match (`no-matching-fixture`) or no consistent match (`state-conflict`) fails closed.
468
+ * Every call is written to the ledger without credential fields.
469
+ *
470
+ * Throws `EmailEmulatorInputInvalid` for an invalid seed.
471
+ */
472
+ const makeEmailEmulator = (options = {}) => {
473
+ const seedResult = decodeSeed(options.seed ?? emailEmulatorDefaultSeed);
474
+ if (Result.isFailure(seedResult)) throw new EmailEmulatorInputInvalid({
475
+ input: "seed",
476
+ reason: issueText(seedResult.failure)
477
+ });
478
+ const seed = seedResult.success;
479
+ const problem = seedProblem(seed);
480
+ if (problem !== void 0) throw new EmailEmulatorInputInvalid({
481
+ input: "seed",
482
+ reason: problem
483
+ });
484
+ const bound = new Map(bindRouteHandlers(emailEmulatorRoutes, methodModels).map(({ route, handler }) => [route.path.slice(12), {
485
+ route,
486
+ model: handler
487
+ }]));
488
+ let state = mailboxFrom(seed);
489
+ let entries = [];
490
+ let faults = [];
491
+ let nextFaultId = 1;
492
+ let nextSeq = 1;
493
+ const used = /* @__PURE__ */ new Set();
494
+ const record = (entry) => {
495
+ entries.push({
496
+ seq: nextSeq,
497
+ ...entry
498
+ });
499
+ nextSeq += 1;
500
+ };
501
+ const refuse = (method, request, reason, evidence) => {
502
+ record({
503
+ method,
504
+ request,
505
+ outcome: "not-emulated",
506
+ evidence,
507
+ reason
508
+ });
509
+ return { notEmulated: { reason: notEmulatedText[reason] } };
510
+ };
511
+ const call = (method, rawRequest) => {
512
+ const request = scrub(rawRequest, false);
513
+ const target = bound.get(method);
514
+ if (target === void 0) return refuse(method, request, "unknown-method", "unknown-method");
515
+ const evidence = target.route.evidence;
516
+ if (!isJsonObject(request)) return refuse(method, request, "invalid-request", evidence);
517
+ const comparable = scrub(request, true);
518
+ const matching = emailEmulatorFixtures.filter((fixture) => fixture.port === "EmailClient" && fixture.method === method && Equal.equals(scrub(fixture.request, true), comparable));
519
+ if (matching.length === 0) return refuse(method, request, "no-matching-fixture", evidence);
520
+ const consistent = matching.filter((fixture) => target.model.consistent(state, request, fixture));
521
+ const chosen = consistent.find((fixture) => !used.has(fixture.id)) ?? consistent[0];
522
+ if (chosen === void 0) return refuse(method, request, "state-conflict", evidence);
523
+ const fault = faults.find((candidate) => candidate.fault.method === method && (candidate.remaining === void 0 || candidate.remaining > 0) && (candidate.fault.match === void 0 || containsSubset(request, candidate.fault.match)));
524
+ if (fault !== void 0) {
525
+ fault.applied += 1;
526
+ if (fault.remaining !== void 0) fault.remaining -= 1;
527
+ record({
528
+ method,
529
+ request,
530
+ outcome: "fault",
531
+ evidence,
532
+ faultId: fault.id
533
+ });
534
+ return { failure: fault.fault.failure };
535
+ }
536
+ target.model.apply(state, request, chosen);
537
+ used.add(chosen.id);
538
+ record({
539
+ method,
540
+ request,
541
+ outcome: "answered",
542
+ evidence,
543
+ fixtureId: chosen.id
544
+ });
545
+ return chosen.failure === void 0 ? { response: chosen.response } : { failure: chosen.failure };
546
+ };
547
+ const faultState = (fault) => ({
548
+ id: fault.id,
549
+ fault: fault.fault,
550
+ remaining: fault.remaining,
551
+ applied: fault.applied
552
+ });
553
+ return {
554
+ call,
555
+ state: () => snapshotOf(state),
556
+ seed: () => snapshotOf(mailboxFrom(seed)),
557
+ ledger: {
558
+ entries: () => entries.map((entry) => ({ ...entry })),
559
+ clear: () => {
560
+ entries = [];
561
+ }
562
+ },
563
+ reset: () => {
564
+ state = mailboxFrom(seed);
565
+ entries = [];
566
+ faults = [];
567
+ used.clear();
568
+ },
569
+ faults: {
570
+ add: (input) => {
571
+ const decoded = decodeFault(input);
572
+ if (Result.isFailure(decoded)) throw new EmailEmulatorInputInvalid({
573
+ input: "fault",
574
+ reason: issueText(decoded.failure)
575
+ });
576
+ if (!bound.has(decoded.success.method)) throw new EmailEmulatorInputInvalid({
577
+ input: "fault",
578
+ reason: `method ${decoded.success.method} has no emulated route`
579
+ });
580
+ const added = {
581
+ id: nextFaultId,
582
+ fault: decoded.success,
583
+ remaining: decoded.success.count,
584
+ applied: 0
585
+ };
586
+ nextFaultId += 1;
587
+ faults.push(added);
588
+ return faultState(added);
589
+ },
590
+ list: () => faults.map(faultState),
591
+ clear: () => {
592
+ faults = [];
593
+ }
594
+ },
595
+ coverage: () => ({
596
+ routes: emailEmulatorRoutes.map((route) => ({
597
+ ...route,
598
+ calls: entries.filter((entry) => `${emailEmulatorPort}.${entry.method}` === route.path).length
599
+ })),
600
+ notEmulatedCalls: entries.filter((entry) => entry.outcome === "not-emulated").length,
601
+ unusedFixtureIds: emailEmulatorFixtures.flatMap((fixture) => used.has(fixture.id) ? [] : [fixture.id])
602
+ })
603
+ };
604
+ };
605
+ //#endregion
606
+ export { EmailEmulatorFault, EmailEmulatorFolder, EmailEmulatorInputInvalid, EmailEmulatorMessage, EmailEmulatorSeed, EmailEmulatorSpecialUse, emailEmulatorDefaultSeed, emailEmulatorFixtures, emailEmulatorPort, emailEmulatorRouteMethod, emailEmulatorRoutes, makeEmailEmulator };
607
+
608
+ //# sourceMappingURL=email.mjs.map