@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":"email.mjs","names":[],"sources":["../src/email.ts"],"sourcesContent":["/**\n * Email emulator: a fixture-driven, in-memory fake backend for the generic `EmailClient` port.\n *\n * Yolk never speaks IMAP, POP3, or SMTP; hosts implement `EmailClient`. This emulator stands in for\n * such a host at the port level: `call(method, request)` takes one port call as plain JSON (the\n * credential-free request) and answers `{ response }`, `{ failure }`, or a fail-closed\n * `{ notEmulated }`. There is no socket, TLS, MIME, or mail library here, no `node:` builtin, and\n * no SDK import: it is a plain structural object, and\n * `emailClientLayerFromBackend` in `@yolk-sdk/connectors/email/conformance` turns it into the\n * `EmailClient` layer.\n *\n * Response behaviour comes ONLY from the email conformance fixtures (copied as data in\n * `email-fixtures.ts`). The emulator keeps an in-memory mailbox (folders, messages, flags) seeded\n * with the mailbox the fixtures describe; the state only selects which fixture answers (the first\n * matching fixture consistent with the mailbox) and is updated with what that fixture says\n * happened (flags set, a message moved to the destination id the fixture names, a draft appended,\n * a Sent copy saved). It never invents an id, a flag, or a response.\n *\n * Request-shape latitude (the only one): credential fields are never compared or recorded, and\n * `connection.host` is not compared (the practice host comes from seeds). Every other connection\n * field (`protocol`, `port`, `security`) is compared. Everything else must equal a fixture request\n * exactly. Anything that does not\n * match (an unknown method, no matching fixture, or no fixture consistent with the mailbox state)\n * fails closed with a ledgered `notEmulated` answer, the port analogue of HTTP 400.\n *\n * @experimental\n */\nimport { Data, Equal, Predicate, Result } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport {\n emailEmulatorFixtures,\n type EmailEmulatorFailure,\n type EmailEmulatorFixture\n} from './email-fixtures.ts'\nimport {\n bindRouteHandlers,\n emulatorRouteKey,\n type EmulatorEvidence,\n type EmulatorRouteEvidence\n} from './route-evidence.ts'\n\nexport type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'\n\nexport { emailEmulatorFixtures, type EmailEmulatorFailure, type EmailEmulatorFixture }\n\n/** The port every email fixture and manifest route names. */\nexport const emailEmulatorPort = 'EmailClient'\n\n/** Manifest routes of port emulators use this pseudo method; `path` is `<Port>.<method>`. */\nexport const emailEmulatorRouteMethod = 'PORT'\n\nconst portRoute = (\n method: string,\n write: boolean,\n caseIds: ReadonlyArray<string>\n): EmulatorRouteEvidence => ({\n method: emailEmulatorRouteMethod,\n path: `${emailEmulatorPort}.${method}`,\n kind: 'connector',\n write,\n caseIds,\n evidence: 'unverified',\n observedAt: undefined\n})\n\nconst listAndGet = 'email.imap.list-and-get-headers'\n\nconst filteredList = 'email.imap.filtered-list-no-fallback'\n\nconst draftsDiscovery = 'email.imap.draft-drafts-discovery'\n\nconst setReadAndFlag = 'email.imap.set-read-and-flag'\n\nconst trashUntrash = 'email.imap.trash-untrash-to-inbox'\n\nconst moveIds = 'email.imap.move-destination-ids'\n\nconst pop3Rejections = 'email.pop3.rejects-folders-drafts-mutations'\n\nconst sentCopyStatuses = 'email.smtp.sent-copy-statuses'\n\nconst legacySentCopy = 'email.smtp.legacy-host-sent-copy'\n\nconst acceptanceNotDelivery = 'email.smtp.acceptance-not-delivery'\n\n/**\n * Route evidence manifest: every emulated `EmailClient` method (`PORT EmailClient.<method>`) and the\n * email conformance cases whose (synthetic, unverified) fixtures it follows. Write methods are\n * unverified connector writes: they need a pending entry in the repo's evidence check until an\n * owner-approved live run against a practice mailbox verifies them.\n */\nexport const emailEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> = [\n portRoute('listMessages', false, [listAndGet, pop3Rejections]),\n portRoute('listMessagesFiltered', false, [filteredList]),\n portRoute('getMessage', false, [\n listAndGet,\n draftsDiscovery,\n setReadAndFlag,\n trashUntrash,\n moveIds\n ]),\n portRoute('createDraft', true, [draftsDiscovery, trashUntrash, moveIds]),\n portRoute('sendMessage', true, [sentCopyStatuses, legacySentCopy, acceptanceNotDelivery]),\n portRoute('setRead', true, [setReadAndFlag]),\n portRoute('setFlag', true, [setReadAndFlag]),\n portRoute('trash', true, [trashUntrash]),\n portRoute('untrash', true, [trashUntrash]),\n portRoute('move', true, [moveIds]),\n portRoute('deletePermanently', true, [draftsDiscovery, trashUntrash, moveIds])\n]\n\nconst NonEmpty = Schema.String.check(Schema.isNonEmpty())\n\n/** SPECIAL-USE attributes (RFC 6154) a seeded folder may advertise. */\nexport const EmailEmulatorSpecialUse = Schema.Literals([\n '\\\\All',\n '\\\\Archive',\n '\\\\Drafts',\n '\\\\Flagged',\n '\\\\Junk',\n '\\\\Sent',\n '\\\\Trash'\n])\n\nexport type EmailEmulatorSpecialUse = typeof EmailEmulatorSpecialUse.Type\n\n/**\n * One message in the emulated mailbox. `id` is the opaque IMAP id (absent for a Sent copy whose\n * id no fixture named), `uidl` the POP3 id of an INBOX message; unknown flags stay absent until a\n * fixture reports them.\n */\nexport const EmailEmulatorMessage = Schema.Struct({\n id: Schema.optionalKey(NonEmpty),\n uidl: Schema.optionalKey(NonEmpty),\n subject: Schema.optionalKey(Schema.String),\n isRead: Schema.optionalKey(Schema.Boolean),\n isFlagged: Schema.optionalKey(Schema.Boolean)\n})\n\nexport type EmailEmulatorMessage = typeof EmailEmulatorMessage.Type\n\nexport const EmailEmulatorFolder = Schema.Struct({\n name: NonEmpty,\n specialUse: Schema.optionalKey(EmailEmulatorSpecialUse),\n messages: Schema.Array(EmailEmulatorMessage)\n})\n\nexport type EmailEmulatorFolder = typeof EmailEmulatorFolder.Type\n\n/** The emulated mailbox: plain JSON, used both as the seed and as the `state()` snapshot. */\nexport const EmailEmulatorSeed = Schema.Struct({\n folders: Schema.Array(EmailEmulatorFolder)\n})\n\nexport type EmailEmulatorSeed = typeof EmailEmulatorSeed.Type\n\n/** The synthetic practice mailbox the email fixtures describe (the default seed). */\nexport const emailEmulatorDefaultSeed: EmailEmulatorSeed = {\n folders: [\n {\n name: 'INBOX',\n messages: [\n {\n id: '1700000001:41',\n uidl: 'uidl-synthetic-0041',\n subject: 'Synthetic welcome',\n isRead: false,\n isFlagged: false\n },\n {\n id: '1700000001:42',\n uidl: 'uidl-synthetic-0042',\n subject: 'Synthetic weekly summary',\n isRead: true,\n isFlagged: false\n }\n ]\n },\n { name: 'Saved Drafts', specialUse: '\\\\Drafts', messages: [] },\n { name: 'Sent Items', specialUse: '\\\\Sent', messages: [] },\n { name: 'Deleted Items', specialUse: '\\\\Trash', messages: [] },\n { name: 'Archive', messages: [] }\n ]\n}\n\nconst Failure = Schema.Struct({\n kind: Schema.Literals(['expected', 'error']),\n code: NonEmpty,\n message: Schema.String,\n status: Schema.optionalKey(Schema.Int)\n})\n\n/**\n * A fault: answer the next `count` calls (every one when omitted) of `method` whose credential-free\n * request contains `match` (a deep subset: objects by key, arrays element by element, other values\n * exactly) with `failure` instead of a fixture. Faults change no mailbox state.\n */\nexport const EmailEmulatorFault = Schema.Struct({\n kind: Schema.Literal('failure'),\n method: NonEmpty,\n match: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)),\n count: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),\n failure: Failure\n})\n\nexport type EmailEmulatorFault = typeof EmailEmulatorFault.Type\n\nexport type EmailEmulatorFaultState = {\n readonly id: number\n readonly fault: EmailEmulatorFault\n /** Remaining matching calls; `undefined` for an unlimited fault. */\n readonly remaining: number | undefined\n readonly applied: number\n}\n\n/** Thrown by `makeEmailEmulator` (invalid seed) and `faults.add` (invalid fault). */\nexport class EmailEmulatorInputInvalid extends Data.TaggedError('EmailEmulatorInputInvalid')<{\n readonly input: 'seed' | 'fault'\n readonly reason: string\n}> {\n override get message(): string {\n return `Invalid email emulator ${this.input}: ${this.reason}`\n }\n}\n\n/** Why a call failed closed. */\nexport type EmailEmulatorNotEmulatedReason =\n | 'unknown-method'\n | 'invalid-request'\n | 'no-matching-fixture'\n | 'state-conflict'\n\nconst notEmulatedText: Record<EmailEmulatorNotEmulatedReason, string> = {\n 'unknown-method': 'unknown-method: the method has no emulated route',\n 'invalid-request': 'invalid-request: the request is not a JSON object',\n 'no-matching-fixture': 'no-matching-fixture: no fixture matches this request',\n 'state-conflict': 'state-conflict: no matching fixture is consistent with the emulated mailbox'\n}\n\n/** One emulator answer; structurally the reply of a plain-JSON `EmailBackend`. */\nexport type EmailEmulatorReply =\n | { readonly response: Schema.Json }\n | { readonly failure: EmailEmulatorFailure }\n | { readonly notEmulated: { readonly reason: string } }\n\nexport type EmailEmulatorLedgerEntry = {\n readonly seq: number\n readonly method: string\n /** The request as received, without credential fields. */\n readonly request: Schema.Json\n readonly outcome: 'answered' | 'fault' | 'not-emulated'\n /** `unknown-method` for a method outside the manifest. */\n readonly evidence: EmulatorEvidence | 'unknown-method'\n readonly fixtureId?: string\n readonly faultId?: number\n readonly reason?: EmailEmulatorNotEmulatedReason\n}\n\nexport type EmailEmulatorRouteCoverage = EmulatorRouteEvidence & {\n /** Ledger calls to this method since the last ledger clear or reset. */\n readonly calls: number\n}\n\nexport type EmailEmulatorCoverage = {\n readonly routes: ReadonlyArray<EmailEmulatorRouteCoverage>\n /** Ledger calls that failed closed. */\n readonly notEmulatedCalls: number\n /** Fixtures that have not answered a call since the emulator was made or last reset. */\n readonly unusedFixtureIds: ReadonlyArray<string>\n}\n\nexport type EmailEmulatorOptions = {\n /** The initial mailbox. Defaults to `emailEmulatorDefaultSeed`. */\n readonly seed?: EmailEmulatorSeed\n}\n\nexport type EmailEmulator = {\n /** Answer one `EmailClient` call; never throws. This makes the emulator an `EmailBackend`. */\n readonly call: (method: string, request: Schema.Json) => EmailEmulatorReply\n /** The current mailbox (a copy). */\n readonly state: () => EmailEmulatorSeed\n /** The seed the emulator started from (a copy). */\n readonly seed: () => EmailEmulatorSeed\n readonly ledger: {\n readonly entries: () => ReadonlyArray<EmailEmulatorLedgerEntry>\n readonly clear: () => void\n }\n /** Clear the ledger and faults, forget fixture use, and restore the seeded mailbox. */\n readonly reset: () => void\n readonly faults: {\n readonly add: (fault: EmailEmulatorFault) => EmailEmulatorFaultState\n readonly list: () => ReadonlyArray<EmailEmulatorFaultState>\n readonly clear: () => void\n }\n readonly coverage: () => EmailEmulatorCoverage\n}\n\n// JSON helpers.\n\ntype JsonObject = Schema.JsonObject\n\nconst isJsonObject = (value: Schema.Json | undefined): value is JsonObject =>\n value !== undefined && value !== null && Predicate.isObject(value) && !Array.isArray(value)\n\nconst objectField = (value: Schema.Json | undefined, key: string): JsonObject | undefined => {\n const field = isJsonObject(value) ? value[key] : undefined\n\n return isJsonObject(field) ? field : undefined\n}\n\nconst stringField = (value: Schema.Json | undefined, key: string): string | undefined => {\n const field = isJsonObject(value) ? value[key] : undefined\n\n return Predicate.isString(field) ? field : undefined\n}\n\nconst booleanField = (value: Schema.Json | undefined, key: string): boolean | undefined => {\n const field = isJsonObject(value) ? value[key] : undefined\n\n return Predicate.isBoolean(field) ? field : undefined\n}\n\nconst arrayField = (\n value: Schema.Json | undefined,\n key: string\n): ReadonlyArray<Schema.Json> | undefined => {\n const field = isJsonObject(value) ? value[key] : undefined\n\n return Array.isArray(field) ? field : undefined\n}\n\n// The same credential field names the conformance secret scan flags, plus `credential(s)`.\nconst credentialKeyPattern =\n /^(?:credentials?|(?:access|refresh|id|auth|api|session|private|bearer|oauth)[_-]?token|token|client[_-]?secret|secret(?:[_-]?key)?|private[_-]?key|password|passwd|api[_-]?key|authorization)$/i\n\n/** A copy without credential fields; `ignoreConnectionHost` applies the request-shape latitude. */\nconst scrub = (value: Schema.Json, ignoreConnectionHost: boolean): Schema.Json => {\n if (Array.isArray(value)) {\n return value.map(item => scrub(item, ignoreConnectionHost))\n }\n\n if (!isJsonObject(value)) {\n return value\n }\n\n const copy: Record<string, Schema.Json> = {}\n\n for (const [key, item] of Object.entries(value)) {\n if (credentialKeyPattern.test(key)) {\n continue\n }\n\n if (ignoreConnectionHost && key === 'connection' && isJsonObject(item)) {\n const { host: _host, ...connection } = item\n\n copy[key] = scrub(connection, ignoreConnectionHost)\n continue\n }\n\n copy[key] = scrub(item, ignoreConnectionHost)\n }\n\n return copy\n}\n\n/** Deep subset: objects by key, arrays element by element (same length), other values exactly. */\nconst containsSubset = (value: Schema.Json | undefined, pattern: Schema.Json): boolean => {\n if (value === undefined) {\n return false\n }\n\n if (Array.isArray(pattern)) {\n return (\n Array.isArray(value) &&\n value.length === pattern.length &&\n pattern.every((item, index) => containsSubset(value[index], item))\n )\n }\n\n if (isJsonObject(pattern)) {\n return (\n isJsonObject(value) &&\n Object.entries(pattern).every(([key, item]) => containsSubset(value[key], item))\n )\n }\n\n return Equal.equals(value, pattern)\n}\n\n// Mailbox state.\n\ntype MessageState = {\n id?: string\n uidl?: string\n subject?: string\n isRead?: boolean\n isFlagged?: boolean\n}\n\ntype FolderState = {\n readonly name: string\n readonly specialUse?: EmailEmulatorSpecialUse\n messages: Array<MessageState>\n}\n\ntype MailboxState = { readonly folders: Array<FolderState> }\n\nconst copyMessage = (message: EmailEmulatorMessage | MessageState): MessageState => {\n const copy: MessageState = {}\n\n if (message.id !== undefined) copy.id = message.id\n\n if (message.uidl !== undefined) copy.uidl = message.uidl\n\n if (message.subject !== undefined) copy.subject = message.subject\n\n if (message.isRead !== undefined) copy.isRead = message.isRead\n\n if (message.isFlagged !== undefined) copy.isFlagged = message.isFlagged\n\n return copy\n}\n\nconst mailboxFrom = (seed: EmailEmulatorSeed): MailboxState => ({\n folders: seed.folders.map(folder =>\n folder.specialUse === undefined\n ? { name: folder.name, messages: folder.messages.map(copyMessage) }\n : {\n name: folder.name,\n specialUse: folder.specialUse,\n messages: folder.messages.map(copyMessage)\n }\n )\n})\n\nconst snapshotOf = (state: MailboxState): EmailEmulatorSeed => ({\n folders: state.folders.map(folder =>\n folder.specialUse === undefined\n ? { name: folder.name, messages: folder.messages.map(copyMessage) }\n : {\n name: folder.name,\n specialUse: folder.specialUse,\n messages: folder.messages.map(copyMessage)\n }\n )\n})\n\nconst folderNamed = (state: MailboxState, name: string | undefined): FolderState | undefined =>\n name === undefined ? undefined : state.folders.find(folder => folder.name === name)\n\nconst specialUseFolder = (\n state: MailboxState,\n use: EmailEmulatorSpecialUse\n): FolderState | undefined => state.folders.find(folder => folder.specialUse === use)\n\nconst isPop3 = (request: JsonObject): boolean =>\n stringField(objectField(request, 'connection'), 'protocol') === 'pop3'\n\nconst messageIn = (\n folder: FolderState | undefined,\n id: string | undefined,\n pop3 = false\n): MessageState | undefined =>\n folder === undefined || id === undefined\n ? undefined\n : folder.messages.find(message => (pop3 ? message.uidl : message.id) === id)\n\nconst flagKeys = ['isRead', 'isFlagged'] as const\n\n/** Reported flags and subject agree with what the mailbox knows (unknown state agrees). */\nconst agrees = (message: MessageState, reported: Schema.Json | undefined): boolean => {\n const subject = stringField(reported, 'subject')\n\n return (\n flagKeys.every(key => {\n const value = booleanField(reported, key)\n\n return value === undefined || message[key] === undefined || message[key] === value\n }) &&\n (subject === undefined || message.subject === undefined || message.subject === subject)\n )\n}\n\n/** Remember flags and subject the mailbox did not know yet. */\nconst learn = (message: MessageState, reported: Schema.Json | undefined): void => {\n for (const key of flagKeys) {\n const value = booleanField(reported, key)\n\n if (message[key] === undefined && value !== undefined) {\n message[key] = value\n }\n }\n\n const subject = stringField(reported, 'subject')\n\n if (message.subject === undefined && subject !== undefined) {\n message.subject = subject\n }\n}\n\n/** A state model for one method: whether a fixture is consistent, and what it changes. */\ntype MethodModel = {\n readonly consistent: (\n state: MailboxState,\n request: JsonObject,\n fixture: EmailEmulatorFixture\n ) => boolean\n readonly apply: (state: MailboxState, request: JsonObject, fixture: EmailEmulatorFixture) => void\n}\n\nconst requestFolder = (state: MailboxState, request: JsonObject) =>\n folderNamed(state, stringField(request, 'folder') ?? 'INBOX')\n\n/** Every known message that certainly passes the request's read/flagged filters. */\nconst certainlyListed = (message: MessageState, request: JsonObject): boolean =>\n flagKeys.every(key => {\n const filter = booleanField(request, key)\n\n return filter === undefined || message[key] === filter\n })\n\nconst listModel: MethodModel = {\n consistent: (state, request, fixture) => {\n if (fixture.failure !== undefined) {\n return true\n }\n\n const folder = requestFolder(state, request)\n const listed = arrayField(fixture.response, 'messages')\n const pop3 = isPop3(request)\n\n if (folder === undefined || listed === undefined) {\n return false\n }\n\n const everyListedKnown = listed.every(summary => {\n const message = messageIn(folder, stringField(summary, 'id'), pop3)\n\n return message !== undefined && agrees(message, summary)\n })\n\n if (!everyListedKnown) {\n return false\n }\n\n if (stringField(fixture.response, 'nextCursor') !== undefined) {\n return true\n }\n\n const listedIds = new Set(listed.map(summary => stringField(summary, 'id')))\n\n return folder.messages.every(message => {\n const id = pop3 ? message.uidl : message.id\n\n return id === undefined || !certainlyListed(message, request) || listedIds.has(id)\n })\n },\n apply: (state, request, fixture) => {\n const folder = requestFolder(state, request)\n const pop3 = isPop3(request)\n\n for (const summary of arrayField(fixture.response, 'messages') ?? []) {\n const message = messageIn(folder, stringField(summary, 'id'), pop3)\n\n if (message !== undefined) {\n learn(message, summary)\n }\n }\n }\n}\n\nconst getModel: MethodModel = {\n consistent: (state, request, fixture) => {\n const id = stringField(request, 'messageId')\n const message = messageIn(requestFolder(state, request), id, isPop3(request))\n\n if (fixture.failure !== undefined) {\n // A failed get is consistent only where the mailbox has no such message.\n return message === undefined\n }\n\n const reported = objectField(fixture.response, 'message')\n\n return message !== undefined && stringField(reported, 'id') === id && agrees(message, reported)\n },\n apply: (state, request, fixture) => {\n const message = messageIn(\n requestFolder(state, request),\n stringField(request, 'messageId'),\n isPop3(request)\n )\n\n if (message !== undefined && fixture.failure === undefined) {\n learn(message, objectField(fixture.response, 'message'))\n }\n }\n}\n\nconst flagModel = (key: 'isRead' | 'isFlagged'): MethodModel => ({\n consistent: (state, request, fixture) => {\n if (fixture.failure !== undefined) {\n return true\n }\n\n const id = stringField(request, 'messageId')\n\n return (\n messageIn(requestFolder(state, request), id) !== undefined &&\n stringField(fixture.response, 'messageId') === id &&\n booleanField(fixture.response, key) === booleanField(request, key)\n )\n },\n apply: (state, request, fixture) => {\n const message = messageIn(requestFolder(state, request), stringField(request, 'messageId'))\n const value = booleanField(request, key)\n\n if (fixture.failure === undefined && message !== undefined && value !== undefined) {\n message[key] = value\n }\n }\n})\n\n/** A move-shaped method: where the message comes from and which destination is acceptable. */\nconst moveModel = (\n source: (state: MailboxState, request: JsonObject) => FolderState | undefined,\n destinationAllowed: (\n state: MailboxState,\n request: JsonObject,\n destination: FolderState\n ) => boolean\n): MethodModel => {\n const plan = (state: MailboxState, request: JsonObject, fixture: EmailEmulatorFixture) => {\n const from = source(state, request)\n const message = messageIn(from, stringField(request, 'messageId'))\n const destination = folderNamed(state, stringField(fixture.response, 'folder'))\n const movedId = stringField(fixture.response, 'messageId')\n\n return { from, message, destination, movedId }\n }\n\n return {\n consistent: (state, request, fixture) => {\n if (fixture.failure !== undefined) {\n return true\n }\n\n const { from, message, destination, movedId } = plan(state, request, fixture)\n\n return (\n booleanField(fixture.response, 'moved') === true &&\n from !== undefined &&\n message !== undefined &&\n destination !== undefined &&\n destination !== from &&\n destinationAllowed(state, request, destination) &&\n messageIn(destination, movedId) === undefined\n )\n },\n apply: (state, request, fixture) => {\n if (fixture.failure !== undefined) {\n return\n }\n\n const { from, message, destination, movedId } = plan(state, request, fixture)\n\n if (from === undefined || message === undefined || destination === undefined) {\n return\n }\n\n from.messages = from.messages.filter(candidate => candidate !== message)\n\n const moved: MessageState = { ...message }\n\n delete moved.uidl\n\n if (movedId === undefined) {\n delete moved.id\n } else {\n moved.id = movedId\n }\n\n destination.messages.push(moved)\n }\n }\n}\n\nconst createDraftModel: MethodModel = {\n consistent: (state, request, fixture) => {\n if (fixture.failure !== undefined) {\n return true\n }\n\n const destination = folderNamed(state, stringField(fixture.response, 'folder'))\n const requested = stringField(request, 'folder')\n\n return (\n booleanField(fixture.response, 'saved') === true &&\n destination !== undefined &&\n (requested === undefined\n ? destination.specialUse === '\\\\Drafts'\n : destination.name === requested) &&\n messageIn(destination, stringField(fixture.response, 'draftId')) === undefined\n )\n },\n apply: (state, request, fixture) => {\n const destination = folderNamed(state, stringField(fixture.response, 'folder'))\n\n if (fixture.failure !== undefined || destination === undefined) {\n return\n }\n\n const draft: MessageState = {}\n const draftId = stringField(fixture.response, 'draftId')\n const subject = stringField(objectField(request, 'message'), 'subject')\n\n if (draftId !== undefined) draft.id = draftId\n\n if (subject !== undefined) draft.subject = subject\n\n destination.messages.push(draft)\n }\n}\n\nconst deletePermanentlyModel: MethodModel = {\n consistent: (state, request, fixture) => {\n if (fixture.failure !== undefined) {\n return true\n }\n\n const folder = requestFolder(state, request)\n const results = arrayField(fixture.response, 'results')\n\n return (\n folder !== undefined &&\n results !== undefined &&\n results.every(result => {\n const present = messageIn(folder, stringField(result, 'messageId')) !== undefined\n const status = stringField(result, 'status')\n\n return status === 'succeeded'\n ? present\n : status !== 'failed' || stringField(result, 'code') !== 'not_found' || !present\n })\n )\n },\n apply: (state, request, fixture) => {\n const folder = requestFolder(state, request)\n\n if (fixture.failure !== undefined || folder === undefined) {\n return\n }\n\n const deleted = new Set(\n (arrayField(fixture.response, 'results') ?? []).flatMap(result =>\n stringField(result, 'status') === 'succeeded' ? [stringField(result, 'messageId')] : []\n )\n )\n\n folder.messages = folder.messages.filter(\n message => message.id === undefined || !deleted.has(message.id)\n )\n }\n}\n\n/** The mailbox a saved Sent copy lands in, or `undefined` when none is consistent. */\nconst savedCopyFolder = (\n state: MailboxState,\n request: JsonObject,\n fixture: EmailEmulatorFixture\n): FolderState | undefined => {\n const requestedCopy = objectField(request, 'sentCopy')\n\n if (requestedCopy === undefined) {\n return undefined\n }\n\n const requestedFolder = stringField(requestedCopy, 'folder')\n const reportedFolder = stringField(objectField(fixture.response, 'sentCopy'), 'folder')\n\n const folder =\n requestedFolder === undefined\n ? reportedFolder === undefined\n ? specialUseFolder(state, '\\\\Sent')\n : folderNamed(state, reportedFolder)\n : folderNamed(state, requestedFolder)\n\n if (folder === undefined || (reportedFolder !== undefined && reportedFolder !== folder.name)) {\n return undefined\n }\n\n return requestedFolder !== undefined || folder.specialUse === '\\\\Sent' ? folder : undefined\n}\n\nconst sentCopyStatus = (fixture: EmailEmulatorFixture) =>\n stringField(objectField(fixture.response, 'sentCopy'), 'status')\n\nconst sendMessageModel: MethodModel = {\n consistent: (state, request, fixture) => {\n if (fixture.failure !== undefined) {\n return true\n }\n\n const status = sentCopyStatus(fixture)\n const requestedCopy = objectField(request, 'sentCopy')\n const requestedFolder = stringField(requestedCopy, 'folder')\n\n // A failed copy needs a requested copy; when the request names a folder, that folder must be\n // missing from the mailbox (the only failure the fixtures record).\n const failedConsistent =\n requestedCopy !== undefined &&\n (requestedFolder === undefined || folderNamed(state, requestedFolder) === undefined)\n\n return (\n booleanField(fixture.response, 'accepted') === true &&\n (status === 'saved'\n ? savedCopyFolder(state, request, fixture) !== undefined\n : status !== 'failed' || failedConsistent)\n )\n },\n apply: (state, request, fixture) => {\n if (fixture.failure !== undefined || sentCopyStatus(fixture) !== 'saved') {\n return\n }\n\n const folder = savedCopyFolder(state, request, fixture)\n const subject = stringField(objectField(request, 'message'), 'subject')\n\n folder?.messages.push(subject === undefined ? {} : { subject })\n }\n}\n\nconst routeKey = (method: string) =>\n emulatorRouteKey(emailEmulatorRouteMethod, `${emailEmulatorPort}.${method}`)\n\nconst methodModels: ReadonlyMap<string, MethodModel> = new Map([\n [routeKey('listMessages'), listModel],\n [routeKey('listMessagesFiltered'), listModel],\n [routeKey('getMessage'), getModel],\n [routeKey('createDraft'), createDraftModel],\n [routeKey('sendMessage'), sendMessageModel],\n [routeKey('setRead'), flagModel('isRead')],\n [routeKey('setFlag'), flagModel('isFlagged')],\n [\n routeKey('trash'),\n moveModel(\n (state, request) => requestFolder(state, request),\n (_state, request, destination) => {\n const trashFolder = stringField(request, 'trashFolder')\n\n return trashFolder === undefined\n ? destination.specialUse === '\\\\Trash'\n : destination.name === trashFolder\n }\n )\n ],\n [\n routeKey('untrash'),\n moveModel(\n (state, request) => {\n const folder = stringField(request, 'folder')\n\n return folder === undefined\n ? specialUseFolder(state, '\\\\Trash')\n : folderNamed(state, folder)\n },\n (_state, request, destination) =>\n destination.name === stringField(request, 'destinationFolder')\n )\n ],\n [\n routeKey('move'),\n moveModel(\n (state, request) => requestFolder(state, request),\n (_state, request, destination) =>\n destination.name === stringField(request, 'destinationFolder')\n )\n ],\n [routeKey('deletePermanently'), deletePermanentlyModel]\n])\n\nconst strict = { onExcessProperty: 'error' } as const\n\nconst decodeSeed = Schema.decodeUnknownResult(EmailEmulatorSeed, strict)\n\nconst decodeFault = Schema.decodeUnknownResult(EmailEmulatorFault, strict)\n\nconst issueText = (error: Schema.SchemaError): string => error.message\n\n/** Folder names and message ids (and POP3 ids) must be unique so fixtures address one message. */\nconst seedProblem = (seed: EmailEmulatorSeed): string | undefined => {\n const names = seed.folders.map(folder => folder.name)\n\n if (new Set(names).size !== names.length) {\n return 'folder names must be unique'\n }\n\n for (const folder of seed.folders) {\n const ids = folder.messages.flatMap(message => (message.id === undefined ? [] : [message.id]))\n\n const uidls = folder.messages.flatMap(message =>\n message.uidl === undefined ? [] : [message.uidl]\n )\n\n if (new Set(ids).size !== ids.length || new Set(uidls).size !== uidls.length) {\n return `message ids in ${folder.name} must be unique`\n }\n }\n\n return undefined\n}\n\n/**\n * Create an email emulator. Each call has its own mailbox, ledger, faults, and fixture use.\n *\n * `call(method, request)` answers in this order: an unknown method or a non-object request fails\n * closed; the first active matching fault answers its failure; otherwise the fixtures whose method\n * and request match (after the request-shape latitude) are checked against the mailbox, and the\n * first consistent one (preferring one not used since the last reset) answers and updates the\n * mailbox; no match (`no-matching-fixture`) or no consistent match (`state-conflict`) fails closed.\n * Every call is written to the ledger without credential fields.\n *\n * Throws `EmailEmulatorInputInvalid` for an invalid seed.\n */\nexport const makeEmailEmulator = (options: EmailEmulatorOptions = {}): EmailEmulator => {\n const seedResult = decodeSeed(options.seed ?? emailEmulatorDefaultSeed)\n\n if (Result.isFailure(seedResult)) {\n throw new EmailEmulatorInputInvalid({ input: 'seed', reason: issueText(seedResult.failure) })\n }\n\n const seed = seedResult.success\n const problem = seedProblem(seed)\n\n if (problem !== undefined) {\n throw new EmailEmulatorInputInvalid({ input: 'seed', reason: problem })\n }\n\n const bound = new Map(\n bindRouteHandlers(emailEmulatorRoutes, methodModels).map(({ route, handler }) => [\n route.path.slice(emailEmulatorPort.length + 1),\n { route, model: handler }\n ])\n )\n\n let state = mailboxFrom(seed)\n let entries: Array<EmailEmulatorLedgerEntry> = []\n\n let faults: Array<{\n id: number\n fault: EmailEmulatorFault\n remaining: number | undefined\n applied: number\n }> = []\n\n let nextFaultId = 1\n let nextSeq = 1\n const used = new Set<string>()\n\n const record = (entry: Omit<EmailEmulatorLedgerEntry, 'seq'>) => {\n entries.push({ seq: nextSeq, ...entry })\n nextSeq += 1\n }\n\n const refuse = (\n method: string,\n request: Schema.Json,\n reason: EmailEmulatorNotEmulatedReason,\n evidence: EmailEmulatorLedgerEntry['evidence']\n ): EmailEmulatorReply => {\n record({ method, request, outcome: 'not-emulated', evidence, reason })\n\n return { notEmulated: { reason: notEmulatedText[reason] } }\n }\n\n const call = (method: string, rawRequest: Schema.Json): EmailEmulatorReply => {\n const request = scrub(rawRequest, false)\n const target = bound.get(method)\n\n if (target === undefined) {\n return refuse(method, request, 'unknown-method', 'unknown-method')\n }\n\n const evidence = target.route.evidence\n\n if (!isJsonObject(request)) {\n return refuse(method, request, 'invalid-request', evidence)\n }\n\n const comparable = scrub(request, true)\n\n const matching = emailEmulatorFixtures.filter(\n fixture =>\n fixture.port === emailEmulatorPort &&\n fixture.method === method &&\n Equal.equals(scrub(fixture.request, true), comparable)\n )\n\n if (matching.length === 0) {\n return refuse(method, request, 'no-matching-fixture', evidence)\n }\n\n const consistent = matching.filter(fixture => target.model.consistent(state, request, fixture))\n const chosen = consistent.find(fixture => !used.has(fixture.id)) ?? consistent[0]\n\n if (chosen === undefined) {\n return refuse(method, request, 'state-conflict', evidence)\n }\n\n // Faults apply only to a call a fixture would answer: an unmatched or state-conflicting call\n // stays refused above and leaves every fault untouched. A fault changes no mailbox state.\n const fault = faults.find(\n candidate =>\n candidate.fault.method === method &&\n (candidate.remaining === undefined || candidate.remaining > 0) &&\n (candidate.fault.match === undefined || containsSubset(request, candidate.fault.match))\n )\n\n if (fault !== undefined) {\n fault.applied += 1\n\n if (fault.remaining !== undefined) {\n fault.remaining -= 1\n }\n\n record({ method, request, outcome: 'fault', evidence, faultId: fault.id })\n\n return { failure: fault.fault.failure }\n }\n\n target.model.apply(state, request, chosen)\n used.add(chosen.id)\n record({ method, request, outcome: 'answered', evidence, fixtureId: chosen.id })\n\n return chosen.failure === undefined\n ? { response: chosen.response }\n : { failure: chosen.failure }\n }\n\n const faultState = (fault: (typeof faults)[number]): EmailEmulatorFaultState => ({\n id: fault.id,\n fault: fault.fault,\n remaining: fault.remaining,\n applied: fault.applied\n })\n\n return {\n call,\n state: () => snapshotOf(state),\n seed: () => snapshotOf(mailboxFrom(seed)),\n ledger: {\n entries: () => entries.map(entry => ({ ...entry })),\n clear: () => {\n entries = []\n }\n },\n reset: () => {\n state = mailboxFrom(seed)\n entries = []\n faults = []\n used.clear()\n },\n faults: {\n add: input => {\n const decoded = decodeFault(input)\n\n if (Result.isFailure(decoded)) {\n throw new EmailEmulatorInputInvalid({\n input: 'fault',\n reason: issueText(decoded.failure)\n })\n }\n\n if (!bound.has(decoded.success.method)) {\n throw new EmailEmulatorInputInvalid({\n input: 'fault',\n reason: `method ${decoded.success.method} has no emulated route`\n })\n }\n\n const added = {\n id: nextFaultId,\n fault: decoded.success,\n remaining: decoded.success.count,\n applied: 0\n }\n\n nextFaultId += 1\n faults.push(added)\n\n return faultState(added)\n },\n list: () => faults.map(faultState),\n clear: () => {\n faults = []\n }\n },\n coverage: () => ({\n routes: emailEmulatorRoutes.map(route => ({\n ...route,\n calls: entries.filter(entry => `${emailEmulatorPort}.${entry.method}` === route.path).length\n })),\n notEmulatedCalls: entries.filter(entry => entry.outcome === 'not-emulated').length,\n unusedFixtureIds: emailEmulatorFixtures.flatMap(fixture =>\n used.has(fixture.id) ? [] : [fixture.id]\n )\n })\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { EmulatorCoverage } from "./emulator-kernel.mjs";
2
+
3
+ //#region src/emulator-compose.d.ts
4
+ /** What composition needs from each emulator. */
5
+ type ComposedPart = {
6
+ readonly fetch: (request: Request) => Promise<Response>;
7
+ readonly reset: () => void;
8
+ readonly coverage: () => EmulatorCoverage;
9
+ };
10
+ type ComposedRoute = {
11
+ /** Control-plane name: `/_emulate/<name>/...`. */readonly name: string; /** API paths this part answers (its manifest paths). */
12
+ readonly paths: ReadonlyArray<string>;
13
+ readonly part: ComposedPart;
14
+ };
15
+ declare const isControlPlanePath: (path: string) => boolean;
16
+ /** Every part's coverage in route order, with the unknown-route counts summed. */
17
+ declare const combinedCoverage: (parts: ReadonlyArray<ComposedPart>) => EmulatorCoverage;
18
+ type ComposedFetchOptions = {
19
+ readonly routes: ReadonlyArray<ComposedRoute>; /** Answers API requests on no part's path (and fails them closed). */
20
+ readonly fallback: ComposedPart;
21
+ /**
22
+ * Answers the remaining `/_emulate/*` requests (not a part's prefix, not `POST
23
+ * /_emulate/reset`). Defaults to a 404 control error.
24
+ */
25
+ readonly control?: (request: Request, path: string) => Promise<Response>;
26
+ };
27
+ /** One fetch handler over several parts. Never rejects. */
28
+ declare const composeFetch: (options: ComposedFetchOptions) => (request: Request) => Promise<Response>;
29
+ /**
30
+ * A wire-core emulator with its subscription-usage route on the same origin: `path` goes to
31
+ * `usage`, `/_emulate/usage/...` is the usage control plane, `POST /_emulate/reset` and `reset()`
32
+ * reset both, and everything else (other API paths, other `/_emulate/*` routes) is the primary
33
+ * emulator's, unchanged. The primary's ledger, faults, script, and coverage stay its own.
34
+ */
35
+ declare const withSubscriptionUsage: <Primary extends ComposedPart, Usage extends ComposedPart>(primary: Primary, usage: Usage, path: string) => Primary & {
36
+ readonly usage: Usage;
37
+ };
38
+ //#endregion
39
+ export { ComposedFetchOptions, ComposedPart, ComposedRoute, combinedCoverage, composeFetch, isControlPlanePath, withSubscriptionUsage };
40
+ //# sourceMappingURL=emulator-compose.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulator-compose.d.mts","names":[],"sources":["../src/emulator-compose.ts"],"mappings":";;;;KAoBY,YAAA;EAAA,SACD,KAAA,GAAQ,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EAAA,SACrC,KAAA;EAAA,SACA,QAAA,QAAgB,gBAAA;AAAA;AAAA,KAGf,aAAA;EAKiB,2DAHlB,IAAA,UAEA;EAAA,SAAA,KAAA,EAAO,aAAA;EAAA,SACP,IAAA,EAAM,YAAY;AAAA;AAAA,cA+BhB,kBAAA,GAAsB,IAAY;AA/BlB;AAAA,cAmChB,gBAAA,GAAoB,KAAA,EAAO,aAAA,CAAc,YAAA,MAAgB,gBAAA;AAAA,KAY1D,oBAAA;EAAA,SACD,MAAA,EAAQ,aAAA,CAAc,aAAA,GAjBc;EAAA,SAmBpC,QAAA,EAAU,YAAA;EAfR;;;;EAAA,SAoBF,OAAA,IAAW,OAAA,EAAS,OAAA,EAAS,IAAA,aAAiB,OAAA,CAAQ,QAAA;AAAA;;cAIpD,YAAA,GACV,OAAA,EAAS,oBAAA,MACT,OAAA,EAAS,OAAA,KAAU,OAAA,CAAQ,QAAA;;;;;;AAhB7B;cAsDY,qBAAA,mBAAyC,YAAA,gBAA4B,YAAA,EAChF,OAAA,EAAS,OAAA,EACT,KAAA,EAAO,KAAA,EACP,IAAA,aACC,OAAA;EAAA,SAAqB,KAAA,EAAO,KAAA;AAAA"}
@@ -0,0 +1,91 @@
1
+ import { controlError, jsonResponse } from "./emulator-kernel.mjs";
2
+ //#region src/emulator-compose.ts
3
+ /**
4
+ * Composition of several kernel-built emulators behind one origin (internal; not a package
5
+ * export).
6
+ *
7
+ * The router takes one route per origin, so every route of one service origin must be answered by
8
+ * one fetch handler. When an origin carries routes of different wire cores (OpenCode Go serves
9
+ * Chat Completions, Messages, Responses, and usage under `https://opencode.ai`; Anthropic, Codex,
10
+ * and Grok serve their subscription-usage route next to their model route), each core keeps its
11
+ * own manifest, ledger, faults, turns, and control plane, and this module dispatches by path:
12
+ *
13
+ * - an API request whose path is a part's route path goes to that part; any other API request goes
14
+ * to the fallback part (which fails it closed as an unknown route and records it);
15
+ * - `/_emulate/<part>/...` is that part's control plane (`/_emulate/...` inside the part);
16
+ * - `POST /_emulate/reset` resets every part.
17
+ *
18
+ * Runtime-portable Web APIs only; no Node builtins and no SDK imports.
19
+ */
20
+ const requestPath = (request) => URL.canParse(request.url) ? new URL(request.url).pathname : "/";
21
+ /** `/_emulate/<name>[/rest]` as the part's own control-plane path, or undefined. */
22
+ const partControlPath = (path, name) => {
23
+ const prefix = `/_emulate/${name}`;
24
+ if (path === prefix) return "/_emulate";
25
+ return path.startsWith(`${prefix}/`) ? `/_emulate${path.slice(prefix.length)}` : void 0;
26
+ };
27
+ /** Re-send a control-plane request to a part under its own control-plane path. */
28
+ const forwardControl = async (request, part, path) => {
29
+ const url = new URL(request.url);
30
+ url.pathname = path;
31
+ const body = request.method === "GET" || request.method === "HEAD" ? void 0 : await request.text();
32
+ return part.fetch(new Request(url, {
33
+ method: request.method,
34
+ headers: request.headers,
35
+ body
36
+ }));
37
+ };
38
+ const isControlPlanePath = (path) => path === "/_emulate" || path.startsWith("/_emulate/");
39
+ /** Every part's coverage in route order, with the unknown-route counts summed. */
40
+ const combinedCoverage = (parts) => {
41
+ const coverages = parts.map((part) => part.coverage());
42
+ return {
43
+ routes: coverages.flatMap((coverage) => coverage.routes),
44
+ unknownRouteRequests: coverages.reduce((total, coverage) => total + coverage.unknownRouteRequests, 0)
45
+ };
46
+ };
47
+ /** One fetch handler over several parts. Never rejects. */
48
+ const composeFetch = (options) => (request) => {
49
+ const handle = async () => {
50
+ const path = requestPath(request);
51
+ if (!isControlPlanePath(path)) return (options.routes.find((candidate) => candidate.paths.includes(path))?.part ?? options.fallback).fetch(request);
52
+ for (const route of options.routes) {
53
+ const partPath = partControlPath(path, route.name);
54
+ if (partPath !== void 0) return forwardControl(request, route.part, partPath);
55
+ }
56
+ if (path === "/_emulate/reset" && request.method === "POST") {
57
+ options.fallback.reset();
58
+ for (const route of options.routes) route.part.reset();
59
+ return jsonResponse(200, { reset: true });
60
+ }
61
+ return options.control === void 0 ? controlError(404, "unknown control-plane route") : options.control(request, path);
62
+ };
63
+ return handle().catch(() => controlError(500, "emulator failed to handle the request"));
64
+ };
65
+ /**
66
+ * A wire-core emulator with its subscription-usage route on the same origin: `path` goes to
67
+ * `usage`, `/_emulate/usage/...` is the usage control plane, `POST /_emulate/reset` and `reset()`
68
+ * reset both, and everything else (other API paths, other `/_emulate/*` routes) is the primary
69
+ * emulator's, unchanged. The primary's ledger, faults, script, and coverage stay its own.
70
+ */
71
+ const withSubscriptionUsage = (primary, usage, path) => ({
72
+ ...primary,
73
+ fetch: composeFetch({
74
+ routes: [{
75
+ name: "usage",
76
+ paths: [path],
77
+ part: usage
78
+ }],
79
+ fallback: primary,
80
+ control: (request) => primary.fetch(request)
81
+ }),
82
+ reset: () => {
83
+ primary.reset();
84
+ usage.reset();
85
+ },
86
+ usage
87
+ });
88
+ //#endregion
89
+ export { combinedCoverage, composeFetch, isControlPlanePath, withSubscriptionUsage };
90
+
91
+ //# sourceMappingURL=emulator-compose.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulator-compose.mjs","names":[],"sources":["../src/emulator-compose.ts"],"sourcesContent":["/**\n * Composition of several kernel-built emulators behind one origin (internal; not a package\n * export).\n *\n * The router takes one route per origin, so every route of one service origin must be answered by\n * one fetch handler. When an origin carries routes of different wire cores (OpenCode Go serves\n * Chat Completions, Messages, Responses, and usage under `https://opencode.ai`; Anthropic, Codex,\n * and Grok serve their subscription-usage route next to their model route), each core keeps its\n * own manifest, ledger, faults, turns, and control plane, and this module dispatches by path:\n *\n * - an API request whose path is a part's route path goes to that part; any other API request goes\n * to the fallback part (which fails it closed as an unknown route and records it);\n * - `/_emulate/<part>/...` is that part's control plane (`/_emulate/...` inside the part);\n * - `POST /_emulate/reset` resets every part.\n *\n * Runtime-portable Web APIs only; no Node builtins and no SDK imports.\n */\nimport { controlError, jsonResponse, type EmulatorCoverage } from './emulator-kernel.ts'\n\n/** What composition needs from each emulator. */\nexport type ComposedPart = {\n readonly fetch: (request: Request) => Promise<Response>\n readonly reset: () => void\n readonly coverage: () => EmulatorCoverage\n}\n\nexport type ComposedRoute = {\n /** Control-plane name: `/_emulate/<name>/...`. */\n readonly name: string\n /** API paths this part answers (its manifest paths). */\n readonly paths: ReadonlyArray<string>\n readonly part: ComposedPart\n}\n\nconst requestPath = (request: Request): string =>\n URL.canParse(request.url) ? new URL(request.url).pathname : '/'\n\n/** `/_emulate/<name>[/rest]` as the part's own control-plane path, or undefined. */\nconst partControlPath = (path: string, name: string): string | undefined => {\n const prefix = `/_emulate/${name}`\n\n if (path === prefix) return '/_emulate'\n\n return path.startsWith(`${prefix}/`) ? `/_emulate${path.slice(prefix.length)}` : undefined\n}\n\n/** Re-send a control-plane request to a part under its own control-plane path. */\nconst forwardControl = async (\n request: Request,\n part: ComposedPart,\n path: string\n): Promise<Response> => {\n const url = new URL(request.url)\n\n url.pathname = path\n\n const body =\n request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.text()\n\n return part.fetch(new Request(url, { method: request.method, headers: request.headers, body }))\n}\n\nexport const isControlPlanePath = (path: string): boolean =>\n path === '/_emulate' || path.startsWith('/_emulate/')\n\n/** Every part's coverage in route order, with the unknown-route counts summed. */\nexport const combinedCoverage = (parts: ReadonlyArray<ComposedPart>): EmulatorCoverage => {\n const coverages = parts.map(part => part.coverage())\n\n return {\n routes: coverages.flatMap(coverage => coverage.routes),\n unknownRouteRequests: coverages.reduce(\n (total, coverage) => total + coverage.unknownRouteRequests,\n 0\n )\n }\n}\n\nexport type ComposedFetchOptions = {\n readonly routes: ReadonlyArray<ComposedRoute>\n /** Answers API requests on no part's path (and fails them closed). */\n readonly fallback: ComposedPart\n /**\n * Answers the remaining `/_emulate/*` requests (not a part's prefix, not `POST\n * /_emulate/reset`). Defaults to a 404 control error.\n */\n readonly control?: (request: Request, path: string) => Promise<Response>\n}\n\n/** One fetch handler over several parts. Never rejects. */\nexport const composeFetch =\n (options: ComposedFetchOptions) =>\n (request: Request): Promise<Response> => {\n const handle = async (): Promise<Response> => {\n const path = requestPath(request)\n\n if (!isControlPlanePath(path)) {\n const route = options.routes.find(candidate => candidate.paths.includes(path))\n\n return (route?.part ?? options.fallback).fetch(request)\n }\n\n for (const route of options.routes) {\n const partPath = partControlPath(path, route.name)\n\n if (partPath !== undefined) return forwardControl(request, route.part, partPath)\n }\n\n if (path === '/_emulate/reset' && request.method === 'POST') {\n options.fallback.reset()\n\n for (const route of options.routes) route.part.reset()\n\n return jsonResponse(200, { reset: true })\n }\n\n return options.control === undefined\n ? controlError(404, 'unknown control-plane route')\n : options.control(request, path)\n }\n\n return handle().catch(() => controlError(500, 'emulator failed to handle the request'))\n }\n\n/**\n * A wire-core emulator with its subscription-usage route on the same origin: `path` goes to\n * `usage`, `/_emulate/usage/...` is the usage control plane, `POST /_emulate/reset` and `reset()`\n * reset both, and everything else (other API paths, other `/_emulate/*` routes) is the primary\n * emulator's, unchanged. The primary's ledger, faults, script, and coverage stay its own.\n */\nexport const withSubscriptionUsage = <Primary extends ComposedPart, Usage extends ComposedPart>(\n primary: Primary,\n usage: Usage,\n path: string\n): Primary & { readonly usage: Usage } => ({\n ...primary,\n fetch: composeFetch({\n routes: [{ name: 'usage', paths: [path], part: usage }],\n fallback: primary,\n control: request => primary.fetch(request)\n }),\n reset: () => {\n primary.reset()\n usage.reset()\n },\n usage\n})\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkCA,MAAM,eAAe,YACnB,IAAI,SAAS,QAAQ,GAAG,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,WAAW;;AAG9D,MAAM,mBAAmB,MAAc,SAAqC;CAC1E,MAAM,SAAS,aAAa;CAE5B,IAAI,SAAS,QAAQ,OAAO;CAE5B,OAAO,KAAK,WAAW,GAAG,OAAO,EAAE,IAAI,YAAY,KAAK,MAAM,OAAO,MAAM,MAAM,KAAA;AACnF;;AAGA,MAAM,iBAAiB,OACrB,SACA,MACA,SACsB;CACtB,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;CAE/B,IAAI,WAAW;CAEf,MAAM,OACJ,QAAQ,WAAW,SAAS,QAAQ,WAAW,SAAS,KAAA,IAAY,MAAM,QAAQ,KAAK;CAEzF,OAAO,KAAK,MAAM,IAAI,QAAQ,KAAK;EAAE,QAAQ,QAAQ;EAAQ,SAAS,QAAQ;EAAS;CAAK,CAAC,CAAC;AAChG;AAEA,MAAa,sBAAsB,SACjC,SAAS,eAAe,KAAK,WAAW,YAAY;;AAGtD,MAAa,oBAAoB,UAAyD;CACxF,MAAM,YAAY,MAAM,KAAI,SAAQ,KAAK,SAAS,CAAC;CAEnD,OAAO;EACL,QAAQ,UAAU,SAAQ,aAAY,SAAS,MAAM;EACrD,sBAAsB,UAAU,QAC7B,OAAO,aAAa,QAAQ,SAAS,sBACtC,CACF;CACF;AACF;;AAcA,MAAa,gBACV,aACA,YAAwC;CACvC,MAAM,SAAS,YAA+B;EAC5C,MAAM,OAAO,YAAY,OAAO;EAEhC,IAAI,CAAC,mBAAmB,IAAI,GAG1B,QAFc,QAAQ,OAAO,MAAK,cAAa,UAAU,MAAM,SAAS,IAAI,CAEhE,GAAG,QAAQ,QAAQ,UAAU,MAAM,OAAO;EAGxD,KAAK,MAAM,SAAS,QAAQ,QAAQ;GAClC,MAAM,WAAW,gBAAgB,MAAM,MAAM,IAAI;GAEjD,IAAI,aAAa,KAAA,GAAW,OAAO,eAAe,SAAS,MAAM,MAAM,QAAQ;EACjF;EAEA,IAAI,SAAS,qBAAqB,QAAQ,WAAW,QAAQ;GAC3D,QAAQ,SAAS,MAAM;GAEvB,KAAK,MAAM,SAAS,QAAQ,QAAQ,MAAM,KAAK,MAAM;GAErD,OAAO,aAAa,KAAK,EAAE,OAAO,KAAK,CAAC;EAC1C;EAEA,OAAO,QAAQ,YAAY,KAAA,IACvB,aAAa,KAAK,6BAA6B,IAC/C,QAAQ,QAAQ,SAAS,IAAI;CACnC;CAEA,OAAO,OAAO,EAAE,YAAY,aAAa,KAAK,uCAAuC,CAAC;AACxF;;;;;;;AAQF,MAAa,yBACX,SACA,OACA,UACyC;CACzC,GAAG;CACH,OAAO,aAAa;EAClB,QAAQ,CAAC;GAAE,MAAM;GAAS,OAAO,CAAC,IAAI;GAAG,MAAM;EAAM,CAAC;EACtD,UAAU;EACV,UAAS,YAAW,QAAQ,MAAM,OAAO;CAC3C,CAAC;CACD,aAAa;EACX,QAAQ,MAAM;EACd,MAAM,MAAM;CACd;CACA;AACF"}
@@ -0,0 +1,53 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ //#region src/emulator-http.d.ts
4
+ /** Statuses a fault or scripted error may answer with: 200-599 without 204, 205, and 3xx. */
5
+ declare const EmulatorResponseStatus: Schema.Int;
6
+ /** Response headers for a fault or scripted error: valid names and values, and no `location`. */
7
+ declare const EmulatorHeaderRecord: Schema.$Record<Schema.String, Schema.String>;
8
+ /** True for header names that carry credentials or session state. */
9
+ declare const isCredentialHeaderName: (name: string) => boolean;
10
+ /** What a redacted credential value becomes in a ledger. */
11
+ declare const redactedCredentialValue = "<redacted>";
12
+ /**
13
+ * A JSON value safe to keep in a ledger: every object key that `isCredentialHeaderName` accepts
14
+ * (for example `requests[].headers.Authorization` in a Graph `$batch` body), at any depth, has
15
+ * its value replaced by `<redacted>`. Other values are copied unchanged.
16
+ */
17
+ declare const redactCredentialFields: (value: Schema.Json) => Schema.Json;
18
+ /**
19
+ * True for query parameter names that carry credentials: every name the conformance fixture scan
20
+ * reports as a credential query parameter, plus every name `isCredentialHeaderName` accepts.
21
+ */
22
+ declare const isCredentialQueryKey: (name: string) => boolean;
23
+ /**
24
+ * Query parameters safe to keep in a ledger: the value of every key that `isCredentialQueryKey`
25
+ * accepts (for example `access_token`, `api_key`, or `X-Amz-Signature`) is replaced by
26
+ * `<redacted>`.
27
+ */
28
+ declare const redactCredentialQuery: (query: URLSearchParams) => Readonly<Record<string, string>>;
29
+ /**
30
+ * Internal response header a stateful core route sets on the bodiless 500 it answers when its
31
+ * handler throws. The wrapper never forwards it: it answers its own error envelope 500 instead
32
+ * and records `responseError` in the ledger.
33
+ */
34
+ declare const handlerFailedHeader = "x-emulator-handler-failed";
35
+ /** The core route's answer when its handler threw (see `handlerFailedHeader`). */
36
+ declare const handlerFailedResponse: () => Response;
37
+ /**
38
+ * Internal request header a stateful wrapper sets on the requests it forwards to its core: the
39
+ * id of the wrapper's job for that request (the fault decision the core route asks for, and what
40
+ * the route decided). Ids come from a counter that never resets (the ledger sequence does), so a
41
+ * ledger clear never makes two jobs share one. Clients cannot set it: wrappers build the core
42
+ * request headers themselves.
43
+ */
44
+ declare const emulatorJobHeader = "x-emulator-job-id";
45
+ /**
46
+ * What a core route answers when the wrapper returns the real answer itself (a fault, or in the
47
+ * shared wrapper's resolved mode a refusal), so that answer never depends on the core runtime's
48
+ * lifecycle: a reset or a close before it is read never cancels it.
49
+ */
50
+ declare const answeredOutsideCore: () => Response;
51
+ //#endregion
52
+ export { EmulatorHeaderRecord, EmulatorResponseStatus, answeredOutsideCore, emulatorJobHeader, handlerFailedHeader, handlerFailedResponse, isCredentialHeaderName, isCredentialQueryKey, redactCredentialFields, redactCredentialQuery, redactedCredentialValue };
53
+ //# sourceMappingURL=emulator-http.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulator-http.d.mts","names":[],"sources":["../src/emulator-http.ts"],"mappings":";;;;cAwBa,sBAAA,EAAsB,MAAA,CAAA,GAGlC;AAHD;AAAA,cA8Ca,oBAAA,EAAoB,MAAA,CAAA,OAAA,CAAA,MAAA,CAAA,MAAA,EAAA,MAAA,CAAA,MAAA;;cA2BpB,sBAAA,GAA0B,IAAY;AAtElD;AAAA,cAiFY,uBAAA;;;;;;cAUA,sBAAA,GAA0B,KAAA,EAAO,MAAA,CAAO,IAAA,KAAO,MAAA,CAAO,IAWlE;;;;;cAaY,oBAAA,GAAwB,IAAY;;;;AAxEhB;AA2BjC;cAqDa,qBAAA,GAAyB,KAAA,EAAO,eAAA,KAAkB,QAAA,CAAS,MAAA;;;AArDrB;AAWnD;;cAuDa,mBAAA;;cAGA,qBAAA,QAA4B,QACqC;AAjD9E;;;;;;;AAAA,cA0Da,iBAAA;;;AA/CZ;AAaD;;cAyCa,mBAAA,QAA0B,QAA+C"}
@@ -0,0 +1,117 @@
1
+ import { Predicate } from "effect";
2
+ import * as Schema from "effect/Schema";
3
+ //#region src/emulator-http.ts
4
+ /**
5
+ * HTTP rules shared by the emulators (internal): which statuses and headers a fault or scripted
6
+ * error may answer with, which request header and query parameter names carry credentials, and the
7
+ * marker a stateful core route uses to report a handler that threw.
8
+ *
9
+ * Faults and scripted errors always carry a body and never redirect, so 1xx, 204, 205, and every
10
+ * 3xx (including 304) are rejected when the fault or turn is added, as are invalid header names or
11
+ * values, a `location` header, and the framing headers the server sets itself. Route statuses
12
+ * (for example a fixture's bodiless 204 or its 202 with a `Location`) follow the fixtures instead.
13
+ *
14
+ * Runtime-portable (no Node builtins): the emulators are plain Web fetch handlers.
15
+ */
16
+ /** Why a status cannot answer an emulated request, or `undefined` when it can. */
17
+ const emulatorStatusProblem = (status) => status >= 300 && status <= 399 ? "emulators never redirect: 3xx statuses are not allowed" : status === 204 || status === 205 ? "a 204 or 205 response cannot carry a body" : void 0;
18
+ /** Statuses a fault or scripted error may answer with: 200-599 without 204, 205, and 3xx. */
19
+ const EmulatorResponseStatus = Schema.Int.check(Schema.isBetween({
20
+ minimum: 200,
21
+ maximum: 599
22
+ }), Schema.makeFilter(emulatorStatusProblem));
23
+ const headerNamePattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
24
+ const headerValuePattern = /^[\t\x20-\x7e\x80-\xff]*$/;
25
+ const framingHeaders = new Set([
26
+ "content-length",
27
+ "transfer-encoding",
28
+ "connection",
29
+ "keep-alive",
30
+ "upgrade"
31
+ ]);
32
+ /** Why a header record cannot be sent by an emulator, or `undefined` when it can. */
33
+ const emulatorHeaderRecordProblem = (headers) => {
34
+ for (const [name, value] of Object.entries(headers)) {
35
+ if (!headerNamePattern.test(name)) return `header name ${JSON.stringify(name)} is not a valid HTTP token`;
36
+ if (name.toLowerCase() === "location") return "emulators never redirect: a location header is not allowed";
37
+ if (framingHeaders.has(name.toLowerCase())) return `header ${name} is set by the server and cannot be scripted`;
38
+ if (!headerValuePattern.test(value)) return `header ${name} has a value with control or non-Latin-1 characters`;
39
+ }
40
+ };
41
+ /** Response headers for a fault or scripted error: valid names and values, and no `location`. */
42
+ const EmulatorHeaderRecord = Schema.Record(Schema.String, Schema.String).check(Schema.makeFilter(emulatorHeaderRecordProblem));
43
+ const credentialHeaderNames = new Set([
44
+ "authorization",
45
+ "proxy-authorization",
46
+ "cookie",
47
+ "set-cookie",
48
+ "x-api-key",
49
+ "api-key",
50
+ "x-goog-api-key",
51
+ "x-auth-token",
52
+ "x-access-token",
53
+ "x-amz-security-token",
54
+ "x-vercel-oidc-token",
55
+ "x-csrf-token"
56
+ ]);
57
+ const credentialHeaderPattern = /(api[-_]?key|secret|password|cookie|authorization)/i;
58
+ const credentialHeaderSegmentPattern = /(^|[-_])(token|key|auth)([-_]|$)/i;
59
+ /** True for header names that carry credentials or session state. */
60
+ const isCredentialHeaderName = (name) => {
61
+ const lower = name.toLowerCase();
62
+ return credentialHeaderNames.has(lower) || credentialHeaderPattern.test(lower) || credentialHeaderSegmentPattern.test(lower);
63
+ };
64
+ /** What a redacted credential value becomes in a ledger. */
65
+ const redactedCredentialValue = "<redacted>";
66
+ const isJsonRecord = (value) => value !== null && Predicate.isObject(value) && !Array.isArray(value);
67
+ /**
68
+ * A JSON value safe to keep in a ledger: every object key that `isCredentialHeaderName` accepts
69
+ * (for example `requests[].headers.Authorization` in a Graph `$batch` body), at any depth, has
70
+ * its value replaced by `<redacted>`. Other values are copied unchanged.
71
+ */
72
+ const redactCredentialFields = (value) => {
73
+ if (Array.isArray(value)) return value.map(redactCredentialFields);
74
+ if (!isJsonRecord(value)) return value;
75
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, isCredentialHeaderName(key) ? redactedCredentialValue : redactCredentialFields(entry)]));
76
+ };
77
+ const credentialQueryParamPattern = /^(api[_-]?key|key|token|access[_-]?token|refresh[_-]?token|id[_-]?token|auth|secret|password|client[_-]?secret|x-amz-signature|x-amz-credential|x-amz-security-token)$/i;
78
+ /**
79
+ * True for query parameter names that carry credentials: every name the conformance fixture scan
80
+ * reports as a credential query parameter, plus every name `isCredentialHeaderName` accepts.
81
+ */
82
+ const isCredentialQueryKey = (name) => credentialQueryParamPattern.test(name) || isCredentialHeaderName(name);
83
+ /**
84
+ * Query parameters safe to keep in a ledger: the value of every key that `isCredentialQueryKey`
85
+ * accepts (for example `access_token`, `api_key`, or `X-Amz-Signature`) is replaced by
86
+ * `<redacted>`.
87
+ */
88
+ const redactCredentialQuery = (query) => Object.fromEntries([...query].map(([key, value]) => [key, isCredentialQueryKey(key) ? redactedCredentialValue : value]));
89
+ /**
90
+ * Internal response header a stateful core route sets on the bodiless 500 it answers when its
91
+ * handler throws. The wrapper never forwards it: it answers its own error envelope 500 instead
92
+ * and records `responseError` in the ledger.
93
+ */
94
+ const handlerFailedHeader = "x-emulator-handler-failed";
95
+ /** The core route's answer when its handler threw (see `handlerFailedHeader`). */
96
+ const handlerFailedResponse = () => new Response(null, {
97
+ status: 500,
98
+ headers: { [handlerFailedHeader]: "1" }
99
+ });
100
+ /**
101
+ * Internal request header a stateful wrapper sets on the requests it forwards to its core: the
102
+ * id of the wrapper's job for that request (the fault decision the core route asks for, and what
103
+ * the route decided). Ids come from a counter that never resets (the ledger sequence does), so a
104
+ * ledger clear never makes two jobs share one. Clients cannot set it: wrappers build the core
105
+ * request headers themselves.
106
+ */
107
+ const emulatorJobHeader = "x-emulator-job-id";
108
+ /**
109
+ * What a core route answers when the wrapper returns the real answer itself (a fault, or in the
110
+ * shared wrapper's resolved mode a refusal), so that answer never depends on the core runtime's
111
+ * lifecycle: a reset or a close before it is read never cancels it.
112
+ */
113
+ const answeredOutsideCore = () => new Response(null, { status: 204 });
114
+ //#endregion
115
+ export { EmulatorHeaderRecord, EmulatorResponseStatus, answeredOutsideCore, emulatorJobHeader, handlerFailedHeader, handlerFailedResponse, isCredentialHeaderName, isCredentialQueryKey, redactCredentialFields, redactCredentialQuery, redactedCredentialValue };
116
+
117
+ //# sourceMappingURL=emulator-http.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulator-http.mjs","names":[],"sources":["../src/emulator-http.ts"],"sourcesContent":["/**\n * HTTP rules shared by the emulators (internal): which statuses and headers a fault or scripted\n * error may answer with, which request header and query parameter names carry credentials, and the\n * marker a stateful core route uses to report a handler that threw.\n *\n * Faults and scripted errors always carry a body and never redirect, so 1xx, 204, 205, and every\n * 3xx (including 304) are rejected when the fault or turn is added, as are invalid header names or\n * values, a `location` header, and the framing headers the server sets itself. Route statuses\n * (for example a fixture's bodiless 204 or its 202 with a `Location`) follow the fixtures instead.\n *\n * Runtime-portable (no Node builtins): the emulators are plain Web fetch handlers.\n */\nimport { Predicate } from 'effect'\nimport * as Schema from 'effect/Schema'\n\n/** Why a status cannot answer an emulated request, or `undefined` when it can. */\nconst emulatorStatusProblem = (status: number): string | undefined =>\n status >= 300 && status <= 399\n ? 'emulators never redirect: 3xx statuses are not allowed'\n : status === 204 || status === 205\n ? 'a 204 or 205 response cannot carry a body'\n : undefined\n\n/** Statuses a fault or scripted error may answer with: 200-599 without 204, 205, and 3xx. */\nexport const EmulatorResponseStatus = Schema.Int.check(\n Schema.isBetween({ minimum: 200, maximum: 599 }),\n Schema.makeFilter(emulatorStatusProblem)\n)\n\n// RFC 9110 token characters for header names.\nconst headerNamePattern = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/\n\n// Visible ASCII, space, tab, and obs-text: what both web `Headers` and Node's HTTP server accept.\nconst headerValuePattern = /^[\\t\\x20-\\x7e\\x80-\\xff]*$/\n\nconst framingHeaders: ReadonlySet<string> = new Set([\n 'content-length',\n 'transfer-encoding',\n 'connection',\n 'keep-alive',\n 'upgrade'\n])\n\n/** Why a header record cannot be sent by an emulator, or `undefined` when it can. */\nconst emulatorHeaderRecordProblem = (\n headers: Readonly<Record<string, string>>\n): string | undefined => {\n for (const [name, value] of Object.entries(headers)) {\n if (!headerNamePattern.test(name)) {\n return `header name ${JSON.stringify(name)} is not a valid HTTP token`\n }\n\n if (name.toLowerCase() === 'location') {\n return 'emulators never redirect: a location header is not allowed'\n }\n\n // The server frames the body itself; a hand-set framing header would corrupt the response.\n if (framingHeaders.has(name.toLowerCase())) {\n return `header ${name} is set by the server and cannot be scripted`\n }\n\n if (!headerValuePattern.test(value)) {\n return `header ${name} has a value with control or non-Latin-1 characters`\n }\n }\n\n return undefined\n}\n\n/** Response headers for a fault or scripted error: valid names and values, and no `location`. */\nexport const EmulatorHeaderRecord = Schema.Record(Schema.String, Schema.String).check(\n Schema.makeFilter(emulatorHeaderRecordProblem)\n)\n\n// Header names that carry credentials or session state: the same rule as the conformance wire\n// helpers (`isCredentialHeaderName` in `@yolk-sdk/conformance`), copied because emulator source\n// never imports SDK packages.\nconst credentialHeaderNames: ReadonlySet<string> = new Set([\n 'authorization',\n 'proxy-authorization',\n 'cookie',\n 'set-cookie',\n 'x-api-key',\n 'api-key',\n 'x-goog-api-key',\n 'x-auth-token',\n 'x-access-token',\n 'x-amz-security-token',\n 'x-vercel-oidc-token',\n 'x-csrf-token'\n])\n\nconst credentialHeaderPattern = /(api[-_]?key|secret|password|cookie|authorization)/i\n\nconst credentialHeaderSegmentPattern = /(^|[-_])(token|key|auth)([-_]|$)/i\n\n/** True for header names that carry credentials or session state. */\nexport const isCredentialHeaderName = (name: string): boolean => {\n const lower = name.toLowerCase()\n\n return (\n credentialHeaderNames.has(lower) ||\n credentialHeaderPattern.test(lower) ||\n credentialHeaderSegmentPattern.test(lower)\n )\n}\n\n/** What a redacted credential value becomes in a ledger. */\nexport const redactedCredentialValue = '<redacted>'\n\nconst isJsonRecord = (value: Schema.Json): value is Schema.JsonObject =>\n value !== null && Predicate.isObject(value) && !Array.isArray(value)\n\n/**\n * A JSON value safe to keep in a ledger: every object key that `isCredentialHeaderName` accepts\n * (for example `requests[].headers.Authorization` in a Graph `$batch` body), at any depth, has\n * its value replaced by `<redacted>`. Other values are copied unchanged.\n */\nexport const redactCredentialFields = (value: Schema.Json): Schema.Json => {\n if (Array.isArray(value)) return value.map(redactCredentialFields)\n\n if (!isJsonRecord(value)) return value\n\n return Object.fromEntries(\n Object.entries(value).map(([key, entry]) => [\n key,\n isCredentialHeaderName(key) ? redactedCredentialValue : redactCredentialFields(entry)\n ])\n )\n}\n\n// Query parameter names that carry credentials: the same list as the conformance fixture scan\n// (`credentialParamPattern` in `@yolk-sdk/conformance`, reported as `credential_query_param`),\n// copied because emulator source never imports SDK packages. It adds the presigned-URL\n// `x-amz-signature` and `x-amz-credential`, which the header rule does not cover.\nconst credentialQueryParamPattern =\n /^(api[_-]?key|key|token|access[_-]?token|refresh[_-]?token|id[_-]?token|auth|secret|password|client[_-]?secret|x-amz-signature|x-amz-credential|x-amz-security-token)$/i\n\n/**\n * True for query parameter names that carry credentials: every name the conformance fixture scan\n * reports as a credential query parameter, plus every name `isCredentialHeaderName` accepts.\n */\nexport const isCredentialQueryKey = (name: string): boolean =>\n credentialQueryParamPattern.test(name) || isCredentialHeaderName(name)\n\n/**\n * Query parameters safe to keep in a ledger: the value of every key that `isCredentialQueryKey`\n * accepts (for example `access_token`, `api_key`, or `X-Amz-Signature`) is replaced by\n * `<redacted>`.\n */\nexport const redactCredentialQuery = (query: URLSearchParams): Readonly<Record<string, string>> =>\n Object.fromEntries(\n [...query].map(([key, value]) => [\n key,\n isCredentialQueryKey(key) ? redactedCredentialValue : value\n ])\n )\n\n/**\n * Internal response header a stateful core route sets on the bodiless 500 it answers when its\n * handler throws. The wrapper never forwards it: it answers its own error envelope 500 instead\n * and records `responseError` in the ledger.\n */\nexport const handlerFailedHeader = 'x-emulator-handler-failed'\n\n/** The core route's answer when its handler threw (see `handlerFailedHeader`). */\nexport const handlerFailedResponse = (): Response =>\n new Response(null, { status: 500, headers: { [handlerFailedHeader]: '1' } })\n\n/**\n * Internal request header a stateful wrapper sets on the requests it forwards to its core: the\n * id of the wrapper's job for that request (the fault decision the core route asks for, and what\n * the route decided). Ids come from a counter that never resets (the ledger sequence does), so a\n * ledger clear never makes two jobs share one. Clients cannot set it: wrappers build the core\n * request headers themselves.\n */\nexport const emulatorJobHeader = 'x-emulator-job-id'\n\n/**\n * What a core route answers when the wrapper returns the real answer itself (a fault, or in the\n * shared wrapper's resolved mode a refusal), so that answer never depends on the core runtime's\n * lifecycle: a reset or a close before it is read never cancels it.\n */\nexport const answeredOutsideCore = (): Response => new Response(null, { status: 204 })\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,MAAM,yBAAyB,WAC7B,UAAU,OAAO,UAAU,MACvB,2DACA,WAAW,OAAO,WAAW,MAC3B,8CACA,KAAA;;AAGR,MAAa,yBAAyB,OAAO,IAAI,MAC/C,OAAO,UAAU;CAAE,SAAS;CAAK,SAAS;AAAI,CAAC,GAC/C,OAAO,WAAW,qBAAqB,CACzC;AAGA,MAAM,oBAAoB;AAG1B,MAAM,qBAAqB;AAE3B,MAAM,iBAAsC,IAAI,IAAI;CAClD;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,MAAM,+BACJ,YACuB;CACvB,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAAG;EACnD,IAAI,CAAC,kBAAkB,KAAK,IAAI,GAC9B,OAAO,eAAe,KAAK,UAAU,IAAI,EAAE;EAG7C,IAAI,KAAK,YAAY,MAAM,YACzB,OAAO;EAIT,IAAI,eAAe,IAAI,KAAK,YAAY,CAAC,GACvC,OAAO,UAAU,KAAK;EAGxB,IAAI,CAAC,mBAAmB,KAAK,KAAK,GAChC,OAAO,UAAU,KAAK;CAE1B;AAGF;;AAGA,MAAa,uBAAuB,OAAO,OAAO,OAAO,QAAQ,OAAO,MAAM,EAAE,MAC9E,OAAO,WAAW,2BAA2B,CAC/C;AAKA,MAAM,wBAA6C,IAAI,IAAI;CACzD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAM,0BAA0B;AAEhC,MAAM,iCAAiC;;AAGvC,MAAa,0BAA0B,SAA0B;CAC/D,MAAM,QAAQ,KAAK,YAAY;CAE/B,OACE,sBAAsB,IAAI,KAAK,KAC/B,wBAAwB,KAAK,KAAK,KAClC,+BAA+B,KAAK,KAAK;AAE7C;;AAGA,MAAa,0BAA0B;AAEvC,MAAM,gBAAgB,UACpB,UAAU,QAAQ,UAAU,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK;;;;;;AAOrE,MAAa,0BAA0B,UAAoC;CACzE,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,IAAI,sBAAsB;CAEjE,IAAI,CAAC,aAAa,KAAK,GAAG,OAAO;CAEjC,OAAO,OAAO,YACZ,OAAO,QAAQ,KAAK,EAAE,KAAK,CAAC,KAAK,WAAW,CAC1C,KACA,uBAAuB,GAAG,IAAI,0BAA0B,uBAAuB,KAAK,CACtF,CAAC,CACH;AACF;AAMA,MAAM,8BACJ;;;;;AAMF,MAAa,wBAAwB,SACnC,4BAA4B,KAAK,IAAI,KAAK,uBAAuB,IAAI;;;;;;AAOvE,MAAa,yBAAyB,UACpC,OAAO,YACL,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,KAAK,WAAW,CAC/B,KACA,qBAAqB,GAAG,IAAI,0BAA0B,KACxD,CAAC,CACH;;;;;;AAOF,MAAa,sBAAsB;;AAGnC,MAAa,8BACX,IAAI,SAAS,MAAM;CAAE,QAAQ;CAAK,SAAS,GAAG,sBAAsB,IAAI;AAAE,CAAC;;;;;;;;AAS7E,MAAa,oBAAoB;;;;;;AAOjC,MAAa,4BAAsC,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC"}