@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":"state.d.mts","names":[],"sources":["../../src/linkedin-search/state.ts"],"mappings":";;;;cAwBa,+BAAA;AAAb;AAAA,cAGa,uCAAA;;;AAH+B;AAG5C;cAMa,uBAAA,GAA2B,GAAA,UAAa,MAAc;;cAItD,0BAAA,EAA0B,MAAkB;AAVL;AAAA,cAavC,+BAAA,EAA+B,MAA6C;;cAG5E,2BAAA;;cA2BA,4BAAA,EAA4B,MAAA,CAAA,MAAA;EAAA;;;;;;KAQ7B,4BAAA,UAAsC,4BAAA,CAA6B,IAAI;;;AAtCM;AAGzF;cAyCa,4BAAA,EAA4B,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;KAM7B,4BAAA,UAAsC,4BAAA,CAA6B,IAAI;;;;;cAMtE,6BAAA,EAA6B,MAAA,CAAA,MAAA;EAAA;;;;;;KAQ9B,6BAAA,UAAuC,6BAAA,CAA8B,IAAI;;cAGxE,iCAAA,EAAiC,MAAA,CAAA,MAAA;;;;;;;;;;;;;;;;;;;;;;;KAWlC,2BAAA,UAAqC,iCAAA,CAAkC,IAAI;;AAxCJ;AAMnF;;cAwCa,0BAAA,EAA0B,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;KAQ3B,0BAAA,UAAoC,0BAAA,CAA2B,IAAI;;cAwIlE,yCAAA;EAAA,SAGH,GAAA;EAAA,SAAA,WAAA;AAAA;;;;;cAwFG,cAAA,GAAkB,KAAA,cAAiB,2BAA2B;;cAS9D,WAAA,GAAe,KAAA,cAAiB,2BAA2B"}
@@ -0,0 +1,261 @@
1
+ import { isRecognisableBearerValue } from "../stateful-secrets.mjs";
2
+ import { Match, Predicate, Result } from "effect";
3
+ import * as Schema from "effect/Schema";
4
+ import { createHash } from "node:crypto";
5
+ //#region src/linkedin-search/state.ts
6
+ /**
7
+ * LinkedIn search emulator state: the Exa people searches and the Enrich Layer profiles the state
8
+ * answers, the absent profiles, and the digests of the API keys a seed marks as rejected on each
9
+ * origin, with the seed input and the default seed (internal; re-exported by
10
+ * `src/linkedin-search.ts`).
11
+ *
12
+ * The default entities are the synthetic LinkedIn search conformance fixtures, copied as data
13
+ * (the seeded query with the three answers the fixtures record for `numResults` 10, 3, and 2, the
14
+ * seeded profile with its profile and email answers, and the seeded absent profile), never
15
+ * imported from SDK code. The default rejected keys are the synthetic invalid keys the two
16
+ * unauthorized cases send (no account data). Every rendered answer keeps the fixtures' key order.
17
+ *
18
+ * Rejected keys are never kept: the state holds only their per-origin digests (SHA-256 of the
19
+ * origin, a space, and the key), so a rejected key that a request carries as its bearer reaches
20
+ * neither the state nor `/_emulate/*`. There are no writes, cursors, minted ids, or clock reads.
21
+ *
22
+ * @experimental
23
+ */
24
+ /** The origin every Exa fixture records; another origin is not emulated. */
25
+ const linkedInSearchEmulatorExaOrigin = "https://api.exa.ai";
26
+ /** The origin every Enrich Layer fixture records (its API base is `/api/v2`). */
27
+ const linkedInSearchEmulatorEnrichLayerOrigin = "https://enrichlayer.com";
28
+ /**
29
+ * The per-origin digest of an API key: SHA-256 (hex) of the origin, a space, and the key. Neither
30
+ * an origin nor a recognisable bearer holds a space, so distinct pairs never share a text.
31
+ */
32
+ const linkedInSearchKeyDigest = (key, origin) => createHash("sha256").update(`${origin} ${key}`, "utf8").digest("hex");
33
+ /** A search query as the cases seed it: trimmed, non-empty, one line, at most 500 characters. */
34
+ const linkedInSearchQueryPattern = /^\S(?:.*\S)?$/;
35
+ /** The profile URL form the emulator takes: `https://<host>/in/<slug>`. */
36
+ const linkedInSearchProfileUrlPattern = /^https:\/\/[A-Za-z0-9.-]+\/in\/[^\s?#]+$/;
37
+ /** The largest `numResults` the emulator takes. */
38
+ const linkedInSearchMaxNumResults = 100;
39
+ const SearchQuery = Schema.String.check(Schema.isPattern(linkedInSearchQueryPattern), Schema.isMaxLength(500));
40
+ const ProfileUrl = Schema.String.check(Schema.isPattern(linkedInSearchProfileUrlPattern), Schema.isMaxLength(2048));
41
+ const NumResults = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(100));
42
+ /**
43
+ * An API key a seed marks as rejected (checked to be a recognisable fail-closed bearer value after
44
+ * decoding, with a constant reason: a key is a credential, so no reason quotes it).
45
+ */
46
+ const RejectedKey = Schema.String;
47
+ /** A SHA-256 digest (64 lower-case hex digits). */
48
+ const Digest = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
49
+ /** One Exa result, in the fixtures' fields (each a string, or absent; never null). */
50
+ const LinkedInSearchEmulatorResult = Schema.Struct({
51
+ title: Schema.optionalKey(Schema.String),
52
+ url: Schema.optionalKey(Schema.String),
53
+ author: Schema.optionalKey(Schema.String),
54
+ publishedDate: Schema.optionalKey(Schema.String),
55
+ text: Schema.optionalKey(Schema.String)
56
+ });
57
+ /**
58
+ * An Exa people search the state answers: the query, the `numResults` it was asked with, and the
59
+ * results answered (1 to `numResults`; no fixture records an empty search).
60
+ */
61
+ const LinkedInSearchEmulatorSearch = Schema.Struct({
62
+ query: SearchQuery,
63
+ numResults: NumResults,
64
+ results: Schema.Array(LinkedInSearchEmulatorResult)
65
+ });
66
+ /**
67
+ * An Enrich Layer profile the state answers, in the profile fixture's fields, and the email its
68
+ * lookup answers (absent: an email lookup of this profile is not emulated).
69
+ */
70
+ const LinkedInSearchEmulatorProfile = Schema.Struct({
71
+ url: ProfileUrl,
72
+ publicIdentifier: Schema.String,
73
+ fullName: Schema.String,
74
+ headline: Schema.String,
75
+ email: Schema.optionalKey(Schema.String)
76
+ });
77
+ /** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
78
+ const LinkedInSearchEmulatorStateSchema = Schema.Struct({
79
+ /** Digests (for the Exa origin) of the keys Exa answers with the recorded 401. */
80
+ exaRejectedKeyDigests: Schema.Array(Digest),
81
+ searches: Schema.Array(LinkedInSearchEmulatorSearch),
82
+ /** Digests (for the Enrich Layer origin) of the keys Enrich Layer answers with the 401. */
83
+ enrichLayerRejectedKeyDigests: Schema.Array(Digest),
84
+ profiles: Schema.Array(LinkedInSearchEmulatorProfile),
85
+ /** Profile URLs that name no profile: their profile lookup answers the recorded 404. */
86
+ absentProfileUrls: Schema.Array(ProfileUrl)
87
+ });
88
+ /**
89
+ * A typed seed: every key, when given, replaces that part of the default seed (the fixture
90
+ * entities). Rejected keys are given as keys and kept only as their per-origin digests.
91
+ */
92
+ const LinkedInSearchEmulatorSeed = Schema.Struct({
93
+ exaRejectedKeys: Schema.optionalKey(Schema.Array(RejectedKey)),
94
+ searches: Schema.optionalKey(Schema.Array(LinkedInSearchEmulatorSearch)),
95
+ enrichLayerRejectedKeys: Schema.optionalKey(Schema.Array(RejectedKey)),
96
+ profiles: Schema.optionalKey(Schema.Array(LinkedInSearchEmulatorProfile)),
97
+ absentProfileUrls: Schema.optionalKey(Schema.Array(ProfileUrl))
98
+ });
99
+ const strict = { onExcessProperty: "error" };
100
+ const decodeSeedInput = Schema.decodeUnknownResult(LinkedInSearchEmulatorSeed, strict);
101
+ const decodeStateInput = Schema.decodeUnknownResult(LinkedInSearchEmulatorStateSchema, strict);
102
+ /** Every field name the seed and state schemas define (the only names a seed error may print). */
103
+ const knownFieldNames = new Set([
104
+ ...Object.keys(LinkedInSearchEmulatorSeed.fields),
105
+ ...Object.keys(LinkedInSearchEmulatorStateSchema.fields),
106
+ ...Object.keys(LinkedInSearchEmulatorSearch.fields),
107
+ ...Object.keys(LinkedInSearchEmulatorResult.fields),
108
+ ...Object.keys(LinkedInSearchEmulatorProfile.fields)
109
+ ]);
110
+ /**
111
+ * A field path in constant text: array indices and the schemas' own field names as written, any
112
+ * other key (one a seed made up, which may be anything, a key included) as `<unexpected key>`.
113
+ */
114
+ const fieldPath = (path) => {
115
+ const text = path.map((segment) => Predicate.isNumber(segment) ? `[${segment}]` : Predicate.isString(segment) && knownFieldNames.has(segment) ? `.${segment}` : ".<unexpected key>").join("").replace(/^\./, "");
116
+ return text === "" ? "the seed root" : text;
117
+ };
118
+ /** The category of a leaf issue, in constant text. */
119
+ const issueCategory = (tag) => {
120
+ switch (tag) {
121
+ case "MissingKey": return "missing key";
122
+ case "UnexpectedKey": return "unexpected key";
123
+ case "InvalidType": return "invalid type";
124
+ case "AnyOf":
125
+ case "OneOf": return "no matching member";
126
+ case "Forbidden": return "forbidden value";
127
+ default: return "invalid value";
128
+ }
129
+ };
130
+ /** The first leaf issue under `issue`, with its full path. */
131
+ const firstLeaf = (issue, path) => Match.value(issue).pipe(Match.tag("Pointer", (pointer) => firstLeaf(pointer.issue, [...path, ...pointer.path])), Match.tag("Filter", (filter) => firstLeaf(filter.issue, path)), Match.tag("Encoding", (encoding) => firstLeaf(encoding.issue, path)), Match.tag("Composite", (composite) => firstLeaf(composite.issues[0], path)), Match.tag("AnyOf", (anyOf) => {
132
+ const [first] = anyOf.issues;
133
+ return first === void 0 ? {
134
+ issue: anyOf,
135
+ path
136
+ } : firstLeaf(first, path);
137
+ }), Match.orElse((leaf) => ({
138
+ issue: leaf,
139
+ path
140
+ })));
141
+ /**
142
+ * Why a seed does not decode, in constant text only: the category of the first issue and its
143
+ * field path, never the input (Effect's own messages quote input values, an unexpected key's value
144
+ * included, and a seed may carry keys). An unexpected key is reported at its parent's path.
145
+ */
146
+ const issueReason = (root) => {
147
+ const { issue, path } = firstLeaf(root, []);
148
+ const where = Predicate.isTagged(issue, "UnexpectedKey") ? path.slice(0, -1) : path;
149
+ return `${issueCategory(issue._tag)} at ${fieldPath(where)}`;
150
+ };
151
+ const seededQuery = "synthetic conformance engineer";
152
+ const person01 = {
153
+ title: "Synthetic Person 01 | Conformance Engineer at Example Synthetic Co",
154
+ url: "https://linkedin.example.com/in/synthetic-person-01",
155
+ author: "Synthetic Person 01",
156
+ text: "Synthetic Person 01. Conformance Engineer at Example Synthetic Co. Synthetic profile text, not a real person."
157
+ };
158
+ const person02 = {
159
+ title: "Synthetic Person 02 | Test Engineer at Example Synthetic Co",
160
+ url: "https://linkedin.example.com/in/synthetic-person-02",
161
+ author: "Synthetic Person 02",
162
+ publishedDate: "2026-01-15T00:00:00.000Z",
163
+ text: "Synthetic Person 02. Test Engineer at Example Synthetic Co. Synthetic profile text, not a real person."
164
+ };
165
+ const person03 = {
166
+ title: "Synthetic Person 03 | Conformance Lead at Example Synthetic Labs",
167
+ url: "https://linkedin.example.com/in/synthetic-person-03"
168
+ };
169
+ /** The limited search's second result: the fixture records it without `publishedDate`. */
170
+ const { publishedDate: _limitedOmits, ...person02Limited } = person02;
171
+ const defaultSearches = [
172
+ {
173
+ query: seededQuery,
174
+ numResults: 10,
175
+ results: [
176
+ person01,
177
+ person02,
178
+ person03
179
+ ]
180
+ },
181
+ {
182
+ query: seededQuery,
183
+ numResults: 3,
184
+ results: [
185
+ person01,
186
+ person02,
187
+ person03
188
+ ]
189
+ },
190
+ {
191
+ query: seededQuery,
192
+ numResults: 2,
193
+ results: [person01, person02Limited]
194
+ }
195
+ ];
196
+ const defaultProfiles = [{
197
+ url: "https://linkedin.example.com/in/synthetic-person-01",
198
+ publicIdentifier: "synthetic-person-01",
199
+ fullName: "Synthetic Person 01",
200
+ headline: "Conformance Engineer at Example Synthetic Co",
201
+ email: "synthetic-person-01@example.com"
202
+ }];
203
+ const defaultAbsentProfileUrls = ["https://linkedin.example.com/in/synthetic-absent-person-00"];
204
+ /** The synthetic invalid keys the unauthorized cases send (never account data). */
205
+ const linkedInSearchEmulatorDefaultRejectedKeys = {
206
+ exa: ["yolk-conformance-invalid-exa-key"],
207
+ enrichLayer: ["yolk-conformance-invalid-enrich-layer-key"]
208
+ };
209
+ /** The index of the first value equal to an earlier one, or `-1`. */
210
+ const repeatIndex = (values) => values.findIndex((value, index) => values.indexOf(value) !== index);
211
+ /**
212
+ * Integrity problems a decoded seed can still have, in constant text only (a category and a field
213
+ * path, never a seeded value: a seed may carry keys anywhere).
214
+ */
215
+ const seedProblem = (parts) => {
216
+ const search = repeatIndex(parts.searches.map((entry) => JSON.stringify([entry.query, entry.numResults])));
217
+ if (search !== -1) return `duplicate search at searches[${search}]`;
218
+ const results = parts.searches.findIndex((entry) => entry.results.length === 0 || entry.results.length > entry.numResults);
219
+ if (results !== -1) return `not 1 to numResults results at searches[${results}].results`;
220
+ const url = repeatIndex([...parts.profiles.map((profile) => profile.url), ...parts.absentProfileUrls]);
221
+ if (url !== -1) return url < parts.profiles.length ? `duplicate profile URL at profiles[${url}].url` : `duplicate profile URL at absentProfileUrls[${url - parts.profiles.length}]`;
222
+ for (const [field, keys] of [["exaRejectedKeys", parts.exaRejectedKeys], ["enrichLayerRejectedKeys", parts.enrichLayerRejectedKeys]]) {
223
+ const unrecognisable = keys.findIndex((key) => !isRecognisableBearerValue(key));
224
+ if (unrecognisable !== -1) return `not a recognisable bearer value at ${field}[${unrecognisable}]`;
225
+ const repeated = repeatIndex(keys);
226
+ if (repeated !== -1) return `duplicate key at ${field}[${repeated}]`;
227
+ }
228
+ };
229
+ const stateFromSeed = (seed) => {
230
+ const parts = {
231
+ exaRejectedKeys: seed.exaRejectedKeys ?? linkedInSearchEmulatorDefaultRejectedKeys.exa,
232
+ searches: seed.searches ?? defaultSearches,
233
+ enrichLayerRejectedKeys: seed.enrichLayerRejectedKeys ?? linkedInSearchEmulatorDefaultRejectedKeys.enrichLayer,
234
+ profiles: seed.profiles ?? defaultProfiles,
235
+ absentProfileUrls: seed.absentProfileUrls ?? defaultAbsentProfileUrls
236
+ };
237
+ return seedProblem(parts) ?? {
238
+ exaRejectedKeyDigests: parts.exaRejectedKeys.map((key) => linkedInSearchKeyDigest(key, "https://api.exa.ai")),
239
+ searches: parts.searches,
240
+ enrichLayerRejectedKeyDigests: parts.enrichLayerRejectedKeys.map((key) => linkedInSearchKeyDigest(key, "https://enrichlayer.com")),
241
+ profiles: parts.profiles,
242
+ absentProfileUrls: parts.absentProfileUrls
243
+ };
244
+ };
245
+ /**
246
+ * Decode and build a seed; a string is the reason it is invalid, in constant text only (a
247
+ * category and a field path; never an input value or a key the seed made up).
248
+ */
249
+ const buildSeedState = (input) => {
250
+ const decoded = decodeSeedInput(input);
251
+ return Result.isFailure(decoded) ? issueReason(decoded.failure.issue) : stateFromSeed(decoded.success);
252
+ };
253
+ /** Decode a full state (a restored snapshot); a string is the reason it is invalid (constant). */
254
+ const decodeState = (input) => {
255
+ const decoded = decodeStateInput(input);
256
+ return Result.isFailure(decoded) ? issueReason(decoded.failure.issue) : decoded.success;
257
+ };
258
+ //#endregion
259
+ export { LinkedInSearchEmulatorProfile, LinkedInSearchEmulatorResult, LinkedInSearchEmulatorSearch, LinkedInSearchEmulatorSeed, LinkedInSearchEmulatorStateSchema, buildSeedState, decodeState, linkedInSearchEmulatorDefaultRejectedKeys, linkedInSearchEmulatorEnrichLayerOrigin, linkedInSearchEmulatorExaOrigin, linkedInSearchKeyDigest, linkedInSearchMaxNumResults, linkedInSearchProfileUrlPattern, linkedInSearchQueryPattern };
260
+
261
+ //# sourceMappingURL=state.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.mjs","names":[],"sources":["../../src/linkedin-search/state.ts"],"sourcesContent":["/**\n * LinkedIn search emulator state: the Exa people searches and the Enrich Layer profiles the state\n * answers, the absent profiles, and the digests of the API keys a seed marks as rejected on each\n * origin, with the seed input and the default seed (internal; re-exported by\n * `src/linkedin-search.ts`).\n *\n * The default entities are the synthetic LinkedIn search conformance fixtures, copied as data\n * (the seeded query with the three answers the fixtures record for `numResults` 10, 3, and 2, the\n * seeded profile with its profile and email answers, and the seeded absent profile), never\n * imported from SDK code. The default rejected keys are the synthetic invalid keys the two\n * unauthorized cases send (no account data). Every rendered answer keeps the fixtures' key order.\n *\n * Rejected keys are never kept: the state holds only their per-origin digests (SHA-256 of the\n * origin, a space, and the key), so a rejected key that a request carries as its bearer reaches\n * neither the state nor `/_emulate/*`. There are no writes, cursors, minted ids, or clock reads.\n *\n * @experimental\n */\nimport { createHash } from 'node:crypto'\nimport { Match, Predicate, Result, type SchemaIssue } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport { isRecognisableBearerValue } from '../stateful-secrets.ts'\n\n/** The origin every Exa fixture records; another origin is not emulated. */\nexport const linkedInSearchEmulatorExaOrigin = 'https://api.exa.ai'\n\n/** The origin every Enrich Layer fixture records (its API base is `/api/v2`). */\nexport const linkedInSearchEmulatorEnrichLayerOrigin = 'https://enrichlayer.com'\n\n/**\n * The per-origin digest of an API key: SHA-256 (hex) of the origin, a space, and the key. Neither\n * an origin nor a recognisable bearer holds a space, so distinct pairs never share a text.\n */\nexport const linkedInSearchKeyDigest = (key: string, origin: string): string =>\n createHash('sha256').update(`${origin} ${key}`, 'utf8').digest('hex')\n\n/** A search query as the cases seed it: trimmed, non-empty, one line, at most 500 characters. */\nexport const linkedInSearchQueryPattern = /^\\S(?:.*\\S)?$/\n\n/** The profile URL form the emulator takes: `https://<host>/in/<slug>`. */\nexport const linkedInSearchProfileUrlPattern = /^https:\\/\\/[A-Za-z0-9.-]+\\/in\\/[^\\s?#]+$/\n\n/** The largest `numResults` the emulator takes. */\nexport const linkedInSearchMaxNumResults = 100\n\nconst SearchQuery = Schema.String.check(\n Schema.isPattern(linkedInSearchQueryPattern),\n Schema.isMaxLength(500)\n)\n\nconst ProfileUrl = Schema.String.check(\n Schema.isPattern(linkedInSearchProfileUrlPattern),\n Schema.isMaxLength(2048)\n)\n\nconst NumResults = Schema.Int.check(\n Schema.isGreaterThanOrEqualTo(1),\n Schema.isLessThanOrEqualTo(linkedInSearchMaxNumResults)\n)\n\n/**\n * An API key a seed marks as rejected (checked to be a recognisable fail-closed bearer value after\n * decoding, with a constant reason: a key is a credential, so no reason quotes it).\n */\nconst RejectedKey = Schema.String\n\n/** A SHA-256 digest (64 lower-case hex digits). */\nconst Digest = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/))\n\n/** One Exa result, in the fixtures' fields (each a string, or absent; never null). */\nexport const LinkedInSearchEmulatorResult = Schema.Struct({\n title: Schema.optionalKey(Schema.String),\n url: Schema.optionalKey(Schema.String),\n author: Schema.optionalKey(Schema.String),\n publishedDate: Schema.optionalKey(Schema.String),\n text: Schema.optionalKey(Schema.String)\n})\n\nexport type LinkedInSearchEmulatorResult = typeof LinkedInSearchEmulatorResult.Type\n\n/**\n * An Exa people search the state answers: the query, the `numResults` it was asked with, and the\n * results answered (1 to `numResults`; no fixture records an empty search).\n */\nexport const LinkedInSearchEmulatorSearch = Schema.Struct({\n query: SearchQuery,\n numResults: NumResults,\n results: Schema.Array(LinkedInSearchEmulatorResult)\n})\n\nexport type LinkedInSearchEmulatorSearch = typeof LinkedInSearchEmulatorSearch.Type\n\n/**\n * An Enrich Layer profile the state answers, in the profile fixture's fields, and the email its\n * lookup answers (absent: an email lookup of this profile is not emulated).\n */\nexport const LinkedInSearchEmulatorProfile = Schema.Struct({\n url: ProfileUrl,\n publicIdentifier: Schema.String,\n fullName: Schema.String,\n headline: Schema.String,\n email: Schema.optionalKey(Schema.String)\n})\n\nexport type LinkedInSearchEmulatorProfile = typeof LinkedInSearchEmulatorProfile.Type\n\n/** The whole emulator state (JSON-compatible; what `snapshot()` returns). */\nexport const LinkedInSearchEmulatorStateSchema = Schema.Struct({\n /** Digests (for the Exa origin) of the keys Exa answers with the recorded 401. */\n exaRejectedKeyDigests: Schema.Array(Digest),\n searches: Schema.Array(LinkedInSearchEmulatorSearch),\n /** Digests (for the Enrich Layer origin) of the keys Enrich Layer answers with the 401. */\n enrichLayerRejectedKeyDigests: Schema.Array(Digest),\n profiles: Schema.Array(LinkedInSearchEmulatorProfile),\n /** Profile URLs that name no profile: their profile lookup answers the recorded 404. */\n absentProfileUrls: Schema.Array(ProfileUrl)\n})\n\nexport type LinkedInSearchEmulatorState = typeof LinkedInSearchEmulatorStateSchema.Type\n\n/**\n * A typed seed: every key, when given, replaces that part of the default seed (the fixture\n * entities). Rejected keys are given as keys and kept only as their per-origin digests.\n */\nexport const LinkedInSearchEmulatorSeed = Schema.Struct({\n exaRejectedKeys: Schema.optionalKey(Schema.Array(RejectedKey)),\n searches: Schema.optionalKey(Schema.Array(LinkedInSearchEmulatorSearch)),\n enrichLayerRejectedKeys: Schema.optionalKey(Schema.Array(RejectedKey)),\n profiles: Schema.optionalKey(Schema.Array(LinkedInSearchEmulatorProfile)),\n absentProfileUrls: Schema.optionalKey(Schema.Array(ProfileUrl))\n})\n\nexport type LinkedInSearchEmulatorSeed = typeof LinkedInSearchEmulatorSeed.Type\n\nconst strict = { onExcessProperty: 'error' } as const\n\nconst decodeSeedInput = Schema.decodeUnknownResult(LinkedInSearchEmulatorSeed, strict)\n\nconst decodeStateInput = Schema.decodeUnknownResult(LinkedInSearchEmulatorStateSchema, strict)\n\n/** Every field name the seed and state schemas define (the only names a seed error may print). */\nconst knownFieldNames: ReadonlySet<string> = new Set([\n ...Object.keys(LinkedInSearchEmulatorSeed.fields),\n ...Object.keys(LinkedInSearchEmulatorStateSchema.fields),\n ...Object.keys(LinkedInSearchEmulatorSearch.fields),\n ...Object.keys(LinkedInSearchEmulatorResult.fields),\n ...Object.keys(LinkedInSearchEmulatorProfile.fields)\n])\n\n/**\n * A field path in constant text: array indices and the schemas' own field names as written, any\n * other key (one a seed made up, which may be anything, a key included) as `<unexpected key>`.\n */\nconst fieldPath = (path: ReadonlyArray<PropertyKey>): string => {\n const text = path\n .map(segment =>\n Predicate.isNumber(segment)\n ? `[${segment}]`\n : Predicate.isString(segment) && knownFieldNames.has(segment)\n ? `.${segment}`\n : '.<unexpected key>'\n )\n .join('')\n .replace(/^\\./, '')\n\n return text === '' ? 'the seed root' : text\n}\n\n/** The category of a leaf issue, in constant text. */\nconst issueCategory = (tag: SchemaIssue.Issue['_tag']): string => {\n switch (tag) {\n case 'MissingKey':\n return 'missing key'\n case 'UnexpectedKey':\n return 'unexpected key'\n case 'InvalidType':\n return 'invalid type'\n case 'AnyOf':\n case 'OneOf':\n return 'no matching member'\n case 'Forbidden':\n return 'forbidden value'\n default:\n return 'invalid value'\n }\n}\n\ntype Leaf = { readonly issue: SchemaIssue.Issue; readonly path: ReadonlyArray<PropertyKey> }\n\n/** The first leaf issue under `issue`, with its full path. */\nconst firstLeaf = (issue: SchemaIssue.Issue, path: ReadonlyArray<PropertyKey>): Leaf =>\n Match.value(issue).pipe(\n Match.tag('Pointer', pointer => firstLeaf(pointer.issue, [...path, ...pointer.path])),\n Match.tag('Filter', filter => firstLeaf(filter.issue, path)),\n Match.tag('Encoding', encoding => firstLeaf(encoding.issue, path)),\n Match.tag('Composite', composite => firstLeaf(composite.issues[0], path)),\n Match.tag('AnyOf', anyOf => {\n const [first] = anyOf.issues\n\n return first === undefined ? { issue: anyOf, path } : firstLeaf(first, path)\n }),\n Match.orElse(leaf => ({ issue: leaf, path }))\n )\n\n/**\n * Why a seed does not decode, in constant text only: the category of the first issue and its\n * field path, never the input (Effect's own messages quote input values, an unexpected key's value\n * included, and a seed may carry keys). An unexpected key is reported at its parent's path.\n */\nconst issueReason = (root: SchemaIssue.Issue): string => {\n const { issue, path } = firstLeaf(root, [])\n\n // The unexpected key itself is input: report it at its parent.\n const where = Predicate.isTagged(issue, 'UnexpectedKey') ? path.slice(0, -1) : path\n\n return `${issueCategory(issue._tag)} at ${fieldPath(where)}`\n}\n\n// Default entities, copied from the fixtures.\n\nconst seededQuery = 'synthetic conformance engineer'\n\nconst person01: LinkedInSearchEmulatorResult = {\n title: 'Synthetic Person 01 | Conformance Engineer at Example Synthetic Co',\n url: 'https://linkedin.example.com/in/synthetic-person-01',\n author: 'Synthetic Person 01',\n text: 'Synthetic Person 01. Conformance Engineer at Example Synthetic Co. Synthetic profile text, not a real person.'\n}\n\nconst person02: LinkedInSearchEmulatorResult = {\n title: 'Synthetic Person 02 | Test Engineer at Example Synthetic Co',\n url: 'https://linkedin.example.com/in/synthetic-person-02',\n author: 'Synthetic Person 02',\n publishedDate: '2026-01-15T00:00:00.000Z',\n text: 'Synthetic Person 02. Test Engineer at Example Synthetic Co. Synthetic profile text, not a real person.'\n}\n\nconst person03: LinkedInSearchEmulatorResult = {\n title: 'Synthetic Person 03 | Conformance Lead at Example Synthetic Labs',\n url: 'https://linkedin.example.com/in/synthetic-person-03'\n}\n\n/** The limited search's second result: the fixture records it without `publishedDate`. */\nconst { publishedDate: _limitedOmits, ...person02Limited } = person02\n\nconst defaultSearches: ReadonlyArray<LinkedInSearchEmulatorSearch> = [\n // The people-results fixture (the connector's default numResults).\n { query: seededQuery, numResults: 10, results: [person01, person02, person03] },\n // The num-results-limit fixture: the control, then the limited search.\n { query: seededQuery, numResults: 3, results: [person01, person02, person03] },\n { query: seededQuery, numResults: 2, results: [person01, person02Limited] }\n]\n\nconst defaultProfiles: ReadonlyArray<LinkedInSearchEmulatorProfile> = [\n {\n url: 'https://linkedin.example.com/in/synthetic-person-01',\n publicIdentifier: 'synthetic-person-01',\n fullName: 'Synthetic Person 01',\n headline: 'Conformance Engineer at Example Synthetic Co',\n email: 'synthetic-person-01@example.com'\n }\n]\n\nconst defaultAbsentProfileUrls: ReadonlyArray<string> = [\n 'https://linkedin.example.com/in/synthetic-absent-person-00'\n]\n\n/** The synthetic invalid keys the unauthorized cases send (never account data). */\nexport const linkedInSearchEmulatorDefaultRejectedKeys = {\n exa: ['yolk-conformance-invalid-exa-key'],\n enrichLayer: ['yolk-conformance-invalid-enrich-layer-key']\n} as const\n\ntype SeedParts = {\n readonly exaRejectedKeys: ReadonlyArray<string>\n readonly searches: ReadonlyArray<LinkedInSearchEmulatorSearch>\n readonly enrichLayerRejectedKeys: ReadonlyArray<string>\n readonly profiles: ReadonlyArray<LinkedInSearchEmulatorProfile>\n readonly absentProfileUrls: ReadonlyArray<string>\n}\n\n/** The index of the first value equal to an earlier one, or `-1`. */\nconst repeatIndex = <A>(values: ReadonlyArray<A>): number =>\n values.findIndex((value, index) => values.indexOf(value) !== index)\n\n/**\n * Integrity problems a decoded seed can still have, in constant text only (a category and a field\n * path, never a seeded value: a seed may carry keys anywhere).\n */\nconst seedProblem = (parts: SeedParts): string | undefined => {\n const search = repeatIndex(\n parts.searches.map(entry => JSON.stringify([entry.query, entry.numResults]))\n )\n\n if (search !== -1) return `duplicate search at searches[${search}]`\n\n // No fixture records an empty search, and Exa answers at most numResults results.\n const results = parts.searches.findIndex(\n entry => entry.results.length === 0 || entry.results.length > entry.numResults\n )\n\n if (results !== -1) return `not 1 to numResults results at searches[${results}].results`\n\n const urls = [...parts.profiles.map(profile => profile.url), ...parts.absentProfileUrls]\n const url = repeatIndex(urls)\n\n if (url !== -1) {\n return url < parts.profiles.length\n ? `duplicate profile URL at profiles[${url}].url`\n : `duplicate profile URL at absentProfileUrls[${url - parts.profiles.length}]`\n }\n\n for (const [field, keys] of [\n ['exaRejectedKeys', parts.exaRejectedKeys],\n ['enrichLayerRejectedKeys', parts.enrichLayerRejectedKeys]\n ] as const) {\n const unrecognisable = keys.findIndex(key => !isRecognisableBearerValue(key))\n\n if (unrecognisable !== -1) {\n return `not a recognisable bearer value at ${field}[${unrecognisable}]`\n }\n\n const repeated = repeatIndex(keys)\n\n if (repeated !== -1) return `duplicate key at ${field}[${repeated}]`\n }\n\n return undefined\n}\n\nconst stateFromSeed = (seed: LinkedInSearchEmulatorSeed): LinkedInSearchEmulatorState | string => {\n const parts: SeedParts = {\n exaRejectedKeys: seed.exaRejectedKeys ?? linkedInSearchEmulatorDefaultRejectedKeys.exa,\n searches: seed.searches ?? defaultSearches,\n enrichLayerRejectedKeys:\n seed.enrichLayerRejectedKeys ?? linkedInSearchEmulatorDefaultRejectedKeys.enrichLayer,\n profiles: seed.profiles ?? defaultProfiles,\n absentProfileUrls: seed.absentProfileUrls ?? defaultAbsentProfileUrls\n }\n\n return (\n seedProblem(parts) ?? {\n exaRejectedKeyDigests: parts.exaRejectedKeys.map(key =>\n linkedInSearchKeyDigest(key, linkedInSearchEmulatorExaOrigin)\n ),\n searches: parts.searches,\n enrichLayerRejectedKeyDigests: parts.enrichLayerRejectedKeys.map(key =>\n linkedInSearchKeyDigest(key, linkedInSearchEmulatorEnrichLayerOrigin)\n ),\n profiles: parts.profiles,\n absentProfileUrls: parts.absentProfileUrls\n }\n )\n}\n\n/**\n * Decode and build a seed; a string is the reason it is invalid, in constant text only (a\n * category and a field path; never an input value or a key the seed made up).\n */\nexport const buildSeedState = (input: unknown): LinkedInSearchEmulatorState | string => {\n const decoded = decodeSeedInput(input)\n\n return Result.isFailure(decoded)\n ? issueReason(decoded.failure.issue)\n : stateFromSeed(decoded.success)\n}\n\n/** Decode a full state (a restored snapshot); a string is the reason it is invalid (constant). */\nexport const decodeState = (input: unknown): LinkedInSearchEmulatorState | string => {\n const decoded = decodeStateInput(input)\n\n return Result.isFailure(decoded) ? issueReason(decoded.failure.issue) : decoded.success\n}\n"],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import { EmulatorEvidence, EmulatorRouteEvidence, emulatorEvidenceHeader } from "./route-evidence.mjs";
2
+ import { StatefulCoverage, StatefulEmulatorApi, StatefulFault, StatefulFaultMatch, StatefulFaultState, StatefulInputKind, StatefulLedgerEntry } from "./stateful-emulator.mjs";
3
+ import { LinkedInSearchEmulatorProfile, LinkedInSearchEmulatorResult, LinkedInSearchEmulatorSearch, LinkedInSearchEmulatorSeed, LinkedInSearchEmulatorState, LinkedInSearchEmulatorStateSchema, linkedInSearchEmulatorEnrichLayerOrigin, linkedInSearchEmulatorExaOrigin } from "./linkedin-search/state.mjs";
4
+ import { LinkedInSearchEmulatorDrills, linkedInSearchEmulatorErrorBodies } from "./linkedin-search/api.mjs";
5
+
6
+ //#region src/linkedin-search.d.ts
7
+ /**
8
+ * Route evidence manifest: every emulated route (all reads) and the conformance cases whose
9
+ * (currently synthetic, unverified) wire claims it follows. Kept in sync with the handlers by
10
+ * construction (both come from one route table).
11
+ */
12
+ declare const linkedInSearchEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence>;
13
+ /**
14
+ * Optional fault filter; an omitted field matches every request. `method` is the HTTP method;
15
+ * `path` is the raw request path (ending in `*`, a prefix); `route` is a route template of the
16
+ * manifest (another value is rejected when the fault is added).
17
+ */
18
+ declare const LinkedInSearchFaultMatch: import("effect/Schema").Struct<{
19
+ readonly method: import("effect/Schema").optionalKey<import("effect/Schema").String>;
20
+ readonly path: import("effect/Schema").optionalKey<import("effect/Schema").String>;
21
+ readonly route: import("effect/Schema").optionalKey<import("effect/Schema").String>;
22
+ }>;
23
+ type LinkedInSearchFaultMatch = typeof StatefulFaultMatch.Type;
24
+ /**
25
+ * A status fault (400-599): answer matching requests with this status, headers, and body instead
26
+ * of the route's answer. The body defaults to `{ error: { type: 'emulator_fault', message } }`;
27
+ * `count` limits how many requests it answers. Only a request the emulator would answer reaches a
28
+ * fault: a request that is not emulated, by its shape or by the state, never uses one up.
29
+ */
30
+ declare const LinkedInSearchFault: import("effect/Schema").Struct<{
31
+ readonly kind: import("effect/Schema").Literal<"status">;
32
+ readonly status: import("effect/Schema").Int;
33
+ readonly headers: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").String>>;
34
+ readonly body: import("effect/Schema").optionalKey<import("effect/Schema").Codec<import("effect/Schema").Json, import("effect/Schema").Json, never, never>>;
35
+ readonly match: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
36
+ readonly method: import("effect/Schema").optionalKey<import("effect/Schema").String>;
37
+ readonly path: import("effect/Schema").optionalKey<import("effect/Schema").String>;
38
+ readonly route: import("effect/Schema").optionalKey<import("effect/Schema").String>;
39
+ }>>;
40
+ readonly count: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
41
+ }>;
42
+ type LinkedInSearchFault = StatefulFault;
43
+ type LinkedInSearchFaultState = StatefulFaultState;
44
+ type LinkedInSearchLedgerEntry = StatefulLedgerEntry;
45
+ type LinkedInSearchCoverage = StatefulCoverage;
46
+ declare const LinkedInSearchEmulatorInputInvalid_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
47
+ readonly _tag: "LinkedInSearchEmulatorInputInvalid";
48
+ } & Readonly<A>;
49
+ /** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */
50
+ declare class LinkedInSearchEmulatorInputInvalid extends LinkedInSearchEmulatorInputInvalid_base<{
51
+ readonly input: StatefulInputKind;
52
+ readonly reason: string;
53
+ }> {
54
+ get message(): string;
55
+ }
56
+ type LinkedInSearchEmulatorOptions = {
57
+ /** Typed seed; defaults to the fixture entities. */readonly seed?: LinkedInSearchEmulatorSeed; /** Drill knobs (tests only): make the emulator disagree with one conformance claim. */
58
+ readonly drills?: LinkedInSearchEmulatorDrills;
59
+ };
60
+ type LinkedInSearchEmulator = StatefulEmulatorApi<LinkedInSearchEmulatorState, LinkedInSearchEmulatorSeed>;
61
+ /**
62
+ * Create a stateful LinkedIn search emulator on the `@emulators/core` custom runtime. Each call
63
+ * has its own state, ledger, and faults. Rejects with `LinkedInSearchEmulatorInputInvalid` for an
64
+ * invalid seed or option. See `src/stateful-emulator.ts` for the request precedence.
65
+ */
66
+ declare const makeLinkedInSearchEmulator: (options?: LinkedInSearchEmulatorOptions) => Promise<LinkedInSearchEmulator>;
67
+ //#endregion
68
+ export { type EmulatorEvidence, type EmulatorRouteEvidence, LinkedInSearchCoverage, LinkedInSearchEmulator, type LinkedInSearchEmulatorDrills, LinkedInSearchEmulatorInputInvalid, LinkedInSearchEmulatorOptions, LinkedInSearchEmulatorProfile, LinkedInSearchEmulatorResult, LinkedInSearchEmulatorSearch, LinkedInSearchEmulatorSeed, type LinkedInSearchEmulatorState, LinkedInSearchEmulatorStateSchema, LinkedInSearchFault, LinkedInSearchFaultMatch, LinkedInSearchFaultState, LinkedInSearchLedgerEntry, emulatorEvidenceHeader, linkedInSearchEmulatorEnrichLayerOrigin, linkedInSearchEmulatorErrorBodies, linkedInSearchEmulatorExaOrigin, linkedInSearchEmulatorRoutes, makeLinkedInSearchEmulator };
69
+ //# sourceMappingURL=linkedin-search.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linkedin-search.d.mts","names":[],"sources":["../src/linkedin-search.ts"],"mappings":";;;;;;;;AAuH8E;AAQ9E;;cARa,4BAAA,EAA8B,aAAa,CAAC,qBAAA;;;;;;cAQ5C,wBAAA,0BAAwB,MAAA;EAAA;;;;KAEzB,wBAAA,UAAkC,kBAAA,CAAmB,IAAI;;;AAArE;;;;cAQa,mBAAA,0BAAmB,MAAA;EAAA;;;;;;;;;;;KAEpB,mBAAA,GAAsB,aAAa;AAAA,KAEnC,wBAAA,GAA2B,kBAAkB;AAAA,KAE7C,yBAAA,GAA4B,mBAAmB;AAAA,KAE/C,sBAAA,GAAyB,gBAAgB;AAAA,cAAA,uCAAA;;;;cAGxC,kCAAA,SAA2C,uCAAA;EAAA,SAG7C,KAAA,EAAO,iBAAA;EAAA,SACP,MAAA;AAAA;EAAA,IAEI,OAAA,CAAA;AAAA;AAAA,KAKH,6BAAA;+DAED,IAAA,GAAO,0BAAA;WAEP,MAAA,GAAS,4BAA4B;AAAA;AAAA,KAGpC,sBAAA,GAAyB,mBAAA,CACnC,2BAAA,EACA,0BAAA;;;;;AA7BF;cA8Ca,0BAAA,GACX,OAAA,GAAS,6BAAA,KACR,OAAA,CAAQ,sBAAA"}
@@ -0,0 +1,158 @@
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 { LinkedInSearchEmulatorProfile, LinkedInSearchEmulatorResult, LinkedInSearchEmulatorSearch, LinkedInSearchEmulatorSeed, LinkedInSearchEmulatorStateSchema, buildSeedState, decodeState, linkedInSearchEmulatorEnrichLayerOrigin, linkedInSearchEmulatorExaOrigin, linkedInSearchKeyDigest } from "./linkedin-search/state.mjs";
5
+ import { linkedInSearchApiRoutes, linkedInSearchEmulatorDrillKnobs, linkedInSearchEmulatorErrorBodies } from "./linkedin-search/api.mjs";
6
+ import { Data, Predicate } from "effect";
7
+ //#region src/linkedin-search.ts
8
+ /**
9
+ * Stateful LinkedIn search emulator (the Exa and Enrich Layer routes of the LinkedIn search
10
+ * conformance cases: `POST /search` on `https://api.exa.ai`, and `GET /api/v2/profile` and
11
+ * `GET /api/v2/profile/email` on `https://enrichlayer.com`), built on the upstream
12
+ * `@emulators/core` custom runtime, with a request ledger, status faults, and an `/_emulate/*`
13
+ * control plane.
14
+ *
15
+ * It never imports SDK code: its answers, error bodies, and default seed are copied as data from
16
+ * the synthetic LinkedIn search conformance fixtures, and every route names the conformance cases
17
+ * it follows in `linkedInSearchEmulatorRoutes`. Only the routes those seven cases send are
18
+ * emulated, and every one is a read (nothing is ever written). Response behaviour comes only from
19
+ * the fixtures, byte for byte: a search answers only the results the state holds for exactly its
20
+ * query and `numResults` (the default seed holds the three the fixtures record, for `numResults`
21
+ * 10, 3, and 2); a profile lookup answers a held profile, or the recorded 404 for a seeded absent
22
+ * profile; an email lookup answers a held profile's recorded email. A key a seed marks as rejected
23
+ * on an origin answers that origin's recorded 401 (the default seed rejects the synthetic invalid
24
+ * keys the two unauthorized cases send). Anything else answers one ledgered 400 not-emulated and
25
+ * uses up no fault. Every route answers only on its recorded origin (`fetchOn(origin)` serves one
26
+ * behind a loopback rewrite).
27
+ *
28
+ * Fail closed, on the shared wrapper's fail-closed mode exactly as `/github` and `/google` use it:
29
+ * a request is recognised only when its raw path is exactly an emulated route path under that
30
+ * route's method, and any `Authorization` header is exactly `Bearer <token>` with a recognisable
31
+ * bearer (the RFC 6750 `b64token` syntax, at least 8 characters, a first character in
32
+ * `[G-Zg-z\-._~+/]` other than `n`, `r`, `t`, `u`, and a character outside `[0-9.eE+-]`). Every
33
+ * other request is ledgered and answered with constant text only (`/<unrecognised>`, a standard
34
+ * method or `<other>`, an empty query, no body, a constant reason). A recognised request that
35
+ * repeats the bearer anywhere (its raw path, the raw query or any query key or value, the recorded
36
+ * `content-type` header, or its body, through the wrapper's fixpoint closure of tolerant
37
+ * percent-decoding and JSON-unescaping, capped, where a cap refuses) is ledgered with constant text
38
+ * only; any other has the bearer scrubbed from every ledgered field and reason. The bearer is never
39
+ * stored, forwarded, or ledgered: routes see only its per-origin digest (the wrapper's opt-in
40
+ * `bearerDigest`; SHA-256 of the origin, a space, and the key), which a plan compares with the
41
+ * digests of the seed's rejected keys for that origin; the state holds only those digests, so a key
42
+ * a request carries as its bearer, a rejected one included, never reaches the state or
43
+ * `/_emulate/*` (a key sent as data elsewhere in a request is ledgered like any other text), and
44
+ * seed errors are constant text (a category and a field path) that never quote one. Refusals never
45
+ * echo a request's own query or body keys (the wrapper's constant-reason `exactQuery` and
46
+ * `exactBodyKeys`).
47
+ *
48
+ * Request-shape latitude (`/linkedin-search`, the only accepted deviations): any bearer value in
49
+ * the RFC 6750 `b64token` syntax (`[A-Za-z0-9\-._~+/]+=*`) of at least 8 characters, starting with
50
+ * a character in `[G-Zg-z\-._~+/]` other than `n`, `r`, `t`, `u`, with at least one outside
51
+ * `[0-9.eE+-]`, that occurs nowhere else in the request (never stored or ledgered; only its
52
+ * per-origin digest is compared, with the digests of the keys the seed marks as rejected on that
53
+ * origin); extra request headers; JSON key order; `content-type` media-type parameters on the
54
+ * search; any percent-encoding of the `linkedin_profile_url` value that decodes once to the same
55
+ * profile URL; and, with a key the seed marks as rejected on the request's origin, any search
56
+ * `query` (one trimmed line of at most 500 characters) with any integer `numResults` from 1 to 100,
57
+ * and any profile URL of the form `https://<host>/in/<slug>`, each answered the origin's
58
+ * recorded 401. `Authorization` must be exactly `Bearer <token>` (that spelling, one space).
59
+ * Everything else (other body keys or values, a `category` other than `people`, a `type` other than
60
+ * `auto`, `contents` other than `{ "text": true }`, a search the state holds no answer for (another
61
+ * query, or a `numResults` no seeded search of that query records), a profile URL the state holds
62
+ * neither as a profile nor as absent, an email lookup of an absent profile, any query parameter on
63
+ * the search, other, missing, or repeated query parameters on a lookup, a query parameter name in
64
+ * any but its plain form (such as `%6cinkedin_profile_url`), empty query components such as a bare
65
+ * `?` or a stray `&`, a body on a lookup, another origin, and a bearer repeated anywhere in the
66
+ * request) is not emulated.
67
+ *
68
+ * Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by
69
+ * `makeLinkedInSearchEmulator` (importing this module has no side effects).
70
+ *
71
+ * @experimental
72
+ */
73
+ /**
74
+ * Route evidence manifest: every emulated route (all reads) and the conformance cases whose
75
+ * (currently synthetic, unverified) wire claims it follows. Kept in sync with the handlers by
76
+ * construction (both come from one route table).
77
+ */
78
+ const linkedInSearchEmulatorRoutes = linkedInSearchApiRoutes.map(routeEvidence);
79
+ /**
80
+ * Optional fault filter; an omitted field matches every request. `method` is the HTTP method;
81
+ * `path` is the raw request path (ending in `*`, a prefix); `route` is a route template of the
82
+ * manifest (another value is rejected when the fault is added).
83
+ */
84
+ const LinkedInSearchFaultMatch = StatefulFaultMatch;
85
+ /**
86
+ * A status fault (400-599): answer matching requests with this status, headers, and body instead
87
+ * of the route's answer. The body defaults to `{ error: { type: 'emulator_fault', message } }`;
88
+ * `count` limits how many requests it answers. Only a request the emulator would answer reaches a
89
+ * fault: a request that is not emulated, by its shape or by the state, never uses one up.
90
+ */
91
+ const LinkedInSearchFault = StatefulFault;
92
+ /** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */
93
+ var LinkedInSearchEmulatorInputInvalid = class extends Data.TaggedError("LinkedInSearchEmulatorInputInvalid") {
94
+ get message() {
95
+ return `Invalid LinkedIn search emulator ${this.input}: ${this.reason}`;
96
+ }
97
+ };
98
+ const inputInvalid = (input, reason) => new LinkedInSearchEmulatorInputInvalid({
99
+ input,
100
+ reason
101
+ });
102
+ /** The constant reason of a request on no emulated route shape. */
103
+ const unrecognisedReason = "no emulated Exa or Enrich Layer route for this method and path";
104
+ /** The constant reason of a request whose `Authorization` header is not one recognisable bearer. */
105
+ const unrecognisedAuthorizationReason = "an unrecognisable Authorization header is not emulated";
106
+ /**
107
+ * Create a stateful LinkedIn search emulator on the `@emulators/core` custom runtime. Each call
108
+ * has its own state, ledger, and faults. Rejects with `LinkedInSearchEmulatorInputInvalid` for an
109
+ * invalid seed or option. See `src/stateful-emulator.ts` for the request precedence.
110
+ */
111
+ const makeLinkedInSearchEmulator = async (options = {}) => {
112
+ const initial = buildSeedState(options.seed ?? {});
113
+ if (Predicate.isString(initial)) throw inputInvalid("seed", initial);
114
+ checkBooleanDrills(options.drills, linkedInSearchEmulatorDrillKnobs, inputInvalid);
115
+ const drills = options.drills ?? {};
116
+ return makeStatefulEmulator({
117
+ routes: linkedInSearchApiRoutes,
118
+ env: { drills: {
119
+ defaultSearchWithoutText: drills.defaultSearchWithoutText === true,
120
+ numResultsIgnored: drills.numResultsIgnored === true,
121
+ profileAnswersEmptyObject: drills.profileAnswersEmptyObject === true,
122
+ emailAnswerOmitsEmail: drills.emailAnswerOmitsEmail === true,
123
+ exaUnauthorizedAs5xx: drills.exaUnauthorizedAs5xx === true,
124
+ enrichLayerUnauthorizedAs2xx: drills.enrichLayerUnauthorizedAs2xx === true,
125
+ absentProfileAs2xx: drills.absentProfileAs2xx === true
126
+ } },
127
+ initial,
128
+ buildSeed: buildSeedState,
129
+ recordHeaders: [{
130
+ name: "content-type",
131
+ json: false
132
+ }],
133
+ failClosed: {
134
+ unrecognised: unrecognisedReason,
135
+ unrecognisedAuthorization: unrecognisedAuthorizationReason
136
+ },
137
+ bearerDigest: linkedInSearchKeyDigest,
138
+ clearRuntime: () => void 0,
139
+ runtimeState: () => ({}),
140
+ seedSummary: (state) => ({
141
+ searches: state.searches.length,
142
+ profiles: state.profiles.length,
143
+ absentProfiles: state.absentProfileUrls.length,
144
+ exaRejectedKeys: state.exaRejectedKeyDigests.length,
145
+ enrichLayerRejectedKeys: state.enrichLayerRejectedKeyDigests.length
146
+ }),
147
+ inputInvalid
148
+ }, statefulCoreRuntime({
149
+ name: "linkedin-search",
150
+ initial,
151
+ decodeState,
152
+ inputInvalid
153
+ }));
154
+ };
155
+ //#endregion
156
+ export { LinkedInSearchEmulatorInputInvalid, LinkedInSearchEmulatorProfile, LinkedInSearchEmulatorResult, LinkedInSearchEmulatorSearch, LinkedInSearchEmulatorSeed, LinkedInSearchEmulatorStateSchema, LinkedInSearchFault, LinkedInSearchFaultMatch, emulatorEvidenceHeader, linkedInSearchEmulatorEnrichLayerOrigin, linkedInSearchEmulatorErrorBodies, linkedInSearchEmulatorExaOrigin, linkedInSearchEmulatorRoutes, makeLinkedInSearchEmulator };
157
+
158
+ //# sourceMappingURL=linkedin-search.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linkedin-search.mjs","names":[],"sources":["../src/linkedin-search.ts"],"sourcesContent":["/**\n * Stateful LinkedIn search emulator (the Exa and Enrich Layer routes of the LinkedIn search\n * conformance cases: `POST /search` on `https://api.exa.ai`, and `GET /api/v2/profile` and\n * `GET /api/v2/profile/email` on `https://enrichlayer.com`), built on the upstream\n * `@emulators/core` custom runtime, with a request ledger, status faults, and an `/_emulate/*`\n * control plane.\n *\n * It never imports SDK code: its answers, error bodies, and default seed are copied as data from\n * the synthetic LinkedIn search conformance fixtures, and every route names the conformance cases\n * it follows in `linkedInSearchEmulatorRoutes`. Only the routes those seven cases send are\n * emulated, and every one is a read (nothing is ever written). Response behaviour comes only from\n * the fixtures, byte for byte: a search answers only the results the state holds for exactly its\n * query and `numResults` (the default seed holds the three the fixtures record, for `numResults`\n * 10, 3, and 2); a profile lookup answers a held profile, or the recorded 404 for a seeded absent\n * profile; an email lookup answers a held profile's recorded email. A key a seed marks as rejected\n * on an origin answers that origin's recorded 401 (the default seed rejects the synthetic invalid\n * keys the two unauthorized cases send). Anything else answers one ledgered 400 not-emulated and\n * uses up no fault. Every route answers only on its recorded origin (`fetchOn(origin)` serves one\n * behind a loopback rewrite).\n *\n * Fail closed, on the shared wrapper's fail-closed mode exactly as `/github` and `/google` use it:\n * a request is recognised only when its raw path is exactly an emulated route path under that\n * route's method, and any `Authorization` header is exactly `Bearer <token>` with a recognisable\n * bearer (the RFC 6750 `b64token` syntax, at least 8 characters, a first character in\n * `[G-Zg-z\\-._~+/]` other than `n`, `r`, `t`, `u`, and a character outside `[0-9.eE+-]`). Every\n * other request is ledgered and answered with constant text only (`/<unrecognised>`, a standard\n * method or `<other>`, an empty query, no body, a constant reason). A recognised request that\n * repeats the bearer anywhere (its raw path, the raw query or any query key or value, the recorded\n * `content-type` header, or its body, through the wrapper's fixpoint closure of tolerant\n * percent-decoding and JSON-unescaping, capped, where a cap refuses) is ledgered with constant text\n * only; any other has the bearer scrubbed from every ledgered field and reason. The bearer is never\n * stored, forwarded, or ledgered: routes see only its per-origin digest (the wrapper's opt-in\n * `bearerDigest`; SHA-256 of the origin, a space, and the key), which a plan compares with the\n * digests of the seed's rejected keys for that origin; the state holds only those digests, so a key\n * a request carries as its bearer, a rejected one included, never reaches the state or\n * `/_emulate/*` (a key sent as data elsewhere in a request is ledgered like any other text), and\n * seed errors are constant text (a category and a field path) that never quote one. Refusals never\n * echo a request's own query or body keys (the wrapper's constant-reason `exactQuery` and\n * `exactBodyKeys`).\n *\n * Request-shape latitude (`/linkedin-search`, the only accepted deviations): any bearer value in\n * the RFC 6750 `b64token` syntax (`[A-Za-z0-9\\-._~+/]+=*`) of at least 8 characters, starting with\n * a character in `[G-Zg-z\\-._~+/]` other than `n`, `r`, `t`, `u`, with at least one outside\n * `[0-9.eE+-]`, that occurs nowhere else in the request (never stored or ledgered; only its\n * per-origin digest is compared, with the digests of the keys the seed marks as rejected on that\n * origin); extra request headers; JSON key order; `content-type` media-type parameters on the\n * search; any percent-encoding of the `linkedin_profile_url` value that decodes once to the same\n * profile URL; and, with a key the seed marks as rejected on the request's origin, any search\n * `query` (one trimmed line of at most 500 characters) with any integer `numResults` from 1 to 100,\n * and any profile URL of the form `https://<host>/in/<slug>`, each answered the origin's\n * recorded 401. `Authorization` must be exactly `Bearer <token>` (that spelling, one space).\n * Everything else (other body keys or values, a `category` other than `people`, a `type` other than\n * `auto`, `contents` other than `{ \"text\": true }`, a search the state holds no answer for (another\n * query, or a `numResults` no seeded search of that query records), a profile URL the state holds\n * neither as a profile nor as absent, an email lookup of an absent profile, any query parameter on\n * the search, other, missing, or repeated query parameters on a lookup, a query parameter name in\n * any but its plain form (such as `%6cinkedin_profile_url`), empty query components such as a bare\n * `?` or a stray `&`, a body on a lookup, another origin, and a bearer repeated anywhere in the\n * request) is not emulated.\n *\n * Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by\n * `makeLinkedInSearchEmulator` (importing this module has no side effects).\n *\n * @experimental\n */\nimport { Data, Predicate } from 'effect'\nimport {\n linkedInSearchApiRoutes,\n linkedInSearchEmulatorDrillKnobs,\n type LinkedInSearchApiEnv,\n type LinkedInSearchEmulatorDrills\n} from './linkedin-search/api.ts'\nimport {\n buildSeedState,\n decodeState,\n linkedInSearchKeyDigest,\n type LinkedInSearchEmulatorSeed,\n type LinkedInSearchEmulatorState\n} from './linkedin-search/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 linkedInSearchEmulatorErrorBodies,\n type LinkedInSearchEmulatorDrills\n} from './linkedin-search/api.ts'\n\nexport {\n LinkedInSearchEmulatorProfile,\n LinkedInSearchEmulatorResult,\n LinkedInSearchEmulatorSearch,\n LinkedInSearchEmulatorSeed,\n LinkedInSearchEmulatorStateSchema,\n linkedInSearchEmulatorEnrichLayerOrigin,\n linkedInSearchEmulatorExaOrigin,\n type LinkedInSearchEmulatorState\n} from './linkedin-search/state.ts'\n\n/**\n * Route evidence manifest: every emulated route (all reads) and the conformance cases whose\n * (currently synthetic, unverified) wire claims it follows. Kept in sync with the handlers by\n * construction (both come from one route table).\n */\nexport const linkedInSearchEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> =\n linkedInSearchApiRoutes.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 LinkedInSearchFaultMatch = StatefulFaultMatch\n\nexport type LinkedInSearchFaultMatch = 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 answer. The body defaults to `{ error: { type: 'emulator_fault', message } }`;\n * `count` limits how many requests it answers. Only a request the emulator would answer reaches a\n * fault: a request that is not emulated, by its shape or by the state, never uses one up.\n */\nexport const LinkedInSearchFault = StatefulFault\n\nexport type LinkedInSearchFault = StatefulFault\n\nexport type LinkedInSearchFaultState = StatefulFaultState\n\nexport type LinkedInSearchLedgerEntry = StatefulLedgerEntry\n\nexport type LinkedInSearchCoverage = StatefulCoverage\n\n/** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */\nexport class LinkedInSearchEmulatorInputInvalid extends Data.TaggedError(\n 'LinkedInSearchEmulatorInputInvalid'\n)<{\n readonly input: StatefulInputKind\n readonly reason: string\n}> {\n override get message(): string {\n return `Invalid LinkedIn search emulator ${this.input}: ${this.reason}`\n }\n}\n\nexport type LinkedInSearchEmulatorOptions = {\n /** Typed seed; defaults to the fixture entities. */\n readonly seed?: LinkedInSearchEmulatorSeed\n /** Drill knobs (tests only): make the emulator disagree with one conformance claim. */\n readonly drills?: LinkedInSearchEmulatorDrills\n}\n\nexport type LinkedInSearchEmulator = StatefulEmulatorApi<\n LinkedInSearchEmulatorState,\n LinkedInSearchEmulatorSeed\n>\n\nconst inputInvalid = (input: StatefulInputKind, reason: string) =>\n new LinkedInSearchEmulatorInputInvalid({ input, reason })\n\n/** The constant reason of a request on no emulated route shape. */\nconst unrecognisedReason = 'no emulated Exa or Enrich Layer route for this method and path'\n\n/** The constant reason of a request whose `Authorization` header is not one recognisable bearer. */\nconst unrecognisedAuthorizationReason = 'an unrecognisable Authorization header is not emulated'\n\n/**\n * Create a stateful LinkedIn search emulator on the `@emulators/core` custom runtime. Each call\n * has its own state, ledger, and faults. Rejects with `LinkedInSearchEmulatorInputInvalid` for an\n * invalid seed or option. See `src/stateful-emulator.ts` for the request precedence.\n */\nexport const makeLinkedInSearchEmulator = async (\n options: LinkedInSearchEmulatorOptions = {}\n): Promise<LinkedInSearchEmulator> => {\n const initial = buildSeedState(options.seed ?? {})\n\n if (Predicate.isString(initial)) {\n throw inputInvalid('seed', initial)\n }\n\n checkBooleanDrills(options.drills, linkedInSearchEmulatorDrillKnobs, inputInvalid)\n\n const drills = options.drills ?? {}\n\n const env: LinkedInSearchApiEnv = {\n drills: {\n defaultSearchWithoutText: drills.defaultSearchWithoutText === true,\n numResultsIgnored: drills.numResultsIgnored === true,\n profileAnswersEmptyObject: drills.profileAnswersEmptyObject === true,\n emailAnswerOmitsEmail: drills.emailAnswerOmitsEmail === true,\n exaUnauthorizedAs5xx: drills.exaUnauthorizedAs5xx === true,\n enrichLayerUnauthorizedAs2xx: drills.enrichLayerUnauthorizedAs2xx === true,\n absentProfileAs2xx: drills.absentProfileAs2xx === true\n }\n }\n\n return makeStatefulEmulator<\n LinkedInSearchEmulatorState,\n LinkedInSearchApiEnv,\n LinkedInSearchEmulatorSeed\n >(\n {\n routes: linkedInSearchApiRoutes,\n env,\n initial,\n buildSeed: buildSeedState,\n recordHeaders: [{ name: 'content-type', json: false }],\n failClosed: {\n unrecognised: unrecognisedReason,\n unrecognisedAuthorization: unrecognisedAuthorizationReason\n },\n bearerDigest: linkedInSearchKeyDigest,\n // No cursors or other runtime data.\n clearRuntime: () => undefined,\n runtimeState: () => ({}),\n seedSummary: state => ({\n searches: state.searches.length,\n profiles: state.profiles.length,\n absentProfiles: state.absentProfileUrls.length,\n exaRejectedKeys: state.exaRejectedKeyDigests.length,\n enrichLayerRejectedKeys: state.enrichLayerRejectedKeyDigests.length\n }),\n inputInvalid\n },\n statefulCoreRuntime({ name: 'linkedin-search', initial, decodeState, inputInvalid })\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuHA,MAAa,+BACX,wBAAwB,IAAI,aAAa;;;;;;AAO3C,MAAa,2BAA2B;;;;;;;AAUxC,MAAa,sBAAsB;;AAWnC,IAAa,qCAAb,cAAwD,KAAK,YAC3D,oCACF,EAGG;CACD,IAAa,UAAkB;EAC7B,OAAO,oCAAoC,KAAK,MAAM,IAAI,KAAK;CACjE;AACF;AAcA,MAAM,gBAAgB,OAA0B,WAC9C,IAAI,mCAAmC;CAAE;CAAO;AAAO,CAAC;;AAG1D,MAAM,qBAAqB;;AAG3B,MAAM,kCAAkC;;;;;;AAOxC,MAAa,6BAA6B,OACxC,UAAyC,CAAC,MACN;CACpC,MAAM,UAAU,eAAe,QAAQ,QAAQ,CAAC,CAAC;CAEjD,IAAI,UAAU,SAAS,OAAO,GAC5B,MAAM,aAAa,QAAQ,OAAO;CAGpC,mBAAmB,QAAQ,QAAQ,kCAAkC,YAAY;CAEjF,MAAM,SAAS,QAAQ,UAAU,CAAC;CAclC,OAAO,qBAKL;EACE,QAAQ;EACR,KAAA,EAlBF,QAAQ;GACN,0BAA0B,OAAO,6BAA6B;GAC9D,mBAAmB,OAAO,sBAAsB;GAChD,2BAA2B,OAAO,8BAA8B;GAChE,uBAAuB,OAAO,0BAA0B;GACxD,sBAAsB,OAAO,yBAAyB;GACtD,8BAA8B,OAAO,iCAAiC;GACtE,oBAAoB,OAAO,uBAAuB;EACpD,EAUI;EACF;EACA,WAAW;EACX,eAAe,CAAC;GAAE,MAAM;GAAgB,MAAM;EAAM,CAAC;EACrD,YAAY;GACV,cAAc;GACd,2BAA2B;EAC7B;EACA,cAAc;EAEd,oBAAoB,KAAA;EACpB,qBAAqB,CAAC;EACtB,cAAa,WAAU;GACrB,UAAU,MAAM,SAAS;GACzB,UAAU,MAAM,SAAS;GACzB,gBAAgB,MAAM,kBAAkB;GACxC,iBAAiB,MAAM,sBAAsB;GAC7C,yBAAyB,MAAM,8BAA8B;EAC/D;EACA;CACF,GACA,oBAAoB;EAAE,MAAM;EAAmB;EAAS;EAAa;CAAa,CAAC,CACrF;AACF"}
@@ -0,0 +1,44 @@
1
+ import { StatefulRoute } from "../stateful-emulator.mjs";
2
+ import { McpRecordedFixture } from "./recordings.mjs";
3
+ import { McpEmulatorState, McpRuntime } from "./state.mjs";
4
+ import * as Schema from "effect/Schema";
5
+
6
+ //#region src/mcp/api.d.ts
7
+ /** Drill knobs (tests only): each makes the emulator disagree with exactly one case's claim. */
8
+ type McpEmulatorDrills = {
9
+ /** `mcp.negotiation.era` (modern): the discover answer also carries a JSON-RPC error. */readonly discoverCarriesErrorResponse?: boolean; /** `mcp.modern.stateless`: the modern discover result lacks `resultType`. */
10
+ readonly discoverWithoutResultType?: boolean; /** `mcp.legacy.session`: minted session ids hold a space (not visible ASCII). */
11
+ readonly sessionIdNotVisibleAscii?: boolean; /** `mcp.transport.response-encoding` (modern): the discover answer holds its response twice. */
12
+ readonly discoverAnsweredTwice?: boolean; /** `mcp.tools.list`: the listing marks the write tool `readOnlyHint: true`. */
13
+ readonly writeToolMarkedReadOnly?: boolean; /** `mcp.tools.call-read`: the read call answers `isError: true`. */
14
+ readonly readCallAnswersToolError?: boolean; /** `mcp.tools.call-tool-error`: invalid arguments answer a JSON-RPC error. */
15
+ readonly invalidCallAnswersRpcError?: boolean; /** `mcp.errors.unknown-tool`: the absent tool answers a tool result. */
16
+ readonly absentCallAnswersResult?: boolean; /** `mcp.auth.rejected`: the 401 carries no `WWW-Authenticate` challenge. */
17
+ readonly unauthorizedWithoutChallenge?: boolean;
18
+ };
19
+ declare const mcpEmulatorDrillKnobs: ReadonlyArray<keyof McpEmulatorDrills>;
20
+ type McpApiEnv = {
21
+ readonly drills: Readonly<Record<keyof McpEmulatorDrills, boolean>>; /** The digest of the reserved invalid credential on the emulated origin. */
22
+ readonly reservedDigest: string; /** Runtime data the commits advance (never in the state). */
23
+ readonly runtime: McpRuntime;
24
+ };
25
+ type McpRoute = StatefulRoute<McpEmulatorState, McpApiEnv>;
26
+ type RpcId = string | number;
27
+ /**
28
+ * Where a copied recording is not in the canonical form id substitution relies on: every JSON
29
+ * answer body, and every SSE `data:` payload, must equal `JSON.stringify(JSON.parse(text))`, and
30
+ * every SSE data line must start with `data: `. `makeMcpEmulator` throws on any problem, so a
31
+ * recording copied in another form (escapes such as `\u00e9` or `\/`, numbers such as `1.0`,
32
+ * spacing) fails loudly instead of answering bytes the substitution would rewrite.
33
+ */
34
+ declare const mcpRecordingProblems: (fixtures: ReadonlyArray<McpRecordedFixture>) => ReadonlyArray<string>;
35
+ /**
36
+ * A JSON-RPC request id of the emulated form: an integer from 0 to 2^53 - 1, or 1 to 64 printable
37
+ * ASCII characters.
38
+ */
39
+ declare const isMcpEmulatedRequestId: (id: Schema.Json | undefined) => id is RpcId;
40
+ /** The route table: one wire route per recorded HTTP shape, each with its manifest variants. */
41
+ declare const mcpApiRoutes: ReadonlyArray<McpRoute>;
42
+ //#endregion
43
+ export { McpApiEnv, McpEmulatorDrills, isMcpEmulatedRequestId, mcpApiRoutes, mcpEmulatorDrillKnobs, mcpRecordingProblems };
44
+ //# sourceMappingURL=api.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/mcp/api.ts"],"mappings":";;;;;;;KAqDY,iBAAA;EAAA,kGAED,4BAAA;WAEA,yBAAA,YAFA;EAAA,SAIA,wBAAA;EAAA,SAEA,qBAAA,YAEA;EAAA,SAAA,uBAAA,YAIA;EAAA,SAFA,wBAAA,YAMA;EAAA,SAJA,0BAAA,YAI4B;EAAA,SAF5B,uBAAA,YAeV;EAAA,SAbU,4BAAA;AAAA;AAAA,cAGE,qBAAA,EAAuB,aAAa,OAAO,iBAAA;AAAA,KAY5C,SAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,MAAA,OAAa,iBAAA;WAE9B,cAAA,UAFiB;EAAA,SAIjB,OAAA,EAAS,UAAA;AAAA;AAAA,KAGf,QAAA,GAAW,aAAA,CAAc,gBAAA,EAAkB,SAAA;AAAA,KAE3C,KAAA;;;;;;;;cA8MQ,oBAAA,GACX,QAAA,EAAU,aAAA,CAAc,kBAAA,MACvB,aAAA;AArN2B;AAC7B;;;AAD6B,cAySjB,sBAAA,GAA0B,EAAA,EAAI,MAAA,CAAO,IAAA,iBAAmB,EAAA,IAAM,KAEf;;cA4lB/C,YAAA,EAAc,aAAa,CAAC,QAAA"}