@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,210 @@
1
+ import { emulatorEvidenceHeader } from "./route-evidence.mjs";
2
+ import { StatefulFault, StatefulFaultMatch, checkBooleanDrills, makeStatefulEmulator, routeEvidence } from "./stateful-emulator.mjs";
3
+ import { statefulCoreRuntime } from "./stateful-core.mjs";
4
+ import { googleEmulatorApisOrigin, googleEmulatorDrillKnobs, googleEmulatorGmailOrigin } from "./google/shared.mjs";
5
+ import { GoogleEmulatorAttachment, GoogleEmulatorCalendar, GoogleEmulatorCalendarEvent, GoogleEmulatorDraft, GoogleEmulatorDriveFile, GoogleEmulatorEventBoundary, GoogleEmulatorGmailLabel, GoogleEmulatorGmailMessage, GoogleEmulatorImpliedMessage, GoogleEmulatorPracticeAddress, GoogleEmulatorProfile, GoogleEmulatorSeed, GoogleEmulatorStateSchema, buildSeedState, decodeState } from "./google/state.mjs";
6
+ import { calendarRoutes } from "./google/calendar.mjs";
7
+ import { driveRoutes } from "./google/drive.mjs";
8
+ import { gmailRoutes } from "./google/gmail.mjs";
9
+ import { Data, Predicate } from "effect";
10
+ //#region src/google.ts
11
+ /**
12
+ * Stateful Google emulator (the Gmail, Calendar, and Drive routes the Google conformance cases
13
+ * send), built on the upstream `@emulators/core` custom runtime, with a request ledger, status
14
+ * faults, and an `/_emulate/*` control plane.
15
+ *
16
+ * It never imports SDK code: its wire shapes, error envelopes, and default seed are copied as data
17
+ * from the synthetic Google conformance fixtures, and every route names the conformance cases it
18
+ * follows in `googleEmulatorRoutes`. Only the routes those thirteen cases (with their cleanup)
19
+ * send are emulated: Gmail on `https://gmail.googleapis.com` (`/gmail/v1/users/me/...` and the
20
+ * multipart send upload), Calendar and Drive on `https://www.googleapis.com` (`/calendar/v3` and
21
+ * `/drive/v3`). Each route answers only on its recorded origin (`fetchOn(origin)` serves one
22
+ * origin behind a loopback rewrite). Response behaviour comes only from the fixtures: anything
23
+ * they do not show answers one ledgered 400 not-emulated, writes nothing, and uses up no fault.
24
+ * That includes an empty listing and every read of the leftover lookup
25
+ * (`findGoogleConformanceLeftovers`, whose label listing, draft search, and free-text event query
26
+ * no fixture records): the lookup fails, and runners print their lookup-failed WARN.
27
+ *
28
+ * The practice send (`google.gmail.send-practice-address`) is irreversible on Gmail. Here it is
29
+ * only recorded in the state (the sent message reads back; nothing is delivered anywhere), and only
30
+ * the recorded 7-bit message whose sole recipient header is `To: <the seeded practice address>`
31
+ * is accepted, and only while that address is the recorded `practice@example.test` (the recorded
32
+ * `sizeEstimate` covers it, as it covers the draft and send subjects, which therefore take only a
33
+ * run id of the fixtures' 13-character length).
34
+ *
35
+ * Fail closed: Google follows the shared fail-closed rule of `src/stateful-emulator.ts` (its opt-in
36
+ * `failClosed` mode, the same rule as `/github`). Every route parameter has a raw pattern matched
37
+ * in full, so a request is recognised only when its raw path is exactly an emulated route shape
38
+ * under that route's method and any `Authorization` header is exactly `Bearer <token>` with a
39
+ * recognisable bearer (below); every other request is ledgered and answered with constant text only
40
+ * (`/<unrecognised>`, a standard method or `<other>`, an empty query, no body, a constant reason).
41
+ * The bearer value is never compared against anything, stored, forwarded, or ledgered. A recognised
42
+ * request that repeats it in its raw path, any path segment, the query or any query key or value,
43
+ * the recorded `content-type` header, or its raw body (the multipart send body included), or, on
44
+ * the draft compose and update routes, in the base64url-decoded MIME of `message.raw` (Gmail's own
45
+ * wire format wraps the draft this way; the routes give it to the wrapper as a decoded view), is
46
+ * ledgered as the constant credential-repeat entry, checked through the wrapper's fixpoint closure
47
+ * of a tolerant percent-decode and a tolerant JSON-unescape; any other recognised request has the
48
+ * bearer scrubbed from its ledgered fields and every not-emulated reason. A `message.raw` the route
49
+ * would refuse (anything but canonical unpadded base64url of exactly the recorded draft MIME, the
50
+ * run id aside) makes the view throw a `DecodedViewRefusal` with one of the route's declared
51
+ * constant reasons (`viewRefusalReasons`: the canonical-base64url reason, the
52
+ * other-than-the-recorded-run-draft reason, the 13-character run-id reason, the extra-key reason),
53
+ * which the wrapper ledgers in the constant entry before anything is recorded (or
54
+ * `the request body repeats the credential` when the raw decodes cleanly to text holding the
55
+ * bearer), so a refused `message.raw` never reaches the ledger. Google refusals use the wrapper's
56
+ * constant-reason `exactQuery` and `exactBodyKeys`, so they never echo a request's own query or
57
+ * body keys.
58
+ *
59
+ * Request-shape latitude (`/google`, the only accepted deviations): any bearer value in the RFC
60
+ * 6750 `b64token` syntax (`[A-Za-z0-9\-._~+/]+=*`) of at least 8 characters, starting with a
61
+ * character in `[G-Zg-z\-._~+/]` other than `n`, `r`, `t`, `u`, with at least one outside
62
+ * `[0-9.eE+-]` (Google's `ya29.…` access tokens qualify), that occurs nowhere else in the request
63
+ * (never compared against anything, stored, or ledgered); extra request headers (except
64
+ * `X-Goog-Drive-Resource-Keys`, which no fixture sends); JSON key order; `content-type` media-type
65
+ * parameters on JSON requests; query parameters in any order; any `run-` run id (at most 40
66
+ * characters) in a run-scoped label name, event summary, or folder name; in a draft subject
67
+ * (compose and update) and the sent subject, only a run id of exactly 13 characters, the length of
68
+ * the fixtures' `run-synthetic`, because the recorded `sizeEstimate` of the draft and sent messages
69
+ * (answered by their message reads and the draft thread) covers the subject; on the practice send,
70
+ * a `content-type` of exactly `multipart/related; boundary=<b>` with any one unquoted boundary of 1
71
+ * to 70 `[A-Za-z0-9_]` characters and no other parameter; a `gmail.list` `maxResults` from 1 to
72
+ * 500, a `calendar.list_events` `maxResults` from 1 to 2500, and a `drive.list_files` `pageSize`
73
+ * from 1 to 1000; any `timeMin` before `timeMax` (RFC 3339 instants with a real calendar date, hour
74
+ * 0 to 23, minute and second 0 to 59, and a `Z` or in-range numeric offset); any id of an item the
75
+ * state holds where a fixture has an id (writes: only items created here, plus label changes,
76
+ * trash, and untrash of a stored non-draft message); and, for an id the state does not hold, only
77
+ * the recorded not-found answers (a `format=minimal` read of a 16-hex-digit message id, a read of a
78
+ * `Label_<1 to 999999999>` label, a delete of an `r-<digits>` draft, and a Drive file read). A seed
79
+ * may set another `practiceAddress`, but then every send is refused, since the recorded
80
+ * `sizeEstimate` of the sent message also covers the address: the send answers only while the
81
+ * seeded address is the recorded `practice@example.test`. On the draft compose and update routes,
82
+ * `message.raw` must be canonical unpadded base64url of exactly the recorded draft MIME of that
83
+ * route, the run id aside; any other `message.raw` (line-wrapped, the standard alphabet, padded,
84
+ * with a stray character, or with MIME-level encodings such as RFC 2047 encoded-words,
85
+ * quoted-printable, or UTF-16) is refused before anything is recorded or a fault is decided, as a
86
+ * constant entry with the route's own declared reason
87
+ * (`message.raw must be canonical base64url UTF-8 MIME`,
88
+ * `a draft compose other than the recorded run draft is not emulated` or its `update` form, the
89
+ * 13-character run-id reason, or `message has a key this route does not take`), or
90
+ * `the request body repeats the credential` when that raw decodes cleanly to text holding the
91
+ * bearer, so a refused `message.raw` never reaches the ledger and an admitted one is the recorded
92
+ * text. `Authorization` must be exactly `Bearer <token>` (that spelling, one space). Everything
93
+ * else (other keys, values, formats, query parameters, empty query components such as a bare `?` or
94
+ * a stray `&`, recorded headers such as Drive's `accept: application/json` missing, another origin,
95
+ * repeated query parameters, any recipient but the seeded practice address, a draft or send run id
96
+ * of another length, a bearer repeated anywhere in the request, including base64url-encoded inside
97
+ * a draft's `message.raw`, and page tokens not issued for the same list since the last reset or
98
+ * seed, or whose list changed) is not emulated.
99
+ *
100
+ * Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by
101
+ * `makeGoogleEmulator` (importing this module has no side effects).
102
+ *
103
+ * @experimental
104
+ */
105
+ const googleApiRoutes = [
106
+ ...gmailRoutes,
107
+ ...calendarRoutes,
108
+ ...driveRoutes
109
+ ];
110
+ /**
111
+ * Route evidence manifest: every emulated route, whether it writes, and the conformance cases
112
+ * whose (currently synthetic, unverified) wire claims it follows. Kept in sync with the handlers
113
+ * by construction (both come from one route table). Every route cites at least one case.
114
+ */
115
+ const googleEmulatorRoutes = googleApiRoutes.map(routeEvidence);
116
+ /**
117
+ * Optional fault filter; an omitted field matches every request. `method` is the HTTP method;
118
+ * `path` is the raw request path (ending in `*`, a prefix); `route` is a route template of the
119
+ * manifest (another value is rejected when the fault is added).
120
+ */
121
+ const GoogleFaultMatch = StatefulFaultMatch;
122
+ /**
123
+ * A status fault (400-599): answer matching requests with this status, headers, and body instead
124
+ * of the route's write, so nothing is written. The body defaults to
125
+ * `{ error: { type: 'emulator_fault', message } }`; `count` limits how many requests it answers.
126
+ * Only a request the emulator would answer reaches a fault: a request that is not emulated, by
127
+ * its shape or by the state, never uses one up.
128
+ */
129
+ const GoogleFault = StatefulFault;
130
+ /** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */
131
+ var GoogleEmulatorInputInvalid = class extends Data.TaggedError("GoogleEmulatorInputInvalid") {
132
+ get message() {
133
+ return `Invalid Google emulator ${this.input}: ${this.reason}`;
134
+ }
135
+ };
136
+ /** The constant reason of a request on no emulated route shape (nothing of it is ledgered). */
137
+ const googleUnrecognisedReason = "no emulated Google route for this method and path";
138
+ /** The constant reason of a request whose `Authorization` header is not one recognisable bearer. */
139
+ const googleUnrecognisedAuthorizationReason = "an unrecognisable Authorization header is not emulated";
140
+ const inputInvalid = (input, reason) => new GoogleEmulatorInputInvalid({
141
+ input,
142
+ reason
143
+ });
144
+ /**
145
+ * Create a stateful Google emulator on the `@emulators/core` custom runtime. Each call has its
146
+ * own state, ledger, faults, and page tokens. Rejects with `GoogleEmulatorInputInvalid` for an
147
+ * invalid seed or option. See `src/stateful-emulator.ts` for the request precedence.
148
+ */
149
+ const makeGoogleEmulator = async (options = {}) => {
150
+ const initial = buildSeedState(options.seed ?? {});
151
+ if (Predicate.isString(initial)) throw inputInvalid("seed", initial);
152
+ checkBooleanDrills(options.drills, googleEmulatorDrillKnobs, inputInvalid);
153
+ const drills = options.drills ?? {};
154
+ const env = {
155
+ now: options.now ?? (() => Date.now()),
156
+ drills: {
157
+ gmailPageRepeats: drills.gmailPageRepeats === true,
158
+ attachmentStandardBase64: drills.attachmentStandardBase64 === true,
159
+ notFoundWithoutMessage: drills.notFoundWithoutMessage === true,
160
+ labelDeleteKeepsOnMessages: drills.labelDeleteKeepsOnMessages === true,
161
+ draftUpdateKeepsContent: drills.draftUpdateKeepsContent === true,
162
+ trashAnswerOmitsTrash: drills.trashAnswerOmitsTrash === true,
163
+ sentMessageWithoutTo: drills.sentMessageWithoutTo === true,
164
+ calendarPageRepeats: drills.calendarPageRepeats === true,
165
+ eventPatchKeepsSummary: drills.eventPatchKeepsSummary === true,
166
+ repeatedEventDeleteConflict: drills.repeatedEventDeleteConflict === true,
167
+ drivePageRepeats: drills.drivePageRepeats === true,
168
+ getFileWithoutParents: drills.getFileWithoutParents === true,
169
+ listIncludesTrashed: drills.listIncludesTrashed === true
170
+ },
171
+ pageTokens: /* @__PURE__ */ new Map(),
172
+ generation: { current: 0 },
173
+ firstIssued: /* @__PURE__ */ new Map()
174
+ };
175
+ return makeStatefulEmulator({
176
+ routes: googleApiRoutes,
177
+ env,
178
+ initial,
179
+ buildSeed: buildSeedState,
180
+ requestProblem: (header) => header("x-goog-drive-resource-keys") === void 0 ? void 0 : "X-Goog-Drive-Resource-Keys is not emulated (no fixture sends it)",
181
+ recordHeaders: [{
182
+ name: "content-type",
183
+ json: false
184
+ }],
185
+ clearRuntime: () => {
186
+ env.pageTokens.clear();
187
+ env.generation.current += 1;
188
+ },
189
+ runtimeState: () => ({ pageTokens: env.pageTokens.size }),
190
+ seedSummary: (state) => ({
191
+ messages: state.messages.length,
192
+ events: state.events.length,
193
+ files: state.files.length
194
+ }),
195
+ inputInvalid,
196
+ failClosed: {
197
+ unrecognised: googleUnrecognisedReason,
198
+ unrecognisedAuthorization: googleUnrecognisedAuthorizationReason
199
+ }
200
+ }, statefulCoreRuntime({
201
+ name: "google",
202
+ initial,
203
+ decodeState,
204
+ inputInvalid
205
+ }));
206
+ };
207
+ //#endregion
208
+ export { GoogleEmulatorAttachment, GoogleEmulatorCalendar, GoogleEmulatorCalendarEvent, GoogleEmulatorDraft, GoogleEmulatorDriveFile, GoogleEmulatorEventBoundary, GoogleEmulatorGmailLabel, GoogleEmulatorGmailMessage, GoogleEmulatorImpliedMessage, GoogleEmulatorInputInvalid, GoogleEmulatorPracticeAddress, GoogleEmulatorProfile, GoogleEmulatorSeed, GoogleEmulatorStateSchema, GoogleFault, GoogleFaultMatch, emulatorEvidenceHeader, googleEmulatorApisOrigin, googleEmulatorGmailOrigin, googleEmulatorRoutes, makeGoogleEmulator };
209
+
210
+ //# sourceMappingURL=google.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.mjs","names":[],"sources":["../src/google.ts"],"sourcesContent":["/**\n * Stateful Google emulator (the Gmail, Calendar, and Drive routes the Google conformance cases\n * send), built on the upstream `@emulators/core` custom runtime, with a request ledger, status\n * faults, and an `/_emulate/*` control plane.\n *\n * It never imports SDK code: its wire shapes, error envelopes, and default seed are copied as data\n * from the synthetic Google conformance fixtures, and every route names the conformance cases it\n * follows in `googleEmulatorRoutes`. Only the routes those thirteen cases (with their cleanup)\n * send are emulated: Gmail on `https://gmail.googleapis.com` (`/gmail/v1/users/me/...` and the\n * multipart send upload), Calendar and Drive on `https://www.googleapis.com` (`/calendar/v3` and\n * `/drive/v3`). Each route answers only on its recorded origin (`fetchOn(origin)` serves one\n * origin behind a loopback rewrite). Response behaviour comes only from the fixtures: anything\n * they do not show answers one ledgered 400 not-emulated, writes nothing, and uses up no fault.\n * That includes an empty listing and every read of the leftover lookup\n * (`findGoogleConformanceLeftovers`, whose label listing, draft search, and free-text event query\n * no fixture records): the lookup fails, and runners print their lookup-failed WARN.\n *\n * The practice send (`google.gmail.send-practice-address`) is irreversible on Gmail. Here it is\n * only recorded in the state (the sent message reads back; nothing is delivered anywhere), and only\n * the recorded 7-bit message whose sole recipient header is `To: <the seeded practice address>`\n * is accepted, and only while that address is the recorded `practice@example.test` (the recorded\n * `sizeEstimate` covers it, as it covers the draft and send subjects, which therefore take only a\n * run id of the fixtures' 13-character length).\n *\n * Fail closed: Google follows the shared fail-closed rule of `src/stateful-emulator.ts` (its opt-in\n * `failClosed` mode, the same rule as `/github`). Every route parameter has a raw pattern matched\n * in full, so a request is recognised only when its raw path is exactly an emulated route shape\n * under that route's method and any `Authorization` header is exactly `Bearer <token>` with a\n * recognisable bearer (below); every other request is ledgered and answered with constant text only\n * (`/<unrecognised>`, a standard method or `<other>`, an empty query, no body, a constant reason).\n * The bearer value is never compared against anything, stored, forwarded, or ledgered. A recognised\n * request that repeats it in its raw path, any path segment, the query or any query key or value,\n * the recorded `content-type` header, or its raw body (the multipart send body included), or, on\n * the draft compose and update routes, in the base64url-decoded MIME of `message.raw` (Gmail's own\n * wire format wraps the draft this way; the routes give it to the wrapper as a decoded view), is\n * ledgered as the constant credential-repeat entry, checked through the wrapper's fixpoint closure\n * of a tolerant percent-decode and a tolerant JSON-unescape; any other recognised request has the\n * bearer scrubbed from its ledgered fields and every not-emulated reason. A `message.raw` the route\n * would refuse (anything but canonical unpadded base64url of exactly the recorded draft MIME, the\n * run id aside) makes the view throw a `DecodedViewRefusal` with one of the route's declared\n * constant reasons (`viewRefusalReasons`: the canonical-base64url reason, the\n * other-than-the-recorded-run-draft reason, the 13-character run-id reason, the extra-key reason),\n * which the wrapper ledgers in the constant entry before anything is recorded (or\n * `the request body repeats the credential` when the raw decodes cleanly to text holding the\n * bearer), so a refused `message.raw` never reaches the ledger. Google refusals use the wrapper's\n * constant-reason `exactQuery` and `exactBodyKeys`, so they never echo a request's own query or\n * body keys.\n *\n * Request-shape latitude (`/google`, the only accepted deviations): any bearer value in the RFC\n * 6750 `b64token` syntax (`[A-Za-z0-9\\-._~+/]+=*`) of at least 8 characters, starting with a\n * character in `[G-Zg-z\\-._~+/]` other than `n`, `r`, `t`, `u`, with at least one outside\n * `[0-9.eE+-]` (Google's `ya29.…` access tokens qualify), that occurs nowhere else in the request\n * (never compared against anything, stored, or ledgered); extra request headers (except\n * `X-Goog-Drive-Resource-Keys`, which no fixture sends); JSON key order; `content-type` media-type\n * parameters on JSON requests; query parameters in any order; any `run-` run id (at most 40\n * characters) in a run-scoped label name, event summary, or folder name; in a draft subject\n * (compose and update) and the sent subject, only a run id of exactly 13 characters, the length of\n * the fixtures' `run-synthetic`, because the recorded `sizeEstimate` of the draft and sent messages\n * (answered by their message reads and the draft thread) covers the subject; on the practice send,\n * a `content-type` of exactly `multipart/related; boundary=<b>` with any one unquoted boundary of 1\n * to 70 `[A-Za-z0-9_]` characters and no other parameter; a `gmail.list` `maxResults` from 1 to\n * 500, a `calendar.list_events` `maxResults` from 1 to 2500, and a `drive.list_files` `pageSize`\n * from 1 to 1000; any `timeMin` before `timeMax` (RFC 3339 instants with a real calendar date, hour\n * 0 to 23, minute and second 0 to 59, and a `Z` or in-range numeric offset); any id of an item the\n * state holds where a fixture has an id (writes: only items created here, plus label changes,\n * trash, and untrash of a stored non-draft message); and, for an id the state does not hold, only\n * the recorded not-found answers (a `format=minimal` read of a 16-hex-digit message id, a read of a\n * `Label_<1 to 999999999>` label, a delete of an `r-<digits>` draft, and a Drive file read). A seed\n * may set another `practiceAddress`, but then every send is refused, since the recorded\n * `sizeEstimate` of the sent message also covers the address: the send answers only while the\n * seeded address is the recorded `practice@example.test`. On the draft compose and update routes,\n * `message.raw` must be canonical unpadded base64url of exactly the recorded draft MIME of that\n * route, the run id aside; any other `message.raw` (line-wrapped, the standard alphabet, padded,\n * with a stray character, or with MIME-level encodings such as RFC 2047 encoded-words,\n * quoted-printable, or UTF-16) is refused before anything is recorded or a fault is decided, as a\n * constant entry with the route's own declared reason\n * (`message.raw must be canonical base64url UTF-8 MIME`,\n * `a draft compose other than the recorded run draft is not emulated` or its `update` form, the\n * 13-character run-id reason, or `message has a key this route does not take`), or\n * `the request body repeats the credential` when that raw decodes cleanly to text holding the\n * bearer, so a refused `message.raw` never reaches the ledger and an admitted one is the recorded\n * text. `Authorization` must be exactly `Bearer <token>` (that spelling, one space). Everything\n * else (other keys, values, formats, query parameters, empty query components such as a bare `?` or\n * a stray `&`, recorded headers such as Drive's `accept: application/json` missing, another origin,\n * repeated query parameters, any recipient but the seeded practice address, a draft or send run id\n * of another length, a bearer repeated anywhere in the request, including base64url-encoded inside\n * a draft's `message.raw`, and page tokens not issued for the same list since the last reset or\n * seed, or whose list changed) is not emulated.\n *\n * Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by\n * `makeGoogleEmulator` (importing this module has no side effects).\n *\n * @experimental\n */\nimport { Data, Predicate } from 'effect'\nimport { calendarRoutes } from './google/calendar.ts'\nimport { driveRoutes } from './google/drive.ts'\nimport { gmailRoutes } from './google/gmail.ts'\nimport {\n googleEmulatorDrillKnobs,\n type GoogleApiEnv,\n type GoogleEmulatorDrills,\n type GoogleRoute\n} from './google/shared.ts'\nimport {\n buildSeedState,\n decodeState,\n type GoogleEmulatorSeed,\n type GoogleEmulatorState\n} from './google/state.ts'\nimport type { EmulatorRouteEvidence } from './route-evidence.ts'\nimport {\n StatefulFault,\n StatefulFaultMatch,\n checkBooleanDrills,\n makeStatefulEmulator,\n routeEvidence,\n type StatefulCoverage,\n type StatefulEmulatorApi,\n type StatefulFaultState,\n type StatefulInputKind,\n type StatefulLedgerEntry\n} from './stateful-emulator.ts'\nimport { statefulCoreRuntime } from './stateful-core.ts'\n\nexport type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'\n\nexport { emulatorEvidenceHeader } from './route-evidence.ts'\n\nexport {\n googleEmulatorApisOrigin,\n googleEmulatorGmailOrigin,\n type GoogleEmulatorDrills\n} from './google/shared.ts'\n\nexport {\n GoogleEmulatorAttachment,\n GoogleEmulatorCalendar,\n GoogleEmulatorCalendarEvent,\n GoogleEmulatorDraft,\n GoogleEmulatorDriveFile,\n GoogleEmulatorEventBoundary,\n GoogleEmulatorGmailLabel,\n GoogleEmulatorGmailMessage,\n GoogleEmulatorImpliedMessage,\n GoogleEmulatorPracticeAddress,\n GoogleEmulatorProfile,\n GoogleEmulatorSeed,\n GoogleEmulatorStateSchema,\n type GoogleEmulatorState\n} from './google/state.ts'\n\nconst googleApiRoutes: ReadonlyArray<GoogleRoute> = [\n ...gmailRoutes,\n ...calendarRoutes,\n ...driveRoutes\n]\n\n/**\n * Route evidence manifest: every emulated route, whether it writes, and the conformance cases\n * whose (currently synthetic, unverified) wire claims it follows. Kept in sync with the handlers\n * by construction (both come from one route table). Every route cites at least one case.\n */\nexport const googleEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> =\n googleApiRoutes.map(routeEvidence)\n\n/**\n * Optional fault filter; an omitted field matches every request. `method` is the HTTP method;\n * `path` is the raw request path (ending in `*`, a prefix); `route` is a route template of the\n * manifest (another value is rejected when the fault is added).\n */\nexport const GoogleFaultMatch = StatefulFaultMatch\n\nexport type GoogleFaultMatch = typeof StatefulFaultMatch.Type\n\n/**\n * A status fault (400-599): answer matching requests with this status, headers, and body instead\n * of the route's write, so nothing is written. The body defaults to\n * `{ error: { type: 'emulator_fault', message } }`; `count` limits how many requests it answers.\n * Only a request the emulator would answer reaches a fault: a request that is not emulated, by\n * its shape or by the state, never uses one up.\n */\nexport const GoogleFault = StatefulFault\n\nexport type GoogleFault = StatefulFault\n\nexport type GoogleFaultState = StatefulFaultState\n\nexport type GoogleLedgerEntry = StatefulLedgerEntry\n\nexport type GoogleCoverage = StatefulCoverage\n\n/** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */\nexport class GoogleEmulatorInputInvalid extends Data.TaggedError('GoogleEmulatorInputInvalid')<{\n readonly input: StatefulInputKind\n readonly reason: string\n}> {\n override get message(): string {\n return `Invalid Google emulator ${this.input}: ${this.reason}`\n }\n}\n\nexport type GoogleEmulatorOptions = {\n /** Typed seed; defaults to the fixture entities (`profile: 'default'`). */\n readonly seed?: GoogleEmulatorSeed\n /**\n * Clock in epoch milliseconds (created and updated event times, Drive folder times, and the\n * sent message's `Date` header). Defaults to `Date.now`.\n */\n readonly now?: () => number\n /** Drill knobs (tests only): make the emulator disagree with one conformance case. */\n readonly drills?: GoogleEmulatorDrills\n}\n\nexport type GoogleEmulator = StatefulEmulatorApi<GoogleEmulatorState, GoogleEmulatorSeed>\n\n/** The constant reason of a request on no emulated route shape (nothing of it is ledgered). */\nconst googleUnrecognisedReason = 'no emulated Google route for this method and path'\n\n/** The constant reason of a request whose `Authorization` header is not one recognisable bearer. */\nconst googleUnrecognisedAuthorizationReason =\n 'an unrecognisable Authorization header is not emulated'\n\nconst inputInvalid = (input: StatefulInputKind, reason: string) =>\n new GoogleEmulatorInputInvalid({ input, reason })\n\n/**\n * Create a stateful Google emulator on the `@emulators/core` custom runtime. Each call has its\n * own state, ledger, faults, and page tokens. Rejects with `GoogleEmulatorInputInvalid` for an\n * invalid seed or option. See `src/stateful-emulator.ts` for the request precedence.\n */\nexport const makeGoogleEmulator = async (\n options: GoogleEmulatorOptions = {}\n): Promise<GoogleEmulator> => {\n const initial = buildSeedState(options.seed ?? {})\n\n if (Predicate.isString(initial)) {\n throw inputInvalid('seed', initial)\n }\n\n checkBooleanDrills(options.drills, googleEmulatorDrillKnobs, inputInvalid)\n\n const drills = options.drills ?? {}\n\n const env: GoogleApiEnv = {\n now: options.now ?? (() => Date.now()),\n drills: {\n gmailPageRepeats: drills.gmailPageRepeats === true,\n attachmentStandardBase64: drills.attachmentStandardBase64 === true,\n notFoundWithoutMessage: drills.notFoundWithoutMessage === true,\n labelDeleteKeepsOnMessages: drills.labelDeleteKeepsOnMessages === true,\n draftUpdateKeepsContent: drills.draftUpdateKeepsContent === true,\n trashAnswerOmitsTrash: drills.trashAnswerOmitsTrash === true,\n sentMessageWithoutTo: drills.sentMessageWithoutTo === true,\n calendarPageRepeats: drills.calendarPageRepeats === true,\n eventPatchKeepsSummary: drills.eventPatchKeepsSummary === true,\n repeatedEventDeleteConflict: drills.repeatedEventDeleteConflict === true,\n drivePageRepeats: drills.drivePageRepeats === true,\n getFileWithoutParents: drills.getFileWithoutParents === true,\n listIncludesTrashed: drills.listIncludesTrashed === true\n },\n pageTokens: new Map(),\n generation: { current: 0 },\n firstIssued: new Map()\n }\n\n return makeStatefulEmulator<GoogleEmulatorState, GoogleApiEnv, GoogleEmulatorSeed>(\n {\n routes: googleApiRoutes,\n env,\n initial,\n buildSeed: buildSeedState,\n requestProblem: header =>\n header('x-goog-drive-resource-keys') === undefined\n ? undefined\n : 'X-Goog-Drive-Resource-Keys is not emulated (no fixture sends it)',\n // `content-type` only (never a credential header); the bearer is checked in it too.\n recordHeaders: [{ name: 'content-type', json: false }],\n // Page token values never cross a reset or seed: the generation makes later tokens distinct.\n clearRuntime: () => {\n env.pageTokens.clear()\n env.generation.current += 1\n },\n runtimeState: () => ({ pageTokens: env.pageTokens.size }),\n seedSummary: state => ({\n messages: state.messages.length,\n events: state.events.length,\n files: state.files.length\n }),\n inputInvalid,\n failClosed: {\n unrecognised: googleUnrecognisedReason,\n unrecognisedAuthorization: googleUnrecognisedAuthorizationReason\n }\n },\n statefulCoreRuntime({ name: 'google', initial, decodeState, inputInvalid })\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwJA,MAAM,kBAA8C;CAClD,GAAG;CACH,GAAG;CACH,GAAG;AACL;;;;;;AAOA,MAAa,uBACX,gBAAgB,IAAI,aAAa;;;;;;AAOnC,MAAa,mBAAmB;;;;;;;;AAWhC,MAAa,cAAc;;AAW3B,IAAa,6BAAb,cAAgD,KAAK,YAAY,4BAA4B,EAG1F;CACD,IAAa,UAAkB;EAC7B,OAAO,2BAA2B,KAAK,MAAM,IAAI,KAAK;CACxD;AACF;;AAiBA,MAAM,2BAA2B;;AAGjC,MAAM,wCACJ;AAEF,MAAM,gBAAgB,OAA0B,WAC9C,IAAI,2BAA2B;CAAE;CAAO;AAAO,CAAC;;;;;;AAOlD,MAAa,qBAAqB,OAChC,UAAiC,CAAC,MACN;CAC5B,MAAM,UAAU,eAAe,QAAQ,QAAQ,CAAC,CAAC;CAEjD,IAAI,UAAU,SAAS,OAAO,GAC5B,MAAM,aAAa,QAAQ,OAAO;CAGpC,mBAAmB,QAAQ,QAAQ,0BAA0B,YAAY;CAEzE,MAAM,SAAS,QAAQ,UAAU,CAAC;CAElC,MAAM,MAAoB;EACxB,KAAK,QAAQ,cAAc,KAAK,IAAI;EACpC,QAAQ;GACN,kBAAkB,OAAO,qBAAqB;GAC9C,0BAA0B,OAAO,6BAA6B;GAC9D,wBAAwB,OAAO,2BAA2B;GAC1D,4BAA4B,OAAO,+BAA+B;GAClE,yBAAyB,OAAO,4BAA4B;GAC5D,uBAAuB,OAAO,0BAA0B;GACxD,sBAAsB,OAAO,yBAAyB;GACtD,qBAAqB,OAAO,wBAAwB;GACpD,wBAAwB,OAAO,2BAA2B;GAC1D,6BAA6B,OAAO,gCAAgC;GACpE,kBAAkB,OAAO,qBAAqB;GAC9C,uBAAuB,OAAO,0BAA0B;GACxD,qBAAqB,OAAO,wBAAwB;EACtD;EACA,4BAAY,IAAI,IAAI;EACpB,YAAY,EAAE,SAAS,EAAE;EACzB,6BAAa,IAAI,IAAI;CACvB;CAEA,OAAO,qBACL;EACE,QAAQ;EACR;EACA;EACA,WAAW;EACX,iBAAgB,WACd,OAAO,4BAA4B,MAAM,KAAA,IACrC,KAAA,IACA;EAEN,eAAe,CAAC;GAAE,MAAM;GAAgB,MAAM;EAAM,CAAC;EAErD,oBAAoB;GAClB,IAAI,WAAW,MAAM;GACrB,IAAI,WAAW,WAAW;EAC5B;EACA,qBAAqB,EAAE,YAAY,IAAI,WAAW,KAAK;EACvD,cAAa,WAAU;GACrB,UAAU,MAAM,SAAS;GACzB,QAAQ,MAAM,OAAO;GACrB,OAAO,MAAM,MAAM;EACrB;EACA;EACA,YAAY;GACV,cAAc;GACd,2BAA2B;EAC7B;CACF,GACA,oBAAoB;EAAE,MAAM;EAAU;EAAS;EAAa;CAAa,CAAC,CAC5E;AACF"}
@@ -0,0 +1,45 @@
1
+ import { StatefulRoute } from "../stateful-emulator.mjs";
2
+ import { LinkedInSearchEmulatorState } from "./state.mjs";
3
+
4
+ //#region src/linkedin-search/api.d.ts
5
+ /** Drill knobs (tests only): each makes the emulator disagree with one conformance claim. */
6
+ type LinkedInSearchEmulatorDrills = {
7
+ /** A search with the default `numResults` (10) answers its results without `text`. */readonly defaultSearchWithoutText?: boolean; /** A search answers the query's seeded search with the most results, whatever `numResults`. */
8
+ readonly numResultsIgnored?: boolean; /** A profile lookup answers an empty JSON object. */
9
+ readonly profileAnswersEmptyObject?: boolean; /** An email lookup answers a JSON object without `email`. */
10
+ readonly emailAnswerOmitsEmail?: boolean; /** A rejected Exa key answers status 500 (with the recorded 401 body). */
11
+ readonly exaUnauthorizedAs5xx?: boolean; /** A rejected Enrich Layer key answers status 200 (with the recorded 401 body). */
12
+ readonly enrichLayerUnauthorizedAs2xx?: boolean; /** An absent profile answers status 200 (with the recorded 404 body). */
13
+ readonly absentProfileAs2xx?: boolean;
14
+ };
15
+ declare const linkedInSearchEmulatorDrillKnobs: ReadonlyArray<keyof LinkedInSearchEmulatorDrills>;
16
+ type LinkedInSearchApiEnv = {
17
+ readonly drills: Readonly<Record<keyof LinkedInSearchEmulatorDrills, boolean>>;
18
+ };
19
+ /**
20
+ * The fixtures' error bodies, byte for byte (`JSON.stringify` keeps this key order): Exa's 401,
21
+ * Enrich Layer's 401 (the profile and the email lookup answer the same body), and Enrich Layer's
22
+ * not-found 404.
23
+ */
24
+ declare const linkedInSearchEmulatorErrorBodies: {
25
+ readonly exaUnauthorized: {
26
+ readonly requestId: "synthetic-request-0003";
27
+ readonly error: "Invalid API key";
28
+ };
29
+ readonly enrichLayerUnauthorized: {
30
+ readonly code: 401;
31
+ readonly description: "Invalid API Key";
32
+ readonly name: "Unauthorized";
33
+ };
34
+ readonly profileNotFound: {
35
+ readonly code: 404;
36
+ readonly description: "Person profile does not exist";
37
+ readonly name: "Not Found";
38
+ };
39
+ };
40
+ type Route = StatefulRoute<LinkedInSearchEmulatorState, LinkedInSearchApiEnv>;
41
+ /** Every emulated route, in manifest order. */
42
+ declare const linkedInSearchApiRoutes: ReadonlyArray<Route>;
43
+ //#endregion
44
+ export { LinkedInSearchApiEnv, LinkedInSearchEmulatorDrills, linkedInSearchApiRoutes, linkedInSearchEmulatorDrillKnobs, linkedInSearchEmulatorErrorBodies };
45
+ //# sourceMappingURL=api.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/linkedin-search/api.ts"],"mappings":";;;;;KA2CY,4BAAA;EAAA,+FAED,wBAAA;WAEA,iBAAA,YAFA;EAAA,SAIA,yBAAA;EAAA,SAEA,qBAAA,YAEA;EAAA,SAAA,oBAAA,YAIA;EAAA,SAFA,4BAAA,YAEkB;EAAA,SAAlB,kBAAA;AAAA;AAAA,cAGE,gCAAA,EAAkC,aAAa,OAAO,4BAAA;AAAA,KAUvD,oBAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,MAAA,OAAa,4BAAA;AAAA;;;;;;cAQ5B,iCAAA;EAAA;;;;;;;;;;;;;;;KAMR,KAAA,GAAQ,aAAA,CAAc,2BAAA,EAA6B,oBAAA;;cAqR3C,uBAAA,EAAyB,aAAa,CAAC,KAAA"}
@@ -0,0 +1,181 @@
1
+ import { exactBodyKeys, exactQuery, integerIn, isNotEmulated, notEmulated, statefulRoute } from "../stateful-emulator.mjs";
2
+ import { linkedInSearchEmulatorEnrichLayerOrigin, linkedInSearchEmulatorExaOrigin, linkedInSearchProfileUrlPattern, linkedInSearchQueryPattern } from "./state.mjs";
3
+ import { Predicate } from "effect";
4
+ //#region src/linkedin-search/api.ts
5
+ /**
6
+ * LinkedIn search emulator API: the route table (evidence, request-shape checks, and state-reading
7
+ * plans), the fixture error bodies, and answer rendering (internal; re-exported by
8
+ * `src/linkedin-search.ts`).
9
+ *
10
+ * Only the routes the seven LinkedIn search conformance cases send are emulated, each on its
11
+ * recorded origin: the Exa people search (`POST /search` on `https://api.exa.ai`) and the Enrich
12
+ * Layer profile and email lookups (`GET /api/v2/profile` and `GET /api/v2/profile/email` on
13
+ * `https://enrichlayer.com`). Every route is a read: a plan reads the state and answers a fixture's
14
+ * response or refuses (400 not-emulated, no fault used up); no commit writes.
15
+ *
16
+ * @experimental
17
+ */
18
+ /** Content type of every answer, as the fixtures record it. */
19
+ const contentType = "application/json";
20
+ const linkedInSearchEmulatorDrillKnobs = [
21
+ "defaultSearchWithoutText",
22
+ "numResultsIgnored",
23
+ "profileAnswersEmptyObject",
24
+ "emailAnswerOmitsEmail",
25
+ "exaUnauthorizedAs5xx",
26
+ "enrichLayerUnauthorizedAs2xx",
27
+ "absentProfileAs2xx"
28
+ ];
29
+ /**
30
+ * The fixtures' error bodies, byte for byte (`JSON.stringify` keeps this key order): Exa's 401,
31
+ * Enrich Layer's 401 (the profile and the email lookup answer the same body), and Enrich Layer's
32
+ * not-found 404.
33
+ */
34
+ const linkedInSearchEmulatorErrorBodies = {
35
+ exaUnauthorized: {
36
+ requestId: "synthetic-request-0003",
37
+ error: "Invalid API key"
38
+ },
39
+ enrichLayerUnauthorized: {
40
+ code: 401,
41
+ description: "Invalid API Key",
42
+ name: "Unauthorized"
43
+ },
44
+ profileNotFound: {
45
+ code: 404,
46
+ description: "Person profile does not exist",
47
+ name: "Not Found"
48
+ }
49
+ };
50
+ const peopleResultsCase = "linkedin-search.search.people-results";
51
+ const numResultsLimitCase = "linkedin-search.search.num-results-limit";
52
+ const profileCase = "linkedin-search.profile.get-profile";
53
+ const emailCase = "linkedin-search.email.lookup-answer";
54
+ const exaUnauthorizedCase = "linkedin-search.errors.exa-unauthorized";
55
+ const enrichLayerUnauthorizedCase = "linkedin-search.errors.enrich-layer-unauthorized";
56
+ const profileNotFoundCase = "linkedin-search.errors.profile-not-found";
57
+ const evidence = (method, path, origin, caseIds) => ({
58
+ method,
59
+ path,
60
+ kind: "connector",
61
+ write: false,
62
+ caseIds,
63
+ evidence: "unverified",
64
+ origin,
65
+ params: {}
66
+ });
67
+ const json = (status, body) => new Response(JSON.stringify(body), {
68
+ status,
69
+ headers: { "content-type": contentType }
70
+ });
71
+ /** A read-only commit. */
72
+ const answer = (response) => () => response();
73
+ /** Whether the request's per-origin bearer digest is one a seed marks as rejected. */
74
+ const rejected = (digests, digest) => digest !== void 0 && digests.includes(digest);
75
+ /** The connector's default `numResults` (the people-results fixture's). */
76
+ const defaultNumResults = 10;
77
+ /** The result fields, in the fixtures' key order. */
78
+ const resultKeys = [
79
+ "title",
80
+ "url",
81
+ "author",
82
+ "publishedDate",
83
+ "text"
84
+ ];
85
+ const renderResult = (result) => {
86
+ const rendered = {};
87
+ for (const key of resultKeys) {
88
+ const value = result[key];
89
+ if (value !== void 0) rendered[key] = value;
90
+ }
91
+ return rendered;
92
+ };
93
+ /** The search the state answers for the input, or `undefined` (drills aside: the exact one). */
94
+ const searchFor = (state, input, env) => {
95
+ const exact = state.searches.find((search) => search.query === input.query && search.numResults === input.numResults);
96
+ if (exact === void 0 || !env.drills.numResultsIgnored) return exact;
97
+ return state.searches.filter((search) => search.query === input.query).reduce((widest, search) => search.results.length > widest.results.length ? search : widest);
98
+ };
99
+ const search = statefulRoute({ ...evidence("POST", "/search", linkedInSearchEmulatorExaOrigin, [
100
+ peopleResultsCase,
101
+ numResultsLimitCase,
102
+ exaUnauthorizedCase
103
+ ]) }, "json", (request) => {
104
+ const query = exactQuery(request, [], [], { rawNames: true });
105
+ if (isNotEmulated(query)) return query;
106
+ const body = exactBodyKeys(request.json, "the search body", [
107
+ "query",
108
+ "category",
109
+ "numResults",
110
+ "type",
111
+ "contents"
112
+ ]);
113
+ if (isNotEmulated(body)) return body;
114
+ const text = body.query;
115
+ if (!Predicate.isString(text) || !linkedInSearchQueryPattern.test(text) || text.length > 500) return notEmulated("query must be one trimmed, non-empty line of at most 500 characters");
116
+ if (body.category !== "people") return notEmulated("category other than people is not emulated");
117
+ if (body.type !== "auto") return notEmulated("type other than auto is not emulated");
118
+ const contents = exactBodyKeys(body.contents, "contents", ["text"]);
119
+ if (isNotEmulated(contents)) return contents;
120
+ if (contents.text !== true) return notEmulated("contents other than { text: true } is not emulated");
121
+ const numResults = integerIn(body.numResults, "numResults", 1, 100);
122
+ if (isNotEmulated(numResults)) return numResults;
123
+ return {
124
+ query: text,
125
+ numResults,
126
+ bearerDigest: request.bearerDigest
127
+ };
128
+ }, (state, input, { env }) => {
129
+ if (rejected(state.exaRejectedKeyDigests, input.bearerDigest)) return answer(() => json(env.drills.exaUnauthorizedAs5xx ? 500 : 401, linkedInSearchEmulatorErrorBodies.exaUnauthorized));
130
+ const found = searchFor(state, input, env);
131
+ if (found === void 0) return notEmulated("a search the state holds no answer for (this query and numResults) is not emulated");
132
+ return answer(() => json(200, { results: found.results.map((result) => {
133
+ const rendered = renderResult(result);
134
+ if (!env.drills.defaultSearchWithoutText || input.numResults !== defaultNumResults) return rendered;
135
+ const { text: _text, ...withoutText } = rendered;
136
+ return withoutText;
137
+ }) }));
138
+ });
139
+ /** The one `linkedin_profile_url` query parameter: a profile URL (decoded once). */
140
+ const lookupInput = (request) => {
141
+ const query = exactQuery(request, ["linkedin_profile_url"], [], { rawNames: true });
142
+ if (isNotEmulated(query)) return query;
143
+ const url = query.linkedin_profile_url ?? "";
144
+ if (!linkedInSearchProfileUrlPattern.test(url) || url.length > 2048) return notEmulated("linkedin_profile_url must be a profile URL https://<host>/in/<slug>");
145
+ return {
146
+ url,
147
+ bearerDigest: request.bearerDigest
148
+ };
149
+ };
150
+ const profileHeld = (state, url) => state.profiles.find((profile) => profile.url === url);
151
+ /** The Enrich Layer unauthorized fixture's 401 (both lookups answer it). */
152
+ const enrichLayerUnauthorized = (env) => answer(() => json(env.drills.enrichLayerUnauthorizedAs2xx ? 200 : 401, linkedInSearchEmulatorErrorBodies.enrichLayerUnauthorized));
153
+ /** Every emulated route, in manifest order. */
154
+ const linkedInSearchApiRoutes = [
155
+ search,
156
+ statefulRoute(evidence("GET", "/api/v2/profile", linkedInSearchEmulatorEnrichLayerOrigin, [
157
+ profileCase,
158
+ enrichLayerUnauthorizedCase,
159
+ profileNotFoundCase
160
+ ]), "none", lookupInput, (state, input, { env }) => {
161
+ if (rejected(state.enrichLayerRejectedKeyDigests, input.bearerDigest)) return enrichLayerUnauthorized(env);
162
+ const held = profileHeld(state, input.url);
163
+ if (held !== void 0) return answer(() => json(200, env.drills.profileAnswersEmptyObject ? {} : {
164
+ public_identifier: held.publicIdentifier,
165
+ full_name: held.fullName,
166
+ headline: held.headline
167
+ }));
168
+ if (state.absentProfileUrls.includes(input.url)) return answer(() => json(env.drills.absentProfileAs2xx ? 200 : 404, linkedInSearchEmulatorErrorBodies.profileNotFound));
169
+ return notEmulated("a profile URL the state holds neither as a profile nor as absent is not emulated");
170
+ }),
171
+ statefulRoute(evidence("GET", "/api/v2/profile/email", linkedInSearchEmulatorEnrichLayerOrigin, [emailCase, enrichLayerUnauthorizedCase]), "none", lookupInput, (state, input, { env }) => {
172
+ if (rejected(state.enrichLayerRejectedKeyDigests, input.bearerDigest)) return enrichLayerUnauthorized(env);
173
+ const address = profileHeld(state, input.url)?.email;
174
+ if (address === void 0) return notEmulated("an email lookup of a profile the state holds no email answer for is not emulated");
175
+ return answer(() => json(200, env.drills.emailAnswerOmitsEmail ? {} : { email: address }));
176
+ })
177
+ ];
178
+ //#endregion
179
+ export { linkedInSearchApiRoutes, linkedInSearchEmulatorDrillKnobs, linkedInSearchEmulatorErrorBodies };
180
+
181
+ //# sourceMappingURL=api.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.mjs","names":[],"sources":["../../src/linkedin-search/api.ts"],"sourcesContent":["/**\n * LinkedIn search emulator API: the route table (evidence, request-shape checks, and state-reading\n * plans), the fixture error bodies, and answer rendering (internal; re-exported by\n * `src/linkedin-search.ts`).\n *\n * Only the routes the seven LinkedIn search conformance cases send are emulated, each on its\n * recorded origin: the Exa people search (`POST /search` on `https://api.exa.ai`) and the Enrich\n * Layer profile and email lookups (`GET /api/v2/profile` and `GET /api/v2/profile/email` on\n * `https://enrichlayer.com`). Every route is a read: a plan reads the state and answers a fixture's\n * response or refuses (400 not-emulated, no fault used up); no commit writes.\n *\n * @experimental\n */\nimport { Predicate } from 'effect'\nimport type * as Schema from 'effect/Schema'\nimport {\n exactBodyKeys,\n exactQuery,\n integerIn,\n isNotEmulated,\n notEmulated,\n statefulRoute,\n type Commit,\n type EmulatedRequest,\n type NotEmulated,\n type StatefulRoute\n} from '../stateful-emulator.ts'\nimport {\n linkedInSearchEmulatorEnrichLayerOrigin,\n linkedInSearchEmulatorExaOrigin,\n linkedInSearchMaxNumResults,\n linkedInSearchProfileUrlPattern,\n linkedInSearchQueryPattern,\n type LinkedInSearchEmulatorProfile,\n type LinkedInSearchEmulatorResult,\n type LinkedInSearchEmulatorSearch,\n type LinkedInSearchEmulatorState\n} from './state.ts'\n\n/** Content type of every answer, as the fixtures record it. */\nconst contentType = 'application/json'\n\n/** Drill knobs (tests only): each makes the emulator disagree with one conformance claim. */\nexport type LinkedInSearchEmulatorDrills = {\n /** A search with the default `numResults` (10) answers its results without `text`. */\n readonly defaultSearchWithoutText?: boolean\n /** A search answers the query's seeded search with the most results, whatever `numResults`. */\n readonly numResultsIgnored?: boolean\n /** A profile lookup answers an empty JSON object. */\n readonly profileAnswersEmptyObject?: boolean\n /** An email lookup answers a JSON object without `email`. */\n readonly emailAnswerOmitsEmail?: boolean\n /** A rejected Exa key answers status 500 (with the recorded 401 body). */\n readonly exaUnauthorizedAs5xx?: boolean\n /** A rejected Enrich Layer key answers status 200 (with the recorded 401 body). */\n readonly enrichLayerUnauthorizedAs2xx?: boolean\n /** An absent profile answers status 200 (with the recorded 404 body). */\n readonly absentProfileAs2xx?: boolean\n}\n\nexport const linkedInSearchEmulatorDrillKnobs: ReadonlyArray<keyof LinkedInSearchEmulatorDrills> = [\n 'defaultSearchWithoutText',\n 'numResultsIgnored',\n 'profileAnswersEmptyObject',\n 'emailAnswerOmitsEmail',\n 'exaUnauthorizedAs5xx',\n 'enrichLayerUnauthorizedAs2xx',\n 'absentProfileAs2xx'\n]\n\nexport type LinkedInSearchApiEnv = {\n readonly drills: Readonly<Record<keyof LinkedInSearchEmulatorDrills, boolean>>\n}\n\n/**\n * The fixtures' error bodies, byte for byte (`JSON.stringify` keeps this key order): Exa's 401,\n * Enrich Layer's 401 (the profile and the email lookup answer the same body), and Enrich Layer's\n * not-found 404.\n */\nexport const linkedInSearchEmulatorErrorBodies = {\n exaUnauthorized: { requestId: 'synthetic-request-0003', error: 'Invalid API key' },\n enrichLayerUnauthorized: { code: 401, description: 'Invalid API Key', name: 'Unauthorized' },\n profileNotFound: { code: 404, description: 'Person profile does not exist', name: 'Not Found' }\n} as const satisfies Readonly<Record<string, Schema.Json>>\n\ntype Route = StatefulRoute<LinkedInSearchEmulatorState, LinkedInSearchApiEnv>\n\nconst peopleResultsCase = 'linkedin-search.search.people-results'\n\nconst numResultsLimitCase = 'linkedin-search.search.num-results-limit'\n\nconst profileCase = 'linkedin-search.profile.get-profile'\n\nconst emailCase = 'linkedin-search.email.lookup-answer'\n\nconst exaUnauthorizedCase = 'linkedin-search.errors.exa-unauthorized'\n\nconst enrichLayerUnauthorizedCase = 'linkedin-search.errors.enrich-layer-unauthorized'\n\nconst profileNotFoundCase = 'linkedin-search.errors.profile-not-found'\n\nconst evidence = (\n method: string,\n path: string,\n origin: string,\n caseIds: ReadonlyArray<string>\n) => ({\n method,\n path,\n kind: 'connector' as const,\n write: false,\n caseIds,\n evidence: 'unverified' as const,\n origin,\n // No route has a path parameter: every raw path is matched exactly.\n params: {}\n})\n\nconst json = (status: number, body: Schema.Json): Response =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': contentType } })\n\n/** A read-only commit. */\nconst answer =\n (response: () => Response): Commit =>\n () =>\n response()\n\n/** Whether the request's per-origin bearer digest is one a seed marks as rejected. */\nconst rejected = (digests: ReadonlyArray<string>, digest: string | undefined): boolean =>\n digest !== undefined && digests.includes(digest)\n\n// Exa people search.\n\ntype SearchInput = {\n readonly query: string\n readonly numResults: number\n readonly bearerDigest: string | undefined\n}\n\n/** The connector's default `numResults` (the people-results fixture's). */\nconst defaultNumResults = 10\n\n/** The result fields, in the fixtures' key order. */\nconst resultKeys = ['title', 'url', 'author', 'publishedDate', 'text'] as const\n\nconst renderResult = (result: LinkedInSearchEmulatorResult): Schema.JsonObject => {\n const rendered: Record<string, string> = {}\n\n for (const key of resultKeys) {\n const value = result[key]\n\n if (value !== undefined) rendered[key] = value\n }\n\n return rendered\n}\n\n/** The search the state answers for the input, or `undefined` (drills aside: the exact one). */\nconst searchFor = (\n state: LinkedInSearchEmulatorState,\n input: SearchInput,\n env: LinkedInSearchApiEnv\n): LinkedInSearchEmulatorSearch | undefined => {\n const exact = state.searches.find(\n search => search.query === input.query && search.numResults === input.numResults\n )\n\n if (exact === undefined || !env.drills.numResultsIgnored) return exact\n\n // The drill: the query's widest seeded answer (the first of the widest), whatever was asked.\n return state.searches\n .filter(search => search.query === input.query)\n .reduce((widest, search) => (search.results.length > widest.results.length ? search : widest))\n}\n\nconst search: Route = statefulRoute(\n {\n ...evidence('POST', '/search', linkedInSearchEmulatorExaOrigin, [\n peopleResultsCase,\n numResultsLimitCase,\n exaUnauthorizedCase\n ])\n },\n 'json',\n (request): SearchInput | NotEmulated => {\n const query = exactQuery(request, [], [], { rawNames: true })\n\n if (isNotEmulated(query)) return query\n\n const body = exactBodyKeys(request.json, 'the search body', [\n 'query',\n 'category',\n 'numResults',\n 'type',\n 'contents'\n ])\n\n if (isNotEmulated(body)) return body\n\n const text = body.query\n\n if (!Predicate.isString(text) || !linkedInSearchQueryPattern.test(text) || text.length > 500) {\n return notEmulated('query must be one trimmed, non-empty line of at most 500 characters')\n }\n\n if (body.category !== 'people') return notEmulated('category other than people is not emulated')\n\n if (body.type !== 'auto') return notEmulated('type other than auto is not emulated')\n\n const contents = exactBodyKeys(body.contents, 'contents', ['text'])\n\n if (isNotEmulated(contents)) return contents\n\n if (contents.text !== true)\n return notEmulated('contents other than { text: true } is not emulated')\n\n const numResults = integerIn(body.numResults, 'numResults', 1, linkedInSearchMaxNumResults)\n\n if (isNotEmulated(numResults)) return numResults\n\n return { query: text, numResults, bearerDigest: request.bearerDigest }\n },\n (state, input, { env }) => {\n if (rejected(state.exaRejectedKeyDigests, input.bearerDigest)) {\n // The Exa unauthorized fixture's 401, whatever the (well-formed) search.\n return answer(() =>\n json(\n env.drills.exaUnauthorizedAs5xx ? 500 : 401,\n linkedInSearchEmulatorErrorBodies.exaUnauthorized\n )\n )\n }\n\n const found = searchFor(state, input, env)\n\n if (found === undefined) {\n return notEmulated(\n 'a search the state holds no answer for (this query and numResults) is not emulated'\n )\n }\n\n return answer(() =>\n json(200, {\n results: found.results.map(result => {\n const rendered = renderResult(result)\n\n if (!env.drills.defaultSearchWithoutText || input.numResults !== defaultNumResults) {\n return rendered\n }\n\n const { text: _text, ...withoutText } = rendered\n\n return withoutText\n })\n })\n )\n }\n)\n\n// Enrich Layer lookups.\n\ntype LookupInput = { readonly url: string; readonly bearerDigest: string | undefined }\n\n/** The one `linkedin_profile_url` query parameter: a profile URL (decoded once). */\nconst lookupInput = (request: EmulatedRequest): LookupInput | NotEmulated => {\n const query = exactQuery(request, ['linkedin_profile_url'], [], { rawNames: true })\n\n if (isNotEmulated(query)) return query\n\n const url = query.linkedin_profile_url ?? ''\n\n if (!linkedInSearchProfileUrlPattern.test(url) || url.length > 2048) {\n return notEmulated('linkedin_profile_url must be a profile URL https://<host>/in/<slug>')\n }\n\n return { url, bearerDigest: request.bearerDigest }\n}\n\nconst profileHeld = (\n state: LinkedInSearchEmulatorState,\n url: string\n): LinkedInSearchEmulatorProfile | undefined => state.profiles.find(profile => profile.url === url)\n\n/** The Enrich Layer unauthorized fixture's 401 (both lookups answer it). */\nconst enrichLayerUnauthorized = (env: LinkedInSearchApiEnv): Commit =>\n answer(() =>\n json(\n env.drills.enrichLayerUnauthorizedAs2xx ? 200 : 401,\n linkedInSearchEmulatorErrorBodies.enrichLayerUnauthorized\n )\n )\n\nconst profile: Route = statefulRoute(\n evidence('GET', '/api/v2/profile', linkedInSearchEmulatorEnrichLayerOrigin, [\n profileCase,\n enrichLayerUnauthorizedCase,\n profileNotFoundCase\n ]),\n 'none',\n lookupInput,\n (state, input, { env }) => {\n if (rejected(state.enrichLayerRejectedKeyDigests, input.bearerDigest)) {\n return enrichLayerUnauthorized(env)\n }\n\n const held = profileHeld(state, input.url)\n\n if (held !== undefined) {\n return answer(() =>\n json(\n 200,\n env.drills.profileAnswersEmptyObject\n ? {}\n : {\n public_identifier: held.publicIdentifier,\n full_name: held.fullName,\n headline: held.headline\n }\n )\n )\n }\n\n if (state.absentProfileUrls.includes(input.url)) {\n return answer(() =>\n json(\n env.drills.absentProfileAs2xx ? 200 : 404,\n linkedInSearchEmulatorErrorBodies.profileNotFound\n )\n )\n }\n\n return notEmulated(\n 'a profile URL the state holds neither as a profile nor as absent is not emulated'\n )\n }\n)\n\nconst email: Route = statefulRoute(\n evidence('GET', '/api/v2/profile/email', linkedInSearchEmulatorEnrichLayerOrigin, [\n emailCase,\n enrichLayerUnauthorizedCase\n ]),\n 'none',\n lookupInput,\n (state, input, { env }) => {\n if (rejected(state.enrichLayerRejectedKeyDigests, input.bearerDigest)) {\n return enrichLayerUnauthorized(env)\n }\n\n const address = profileHeld(state, input.url)?.email\n\n if (address === undefined) {\n return notEmulated(\n 'an email lookup of a profile the state holds no email answer for is not emulated'\n )\n }\n\n return answer(() => json(200, env.drills.emailAnswerOmitsEmail ? {} : { email: address }))\n }\n)\n\n/** Every emulated route, in manifest order. */\nexport const linkedInSearchApiRoutes: ReadonlyArray<Route> = [search, profile, email]\n"],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ //#region src/linkedin-search/state.d.ts
4
+ /** The origin every Exa fixture records; another origin is not emulated. */
5
+ declare const linkedInSearchEmulatorExaOrigin = "https://api.exa.ai";
6
+ /** The origin every Enrich Layer fixture records (its API base is `/api/v2`). */
7
+ declare const linkedInSearchEmulatorEnrichLayerOrigin = "https://enrichlayer.com";
8
+ /**
9
+ * The per-origin digest of an API key: SHA-256 (hex) of the origin, a space, and the key. Neither
10
+ * an origin nor a recognisable bearer holds a space, so distinct pairs never share a text.
11
+ */
12
+ declare const linkedInSearchKeyDigest: (key: string, origin: string) => string;
13
+ /** A search query as the cases seed it: trimmed, non-empty, one line, at most 500 characters. */
14
+ declare const linkedInSearchQueryPattern: RegExp;
15
+ /** The profile URL form the emulator takes: `https://<host>/in/<slug>`. */
16
+ declare const linkedInSearchProfileUrlPattern: RegExp;
17
+ /** The largest `numResults` the emulator takes. */
18
+ declare const linkedInSearchMaxNumResults = 100;
19
+ /** One Exa result, in the fixtures' fields (each a string, or absent; never null). */
20
+ declare const LinkedInSearchEmulatorResult: Schema.Struct<{
21
+ readonly title: Schema.optionalKey<Schema.String>;
22
+ readonly url: Schema.optionalKey<Schema.String>;
23
+ readonly author: Schema.optionalKey<Schema.String>;
24
+ readonly publishedDate: Schema.optionalKey<Schema.String>;
25
+ readonly text: Schema.optionalKey<Schema.String>;
26
+ }>;
27
+ type LinkedInSearchEmulatorResult = typeof LinkedInSearchEmulatorResult.Type;
28
+ /**
29
+ * An Exa people search the state answers: the query, the `numResults` it was asked with, and the
30
+ * results answered (1 to `numResults`; no fixture records an empty search).
31
+ */
32
+ declare const LinkedInSearchEmulatorSearch: Schema.Struct<{
33
+ readonly query: Schema.String;
34
+ readonly numResults: Schema.Int;
35
+ readonly results: Schema.$Array<Schema.Struct<{
36
+ readonly title: Schema.optionalKey<Schema.String>;
37
+ readonly url: Schema.optionalKey<Schema.String>;
38
+ readonly author: Schema.optionalKey<Schema.String>;
39
+ readonly publishedDate: Schema.optionalKey<Schema.String>;
40
+ readonly text: Schema.optionalKey<Schema.String>;
41
+ }>>;
42
+ }>;
43
+ type LinkedInSearchEmulatorSearch = typeof LinkedInSearchEmulatorSearch.Type;
44
+ /**
45
+ * An Enrich Layer profile the state answers, in the profile fixture's fields, and the email its
46
+ * lookup answers (absent: an email lookup of this profile is not emulated).
47
+ */
48
+ declare const LinkedInSearchEmulatorProfile: Schema.Struct<{
49
+ readonly url: Schema.String;
50
+ readonly publicIdentifier: Schema.String;
51
+ readonly fullName: Schema.String;
52
+ readonly headline: Schema.String;
53
+ readonly email: Schema.optionalKey<Schema.String>;
54
+ }>;
55
+ type LinkedInSearchEmulatorProfile = typeof LinkedInSearchEmulatorProfile.Type;
56
+ /** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
57
+ declare const LinkedInSearchEmulatorStateSchema: Schema.Struct<{
58
+ /** Digests (for the Exa origin) of the keys Exa answers with the recorded 401. */readonly exaRejectedKeyDigests: Schema.$Array<Schema.String>;
59
+ readonly searches: Schema.$Array<Schema.Struct<{
60
+ readonly query: Schema.String;
61
+ readonly numResults: Schema.Int;
62
+ readonly results: Schema.$Array<Schema.Struct<{
63
+ readonly title: Schema.optionalKey<Schema.String>;
64
+ readonly url: Schema.optionalKey<Schema.String>;
65
+ readonly author: Schema.optionalKey<Schema.String>;
66
+ readonly publishedDate: Schema.optionalKey<Schema.String>;
67
+ readonly text: Schema.optionalKey<Schema.String>;
68
+ }>>;
69
+ }>>; /** Digests (for the Enrich Layer origin) of the keys Enrich Layer answers with the 401. */
70
+ readonly enrichLayerRejectedKeyDigests: Schema.$Array<Schema.String>;
71
+ readonly profiles: Schema.$Array<Schema.Struct<{
72
+ readonly url: Schema.String;
73
+ readonly publicIdentifier: Schema.String;
74
+ readonly fullName: Schema.String;
75
+ readonly headline: Schema.String;
76
+ readonly email: Schema.optionalKey<Schema.String>;
77
+ }>>; /** Profile URLs that name no profile: their profile lookup answers the recorded 404. */
78
+ readonly absentProfileUrls: Schema.$Array<Schema.String>;
79
+ }>;
80
+ type LinkedInSearchEmulatorState = typeof LinkedInSearchEmulatorStateSchema.Type;
81
+ /**
82
+ * A typed seed: every key, when given, replaces that part of the default seed (the fixture
83
+ * entities). Rejected keys are given as keys and kept only as their per-origin digests.
84
+ */
85
+ declare const LinkedInSearchEmulatorSeed: Schema.Struct<{
86
+ readonly exaRejectedKeys: Schema.optionalKey<Schema.$Array<Schema.String>>;
87
+ readonly searches: Schema.optionalKey<Schema.$Array<Schema.Struct<{
88
+ readonly query: Schema.String;
89
+ readonly numResults: Schema.Int;
90
+ readonly results: Schema.$Array<Schema.Struct<{
91
+ readonly title: Schema.optionalKey<Schema.String>;
92
+ readonly url: Schema.optionalKey<Schema.String>;
93
+ readonly author: Schema.optionalKey<Schema.String>;
94
+ readonly publishedDate: Schema.optionalKey<Schema.String>;
95
+ readonly text: Schema.optionalKey<Schema.String>;
96
+ }>>;
97
+ }>>>;
98
+ readonly enrichLayerRejectedKeys: Schema.optionalKey<Schema.$Array<Schema.String>>;
99
+ readonly profiles: Schema.optionalKey<Schema.$Array<Schema.Struct<{
100
+ readonly url: Schema.String;
101
+ readonly publicIdentifier: Schema.String;
102
+ readonly fullName: Schema.String;
103
+ readonly headline: Schema.String;
104
+ readonly email: Schema.optionalKey<Schema.String>;
105
+ }>>>;
106
+ readonly absentProfileUrls: Schema.optionalKey<Schema.$Array<Schema.String>>;
107
+ }>;
108
+ type LinkedInSearchEmulatorSeed = typeof LinkedInSearchEmulatorSeed.Type;
109
+ /** The synthetic invalid keys the unauthorized cases send (never account data). */
110
+ declare const linkedInSearchEmulatorDefaultRejectedKeys: {
111
+ readonly exa: readonly ["yolk-conformance-invalid-exa-key"];
112
+ readonly enrichLayer: readonly ["yolk-conformance-invalid-enrich-layer-key"];
113
+ };
114
+ /**
115
+ * Decode and build a seed; a string is the reason it is invalid, in constant text only (a
116
+ * category and a field path; never an input value or a key the seed made up).
117
+ */
118
+ declare const buildSeedState: (input: unknown) => LinkedInSearchEmulatorState | string;
119
+ /** Decode a full state (a restored snapshot); a string is the reason it is invalid (constant). */
120
+ declare const decodeState: (input: unknown) => LinkedInSearchEmulatorState | string;
121
+ //#endregion
122
+ export { LinkedInSearchEmulatorProfile, LinkedInSearchEmulatorResult, LinkedInSearchEmulatorSearch, LinkedInSearchEmulatorSeed, LinkedInSearchEmulatorState, LinkedInSearchEmulatorStateSchema, buildSeedState, decodeState, linkedInSearchEmulatorDefaultRejectedKeys, linkedInSearchEmulatorEnrichLayerOrigin, linkedInSearchEmulatorExaOrigin, linkedInSearchKeyDigest, linkedInSearchMaxNumResults, linkedInSearchProfileUrlPattern, linkedInSearchQueryPattern };
123
+ //# sourceMappingURL=state.d.mts.map