@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,264 @@
1
+ import { Predicate, Result } from "effect";
2
+ import * as Schema from "effect/Schema";
3
+ //#region src/microsoft/graph.ts
4
+ /**
5
+ * Shared Microsoft Graph emulator wire helpers (internal): the error envelope and codes, JSON
6
+ * responses, `@odata.context`, `$select` projection, `$top`/`$skip` paging with
7
+ * `@odata.nextLink` (folder messages only), single-page `$top` collections (`$top` always required
8
+ * and capped at the largest value a fixture sends to the route), the `Prefer` header, strict nested
9
+ * body objects, and the route handler contract.
10
+ *
11
+ * @experimental
12
+ */
13
+ /**
14
+ * Error codes the emulator answers with. `ErrorItemNotFound`, `itemNotFound`, and
15
+ * `ErrorIrresolvableConflict` are copied from the fixtures; `InvalidAuthenticationToken` (missing
16
+ * bearer), `ErrorInvalidUser` (a user other than the seeded mailbox), and `TooManyRequests` (the
17
+ * default 429 fault body) are documented Graph codes no fixture records; the `Synthetic*` codes
18
+ * are emulator codes for requests it refuses to emulate (fail closed) or cannot answer.
19
+ */
20
+ const microsoftEmulatorErrorCodes = {
21
+ mailItemNotFound: "ErrorItemNotFound",
22
+ driveItemNotFound: "itemNotFound",
23
+ conflict: "ErrorIrresolvableConflict",
24
+ unauthenticated: "InvalidAuthenticationToken",
25
+ invalidUser: "ErrorInvalidUser",
26
+ rateLimited: "TooManyRequests",
27
+ unknownRoute: "SyntheticRouteNotEmulated",
28
+ unsupportedQuery: "SyntheticQueryNotEmulated",
29
+ unsupportedValue: "SyntheticValueNotEmulated",
30
+ invalidRequest: "SyntheticInvalidRequest",
31
+ invalidBody: "SyntheticInvalidBody",
32
+ upstreamError: "SyntheticUpstreamError"
33
+ };
34
+ const codes = microsoftEmulatorErrorCodes;
35
+ /** Graph JSON content type, as the fixtures record it. */
36
+ const graphContentType = "application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8";
37
+ const jsonResponse = (status, body, headers = {}) => {
38
+ const responseHeaders = new Headers(headers);
39
+ if (!responseHeaders.has("content-type")) responseHeaders.set("content-type", graphContentType);
40
+ return new Response(JSON.stringify(body), {
41
+ status,
42
+ headers: responseHeaders
43
+ });
44
+ };
45
+ /** Graph API version prefix of every Graph route. */
46
+ const microsoftEmulatorBasePath = "/v1.0";
47
+ /** A percent-decoded path segment, or `undefined` for invalid percent-encoding. */
48
+ const decodeSegment = (segment) => {
49
+ try {
50
+ return decodeURIComponent(segment);
51
+ } catch {
52
+ return;
53
+ }
54
+ };
55
+ /**
56
+ * An OData key segment as the fixtures write it in `@odata.context`: `('ada%40example.test')`,
57
+ * `('b%21synthetic-drive-0001')` (percent-encoded, including `!'()*`).
58
+ */
59
+ const odataKey = (value) => `('${encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`)}')`;
60
+ /** `(a,b,c)` for a `$select`, empty without one, as the fixture contexts write it. */
61
+ const selectSuffix = (fields) => fields === void 0 ? "" : `(${fields.join(",")})`;
62
+ /** The fixture `@odata.context` URL: `{graphOrigin}/v1.0/$metadata#{path}`. */
63
+ const metadataContext = (env, path) => `${env.graphOrigin}${microsoftEmulatorBasePath}/$metadata#${path}`;
64
+ /** An entity body with its `@odata.context` first, as the fixtures record it. */
65
+ const entity = (context, body) => ({
66
+ "@odata.context": context,
67
+ ...body
68
+ });
69
+ /** A bodiless success (204 delete, 202 cancel/copy), as the fixtures record them. */
70
+ const emptyResponse = (status, headers = {}) => new Response(null, {
71
+ status,
72
+ headers
73
+ });
74
+ /** The Graph error envelope `{ error: { code, message, innerError } }`. */
75
+ const graphError = (context, status, code, message) => jsonResponse(status, { error: {
76
+ code,
77
+ message,
78
+ innerError: {
79
+ date: context.date,
80
+ "request-id": context.requestId,
81
+ "client-request-id": context.clientRequestId
82
+ }
83
+ } });
84
+ const padded = (value, width) => String(value).padStart(width, "0");
85
+ /** A synthetic request id for ledger sequence `seq`. */
86
+ const syntheticRequestId = (seq) => `00000000-0000-4000-8000-${padded(seq % 0xe8d4a51000, 12)}`;
87
+ const clientRequestIdPattern = /^[A-Za-z0-9-]{1,64}$/;
88
+ /** The error context of one request; `client-request-id` is echoed only when it is plain. */
89
+ const errorContext = (now, seq, clientRequestId) => {
90
+ const requestId = syntheticRequestId(seq);
91
+ return {
92
+ date: (/* @__PURE__ */ new Date(Math.floor(now / 1e3) * 1e3)).toISOString().slice(0, 19),
93
+ requestId,
94
+ clientRequestId: clientRequestId !== null && clientRequestIdPattern.test(clientRequestId) ? clientRequestId : requestId
95
+ };
96
+ };
97
+ const unquote = (value) => value.length >= 2 && value.startsWith("\"") && value.endsWith("\"") ? value.slice(1, -1) : value;
98
+ /** Parse `Prefer`: comma-separated `name=value` preferences. */
99
+ const parsePreferences = (header) => {
100
+ let immutableId = false;
101
+ let timezone;
102
+ for (const part of (header ?? "").split(",")) {
103
+ const separator = part.indexOf("=");
104
+ const name = (separator === -1 ? part : part.slice(0, separator)).trim().toLowerCase();
105
+ const value = separator === -1 ? "" : unquote(part.slice(separator + 1).trim());
106
+ if (name === "idtype" && value.toLowerCase() === "immutableid") immutableId = true;
107
+ if (name === "outlook.timezone") timezone = value;
108
+ }
109
+ return {
110
+ immutableId,
111
+ timezone
112
+ };
113
+ };
114
+ const isJsonObject = (value) => value !== void 0 && value !== null && Predicate.isObject(value) && !Array.isArray(value);
115
+ const decodeJsonText = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Json));
116
+ /** Parsed JSON, or `undefined` for invalid JSON. */
117
+ const parseJsonText = (text) => {
118
+ const result = decodeJsonText(text);
119
+ return Result.isSuccess(result) ? result.success : void 0;
120
+ };
121
+ /** The user a `/users/{userId}` segment names (id, `mail`, or UPN, case-insensitive). */
122
+ const resolveUser = (state, request) => {
123
+ const userId = (request.params.userId ?? "").toLowerCase();
124
+ const user = state.user;
125
+ return [
126
+ user.id,
127
+ user.mail,
128
+ user.userPrincipalName
129
+ ].some((candidate) => candidate.toLowerCase() === userId) ? user : request.error(404, codes.invalidUser, "Synthetic: the requested user is not emulated.");
130
+ };
131
+ /** The `$select` fields, `undefined` without `$select`, or a 400 for a field not emulated. */
132
+ const selectedFields = (request, allowed) => {
133
+ const raw = request.query.get("$select");
134
+ if (raw === null) return void 0;
135
+ const fields = raw.split(",").map((field) => field.trim());
136
+ const unknown = fields.find((field) => !allowed.includes(field));
137
+ return unknown === void 0 ? fields : request.error(400, codes.unsupportedQuery, `Synthetic: $select field '${unknown}' is not emulated on this resource.`);
138
+ };
139
+ /** Annotations and the id are always returned, whatever `$select` names. */
140
+ const alwaysReturned = [
141
+ "@odata.etag",
142
+ "@odata.type",
143
+ "@odata.mediaContentType",
144
+ "id"
145
+ ];
146
+ /** Keep only the selected keys (plus annotations and `id`); everything without `$select`. */
147
+ const project = (full, fields) => fields === void 0 ? full : Object.fromEntries(Object.entries(full).filter(([key]) => alwaysReturned.includes(key) || fields.includes(key)));
148
+ const integerQuery = (request, key, minimum, maximum) => {
149
+ const raw = request.query.get(key);
150
+ if (raw === null) return void 0;
151
+ const value = Number(raw);
152
+ return /^\d+$/.test(raw) && value >= minimum && value <= maximum ? value : request.error(400, codes.invalidRequest, `Synthetic: ${key} must be an integer from ${minimum} to ${maximum}.`);
153
+ };
154
+ /**
155
+ * The required `$top` of a collection whose fixtures always send it: a positive integer (400
156
+ * invalid otherwise), and 400 not emulated when it is missing or above `maxTop`, the largest
157
+ * `$top` any fixture sends to that route.
158
+ */
159
+ const requiredTop = (request, maxTop) => {
160
+ const top = integerQuery(request, "$top", 1, Number.MAX_SAFE_INTEGER);
161
+ if (top instanceof Response) return top;
162
+ if (top === void 0) return notEmulated(request, "requests without $top are not emulated on this collection.");
163
+ return top > maxTop ? notEmulated(request, `$top above ${maxTop} (the largest page size a fixture sends) is not emulated on this collection.`) : top;
164
+ };
165
+ /**
166
+ * One `$top`/`$skip` page of `items`. `$top` is required and at most `maxTop` (see
167
+ * `requiredTop`); no fixture records a default page size.
168
+ */
169
+ const pageOf = (items, request, maxTop) => {
170
+ const top = requiredTop(request, maxTop);
171
+ if (top instanceof Response) return top;
172
+ const skip = integerQuery(request, "$skip", 0, Number.MAX_SAFE_INTEGER);
173
+ if (skip instanceof Response) return skip;
174
+ const start = skip ?? 0;
175
+ const end = start + top;
176
+ return {
177
+ items: items.slice(start, end),
178
+ top,
179
+ nextSkip: end < items.length ? end : void 0
180
+ };
181
+ };
182
+ /**
183
+ * A collection whose fixtures send `$top` and never page (calendar views, children listings):
184
+ * the items, or 400 not emulated without `$top`, with `$top` above `maxTop` (see `requiredTop`),
185
+ * or with more items than `$top` (no fixture records a default page size or a second page of
186
+ * these collections).
187
+ */
188
+ const singlePage = (items, request, maxTop) => {
189
+ const top = requiredTop(request, maxTop);
190
+ if (top instanceof Response) return top;
191
+ return items.length > top ? notEmulated(request, `more than $top (${top}) results: paging this collection is not emulated.`) : items;
192
+ };
193
+ /**
194
+ * The `@odata.nextLink` of a page: the configured Graph origin, the request's raw path (so
195
+ * `/users/ada%40example.test` keeps its `%40`), the carried query keys, then `$top` and `$skip`,
196
+ * encoded like the fixtures (`%24select=id%2Csubject...`). `undefined` on the last page or when
197
+ * the `omitNextLink` drill knob is set. Clients follow it unchanged (opaque).
198
+ */
199
+ const nextLinkOf = (env, request, page, carried) => {
200
+ if (page.nextSkip === void 0 || env.drills.omitNextLink) return void 0;
201
+ const params = new URLSearchParams();
202
+ for (const key of carried) {
203
+ const value = request.query.get(key);
204
+ if (value !== null) params.set(key, value);
205
+ }
206
+ params.set("$top", String(page.top));
207
+ params.set("$skip", String(page.nextSkip));
208
+ return `${env.graphOrigin}${request.path}?${params.toString()}`;
209
+ };
210
+ /** `{ @odata.context, value, @odata.nextLink? }`, as the fixture pages record them. */
211
+ const collection = (context, value, nextLink) => nextLink === void 0 ? {
212
+ "@odata.context": context,
213
+ value
214
+ } : {
215
+ "@odata.context": context,
216
+ value,
217
+ "@odata.nextLink": nextLink
218
+ };
219
+ /** A write body that is a JSON object with only `allowed` keys, or a 400. */
220
+ const bodyObject = (request, allowed) => {
221
+ if (!isJsonObject(request.body)) return request.error(400, codes.invalidBody, "Synthetic: the request body must be a JSON object.");
222
+ const unknown = Object.keys(request.body).find((key) => !allowed.includes(key));
223
+ return unknown === void 0 ? request.body : request.error(400, codes.unsupportedValue, `Synthetic: property '${unknown}' is not emulated on this request.`);
224
+ };
225
+ /** 400 for an invalid value. */
226
+ const invalidValue = (request, message) => request.error(400, codes.invalidRequest, `Synthetic: ${message}`);
227
+ /** 400 for a valid Graph request the emulator does not emulate (fail closed). */
228
+ const notEmulated = (request, message) => request.error(400, codes.unsupportedValue, `Synthetic: ${message}`);
229
+ /**
230
+ * A nested JSON object with only `allowed` keys (fail closed on anything else, before any write),
231
+ * or a 400. `expected` names the expected object in the error message.
232
+ */
233
+ const nestedObject = (request, value, allowed, expected) => {
234
+ if (!isJsonObject(value)) return invalidValue(request, `expected ${expected}.`);
235
+ const unknown = Object.keys(value).find((key) => !allowed.includes(key));
236
+ return unknown === void 0 ? value : notEmulated(request, `property '${unknown}' is not emulated in ${expected}.`);
237
+ };
238
+ /**
239
+ * An `itemBody` write value, `{ contentType, content }` with only those keys. The fixtures write
240
+ * text bodies only (`Text`/`text`); any other content type is not emulated.
241
+ */
242
+ const textBody = (request, value) => {
243
+ const body = nestedObject(request, value, ["contentType", "content"], "body { contentType, content }");
244
+ if (body instanceof Response) return body;
245
+ if (!Predicate.isString(body.contentType) || !Predicate.isString(body.content)) return invalidValue(request, "body must be { contentType, content } with string values.");
246
+ return body.contentType.toLowerCase() === "text" ? body.content : notEmulated(request, `body contentType ${body.contentType} is not emulated (text only).`);
247
+ };
248
+ /** A Graph timestamp (`2026-09-29T10:00:00Z`) from the emulator clock. */
249
+ const nowTimestamp = (env) => (/* @__PURE__ */ new Date(Math.floor(env.now() / 1e3) * 1e3)).toISOString().replace(".000Z", "Z");
250
+ /** The next change key (bumps the counter). */
251
+ const nextChangeKey = (state, prefix) => {
252
+ const number = state.counters.nextChangeKeyNumber;
253
+ state.counters = {
254
+ ...state.counters,
255
+ nextChangeKeyNumber: number + 1
256
+ };
257
+ return `${prefix}${padded(number, 4)}`;
258
+ };
259
+ /** The `personal/{site}` segment of the mailbox owner (`ada@example.test` → `ada_example_test`). */
260
+ const personalSite = (user) => user.mail.toLowerCase().replace(/[^a-z0-9]/g, "_");
261
+ //#endregion
262
+ export { bodyObject, codes, collection, decodeSegment, emptyResponse, entity, errorContext, graphContentType, graphError, invalidValue, isJsonObject, jsonResponse, metadataContext, microsoftEmulatorBasePath, microsoftEmulatorErrorCodes, nestedObject, nextChangeKey, nextLinkOf, notEmulated, nowTimestamp, odataKey, padded, pageOf, parseJsonText, parsePreferences, personalSite, project, resolveUser, selectSuffix, selectedFields, singlePage, syntheticRequestId, textBody };
263
+
264
+ //# sourceMappingURL=graph.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.mjs","names":[],"sources":["../../src/microsoft/graph.ts"],"sourcesContent":["/**\n * Shared Microsoft Graph emulator wire helpers (internal): the error envelope and codes, JSON\n * responses, `@odata.context`, `$select` projection, `$top`/`$skip` paging with\n * `@odata.nextLink` (folder messages only), single-page `$top` collections (`$top` always required\n * and capped at the largest value a fixture sends to the route), the `Prefer` header, strict nested\n * body objects, and the route handler contract.\n *\n * @experimental\n */\nimport { Predicate, Result } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport type { MicrosoftEmulatorState, MicrosoftEmulatorUser } from './state.ts'\n\n/**\n * Error codes the emulator answers with. `ErrorItemNotFound`, `itemNotFound`, and\n * `ErrorIrresolvableConflict` are copied from the fixtures; `InvalidAuthenticationToken` (missing\n * bearer), `ErrorInvalidUser` (a user other than the seeded mailbox), and `TooManyRequests` (the\n * default 429 fault body) are documented Graph codes no fixture records; the `Synthetic*` codes\n * are emulator codes for requests it refuses to emulate (fail closed) or cannot answer.\n */\nexport const microsoftEmulatorErrorCodes = {\n mailItemNotFound: 'ErrorItemNotFound',\n driveItemNotFound: 'itemNotFound',\n conflict: 'ErrorIrresolvableConflict',\n unauthenticated: 'InvalidAuthenticationToken',\n invalidUser: 'ErrorInvalidUser',\n rateLimited: 'TooManyRequests',\n unknownRoute: 'SyntheticRouteNotEmulated',\n unsupportedQuery: 'SyntheticQueryNotEmulated',\n unsupportedValue: 'SyntheticValueNotEmulated',\n invalidRequest: 'SyntheticInvalidRequest',\n invalidBody: 'SyntheticInvalidBody',\n upstreamError: 'SyntheticUpstreamError'\n} as const\n\nexport const codes = microsoftEmulatorErrorCodes\n\n/** Graph JSON content type, as the fixtures record it. */\nexport const graphContentType =\n 'application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8'\n\nexport const jsonResponse = (\n status: number,\n body: unknown,\n headers: HeadersInit = {}\n): Response => {\n const responseHeaders = new Headers(headers)\n\n if (!responseHeaders.has('content-type')) responseHeaders.set('content-type', graphContentType)\n\n return new Response(JSON.stringify(body), { status, headers: responseHeaders })\n}\n\n/** Graph API version prefix of every Graph route. */\nexport const microsoftEmulatorBasePath = '/v1.0'\n\n/** A percent-decoded path segment, or `undefined` for invalid percent-encoding. */\nexport const decodeSegment = (segment: string): string | undefined => {\n try {\n return decodeURIComponent(segment)\n } catch {\n return undefined\n }\n}\n\n/**\n * An OData key segment as the fixtures write it in `@odata.context`: `('ada%40example.test')`,\n * `('b%21synthetic-drive-0001')` (percent-encoded, including `!'()*`).\n */\nexport const odataKey = (value: string): string =>\n `('${encodeURIComponent(value).replace(\n /[!'()*]/g,\n character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`\n )}')`\n\n/** `(a,b,c)` for a `$select`, empty without one, as the fixture contexts write it. */\nexport const selectSuffix = (fields: ReadonlyArray<string> | undefined): string =>\n fields === undefined ? '' : `(${fields.join(',')})`\n\n/** The fixture `@odata.context` URL: `{graphOrigin}/v1.0/$metadata#{path}`. */\nexport const metadataContext = (env: MicrosoftApiEnv, path: string): string =>\n `${env.graphOrigin}${microsoftEmulatorBasePath}/$metadata#${path}`\n\n/** An entity body with its `@odata.context` first, as the fixtures record it. */\nexport const entity = (context: string, body: Schema.JsonObject): Schema.JsonObject => ({\n '@odata.context': context,\n ...body\n})\n\n/** A bodiless success (204 delete, 202 cancel/copy), as the fixtures record them. */\nexport const emptyResponse = (status: number, headers: HeadersInit = {}): Response =>\n new Response(null, { status, headers })\n\n/** Where a Graph error envelope's `innerError` values come from. */\nexport type ErrorContext = {\n /** `innerError.date` (`YYYY-MM-DDTHH:MM:SS`, UTC). */\n readonly date: string\n readonly requestId: string\n readonly clientRequestId: string\n}\n\n/** The Graph error envelope `{ error: { code, message, innerError } }`. */\nexport const graphError = (\n context: ErrorContext,\n status: number,\n code: string,\n message: string\n): Response =>\n jsonResponse(status, {\n error: {\n code,\n message,\n innerError: {\n date: context.date,\n 'request-id': context.requestId,\n 'client-request-id': context.clientRequestId\n }\n }\n })\n\nexport const padded = (value: number, width: number): string => String(value).padStart(width, '0')\n\n/** A synthetic request id for ledger sequence `seq`. */\nexport const syntheticRequestId = (seq: number): string =>\n `00000000-0000-4000-8000-${padded(seq % 1_000_000_000_000, 12)}`\n\nconst clientRequestIdPattern = /^[A-Za-z0-9-]{1,64}$/\n\n/** The error context of one request; `client-request-id` is echoed only when it is plain. */\nexport const errorContext = (\n now: number,\n seq: number,\n clientRequestId: string | null\n): ErrorContext => {\n const requestId = syntheticRequestId(seq)\n\n return {\n date: new Date(Math.floor(now / 1000) * 1000).toISOString().slice(0, 19),\n requestId,\n clientRequestId:\n clientRequestId !== null && clientRequestIdPattern.test(clientRequestId)\n ? clientRequestId\n : requestId\n }\n}\n\n/** The parsed `Prefer` preferences the emulator reads (others are ignored, as Graph does). */\nexport type Preferences = {\n /** `IdType=\"ImmutableId\"`: answer message ids in immutable form. */\n readonly immutableId: boolean\n /** `outlook.timezone`, when sent. */\n readonly timezone: string | undefined\n}\n\nconst unquote = (value: string): string =>\n value.length >= 2 && value.startsWith('\"') && value.endsWith('\"') ? value.slice(1, -1) : value\n\n/** Parse `Prefer`: comma-separated `name=value` preferences. */\nexport const parsePreferences = (header: string | null): Preferences => {\n let immutableId = false\n let timezone: string | undefined\n\n for (const part of (header ?? '').split(',')) {\n const separator = part.indexOf('=')\n const name = (separator === -1 ? part : part.slice(0, separator)).trim().toLowerCase()\n const value = separator === -1 ? '' : unquote(part.slice(separator + 1).trim())\n\n if (name === 'idtype' && value.toLowerCase() === 'immutableid') immutableId = true\n\n if (name === 'outlook.timezone') timezone = value\n }\n\n return { immutableId, timezone }\n}\n\n/** One routed API request, as the handlers see it. */\nexport type RouteRequest = {\n /** Decoded path parameters. */\n readonly params: Readonly<Record<string, string>>\n /** Raw (still percent-encoded) path, for example `/v1.0/users/ada%40example.test/...`. */\n readonly path: string\n readonly query: URLSearchParams\n /** Parsed JSON body; `undefined` when absent. */\n readonly body: Schema.Json | undefined\n readonly prefer: Preferences\n /** A Graph error envelope response for this request. */\n readonly error: (status: number, code: string, message: string) => Response\n}\n\n/** Drill knobs (tests only): each makes the emulator disagree with one conformance claim. */\nexport type MicrosoftEmulatorDrills = {\n /** `true`: `calendarView` answers an empty `value` for every range. */\n readonly calendarRangeEmpty?: boolean\n /** `true`: event create answers 201 without the new event's `id` (the event is still created). */\n readonly createOmitsId?: boolean\n /** Fractional digits of event `dateTime` values (Graph uses 7); `3` drills the precision case. */\n readonly timestampPrecisionDigits?: number\n /** `true`: folder message pages never carry `@odata.nextLink`. */\n readonly omitNextLink?: boolean\n}\n\n/** A OneDrive copy the monitor reports on (runtime-only, like the ledger; not in the state). */\nexport type CopyMonitor = {\n readonly id: string\n readonly sourceId: string\n readonly destinationParentId: string\n /** In-progress answers left before the copy runs and completes. */\n pollsLeft: number\n /** Set once the copy ran and completed. */\n resourceId: string | undefined\n}\n\nexport type MicrosoftApiEnv = {\n /** Clock in epoch milliseconds (timestamps of created items). */\n readonly now: () => number\n /** Origin of `@odata.nextLink` values, for example `https://graph.microsoft.com`. */\n readonly graphOrigin: string\n /** Origin of OneDrive `webUrl` values and copy monitor URLs. */\n readonly sharePointOrigin: string\n readonly drills: Required<MicrosoftEmulatorDrills>\n /** In-progress monitor answers before a copy completes (default 0: the first poll completes). */\n readonly copyInProgressPolls: number\n /** How long a message write holds its message; a concurrent write in that time loses (409). */\n readonly conflictWindowMs: number\n /** Immutable ids of messages with a write in flight (runtime-only). */\n readonly messageLocks: Set<string>\n /** Copy monitors by id (runtime-only; cleared by reset and seed). */\n readonly monitors: Map<string, CopyMonitor>\n readonly monitorCounter: { next: number }\n}\n\n/**\n * A message write's answer and the message it holds once committed: the write holds it for\n * `conflictWindowMs` before answering, so an overlapping write to it gets 409.\n */\nexport type HeldAnswer = {\n readonly response: Response\n /** Immutable id of the message held. */\n readonly holds: string\n}\n\n/**\n * A route handler. It runs on a draft of the state and of the copy monitors (see\n * `registerMicrosoftApi`) and decides the whole answer synchronously; it replaces whole state lists\n * and counters, never editing them in place. Any answer that is not 2xx is a refusal.\n */\nexport type RouteHandler = (\n state: MicrosoftEmulatorState,\n request: RouteRequest,\n env: MicrosoftApiEnv\n) => Response | HeldAnswer\n\nexport const isJsonObject = (value: Schema.Json | undefined): value is Schema.JsonObject =>\n value !== undefined && value !== null && Predicate.isObject(value) && !Array.isArray(value)\n\nconst decodeJsonText = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Json))\n\n/** Parsed JSON, or `undefined` for invalid JSON. */\nexport const parseJsonText = (text: string): Schema.Json | undefined => {\n const result = decodeJsonText(text)\n\n return Result.isSuccess(result) ? result.success : undefined\n}\n\n/** The user a `/users/{userId}` segment names (id, `mail`, or UPN, case-insensitive). */\nexport const resolveUser = (\n state: MicrosoftEmulatorState,\n request: RouteRequest\n): MicrosoftEmulatorUser | Response => {\n const userId = (request.params.userId ?? '').toLowerCase()\n const user = state.user\n\n return [user.id, user.mail, user.userPrincipalName].some(\n candidate => candidate.toLowerCase() === userId\n )\n ? user\n : request.error(404, codes.invalidUser, 'Synthetic: the requested user is not emulated.')\n}\n\n/** The `$select` fields, `undefined` without `$select`, or a 400 for a field not emulated. */\nexport const selectedFields = (\n request: RouteRequest,\n allowed: ReadonlyArray<string>\n): ReadonlyArray<string> | undefined | Response => {\n const raw = request.query.get('$select')\n\n if (raw === null) return undefined\n\n const fields = raw.split(',').map(field => field.trim())\n const unknown = fields.find(field => !allowed.includes(field))\n\n return unknown === undefined\n ? fields\n : request.error(\n 400,\n codes.unsupportedQuery,\n `Synthetic: $select field '${unknown}' is not emulated on this resource.`\n )\n}\n\n/** Annotations and the id are always returned, whatever `$select` names. */\nconst alwaysReturned: ReadonlyArray<string> = [\n '@odata.etag',\n '@odata.type',\n '@odata.mediaContentType',\n 'id'\n]\n\n/** Keep only the selected keys (plus annotations and `id`); everything without `$select`. */\nexport const project = (\n full: Schema.JsonObject,\n fields: ReadonlyArray<string> | undefined\n): Schema.JsonObject =>\n fields === undefined\n ? full\n : Object.fromEntries(\n Object.entries(full).filter(([key]) => alwaysReturned.includes(key) || fields.includes(key))\n )\n\nconst integerQuery = (\n request: RouteRequest,\n key: string,\n minimum: number,\n maximum: number\n): number | undefined | Response => {\n const raw = request.query.get(key)\n\n if (raw === null) return undefined\n\n const value = Number(raw)\n\n return /^\\d+$/.test(raw) && value >= minimum && value <= maximum\n ? value\n : request.error(\n 400,\n codes.invalidRequest,\n `Synthetic: ${key} must be an integer from ${minimum} to ${maximum}.`\n )\n}\n\n/**\n * The required `$top` of a collection whose fixtures always send it: a positive integer (400\n * invalid otherwise), and 400 not emulated when it is missing or above `maxTop`, the largest\n * `$top` any fixture sends to that route.\n */\nconst requiredTop = (request: RouteRequest, maxTop: number): number | Response => {\n const top = integerQuery(request, '$top', 1, Number.MAX_SAFE_INTEGER)\n\n if (top instanceof Response) return top\n\n if (top === undefined) {\n return notEmulated(request, 'requests without $top are not emulated on this collection.')\n }\n\n return top > maxTop\n ? notEmulated(\n request,\n `$top above ${maxTop} (the largest page size a fixture sends) is not emulated on this collection.`\n )\n : top\n}\n\nexport type Page<A> = {\n readonly items: ReadonlyArray<A>\n readonly top: number\n /** `$skip` of the next page, when there is one. */\n readonly nextSkip: number | undefined\n}\n\n/**\n * One `$top`/`$skip` page of `items`. `$top` is required and at most `maxTop` (see\n * `requiredTop`); no fixture records a default page size.\n */\nexport const pageOf = <A>(\n items: ReadonlyArray<A>,\n request: RouteRequest,\n maxTop: number\n): Page<A> | Response => {\n const top = requiredTop(request, maxTop)\n\n if (top instanceof Response) return top\n\n const skip = integerQuery(request, '$skip', 0, Number.MAX_SAFE_INTEGER)\n\n if (skip instanceof Response) return skip\n\n const start = skip ?? 0\n const end = start + top\n\n return {\n items: items.slice(start, end),\n top,\n nextSkip: end < items.length ? end : undefined\n }\n}\n\n/**\n * A collection whose fixtures send `$top` and never page (calendar views, children listings):\n * the items, or 400 not emulated without `$top`, with `$top` above `maxTop` (see `requiredTop`),\n * or with more items than `$top` (no fixture records a default page size or a second page of\n * these collections).\n */\nexport const singlePage = <A>(\n items: ReadonlyArray<A>,\n request: RouteRequest,\n maxTop: number\n): ReadonlyArray<A> | Response => {\n const top = requiredTop(request, maxTop)\n\n if (top instanceof Response) return top\n\n return items.length > top\n ? notEmulated(\n request,\n `more than $top (${top}) results: paging this collection is not emulated.`\n )\n : items\n}\n\n/**\n * The `@odata.nextLink` of a page: the configured Graph origin, the request's raw path (so\n * `/users/ada%40example.test` keeps its `%40`), the carried query keys, then `$top` and `$skip`,\n * encoded like the fixtures (`%24select=id%2Csubject...`). `undefined` on the last page or when\n * the `omitNextLink` drill knob is set. Clients follow it unchanged (opaque).\n */\nexport const nextLinkOf = (\n env: MicrosoftApiEnv,\n request: RouteRequest,\n page: Page<unknown>,\n carried: ReadonlyArray<string>\n): string | undefined => {\n if (page.nextSkip === undefined || env.drills.omitNextLink) return undefined\n\n const params = new URLSearchParams()\n\n for (const key of carried) {\n const value = request.query.get(key)\n\n if (value !== null) params.set(key, value)\n }\n\n params.set('$top', String(page.top))\n params.set('$skip', String(page.nextSkip))\n\n return `${env.graphOrigin}${request.path}?${params.toString()}`\n}\n\n/** `{ @odata.context, value, @odata.nextLink? }`, as the fixture pages record them. */\nexport const collection = (\n context: string,\n value: ReadonlyArray<unknown>,\n nextLink: string | undefined\n) =>\n nextLink === undefined\n ? { '@odata.context': context, value }\n : { '@odata.context': context, value, '@odata.nextLink': nextLink }\n\n/** A write body that is a JSON object with only `allowed` keys, or a 400. */\nexport const bodyObject = (\n request: RouteRequest,\n allowed: ReadonlyArray<string>\n): Schema.JsonObject | Response => {\n if (!isJsonObject(request.body)) {\n return request.error(\n 400,\n codes.invalidBody,\n 'Synthetic: the request body must be a JSON object.'\n )\n }\n\n const unknown = Object.keys(request.body).find(key => !allowed.includes(key))\n\n return unknown === undefined\n ? request.body\n : request.error(\n 400,\n codes.unsupportedValue,\n `Synthetic: property '${unknown}' is not emulated on this request.`\n )\n}\n\n/** 400 for an invalid value. */\nexport const invalidValue = (request: RouteRequest, message: string): Response =>\n request.error(400, codes.invalidRequest, `Synthetic: ${message}`)\n\n/** 400 for a valid Graph request the emulator does not emulate (fail closed). */\nexport const notEmulated = (request: RouteRequest, message: string): Response =>\n request.error(400, codes.unsupportedValue, `Synthetic: ${message}`)\n\n/**\n * A nested JSON object with only `allowed` keys (fail closed on anything else, before any write),\n * or a 400. `expected` names the expected object in the error message.\n */\nexport const nestedObject = (\n request: RouteRequest,\n value: Schema.Json | undefined,\n allowed: ReadonlyArray<string>,\n expected: string\n): Schema.JsonObject | Response => {\n if (!isJsonObject(value)) return invalidValue(request, `expected ${expected}.`)\n\n const unknown = Object.keys(value).find(key => !allowed.includes(key))\n\n return unknown === undefined\n ? value\n : notEmulated(request, `property '${unknown}' is not emulated in ${expected}.`)\n}\n\n/**\n * An `itemBody` write value, `{ contentType, content }` with only those keys. The fixtures write\n * text bodies only (`Text`/`text`); any other content type is not emulated.\n */\nexport const textBody = (\n request: RouteRequest,\n value: Schema.Json | undefined\n): string | Response => {\n const body = nestedObject(\n request,\n value,\n ['contentType', 'content'],\n 'body { contentType, content }'\n )\n\n if (body instanceof Response) return body\n\n if (!Predicate.isString(body.contentType) || !Predicate.isString(body.content)) {\n return invalidValue(request, 'body must be { contentType, content } with string values.')\n }\n\n return body.contentType.toLowerCase() === 'text'\n ? body.content\n : notEmulated(request, `body contentType ${body.contentType} is not emulated (text only).`)\n}\n\n/** A Graph timestamp (`2026-09-29T10:00:00Z`) from the emulator clock. */\nexport const nowTimestamp = (env: MicrosoftApiEnv): string =>\n new Date(Math.floor(env.now() / 1000) * 1000).toISOString().replace('.000Z', 'Z')\n\n/** The next change key (bumps the counter). */\nexport const nextChangeKey = (state: MicrosoftEmulatorState, prefix: string): string => {\n const number = state.counters.nextChangeKeyNumber\n\n state.counters = { ...state.counters, nextChangeKeyNumber: number + 1 }\n\n return `${prefix}${padded(number, 4)}`\n}\n\n/** The `personal/{site}` segment of the mailbox owner (`ada@example.test` → `ada_example_test`). */\nexport const personalSite = (user: MicrosoftEmulatorUser): string =>\n user.mail.toLowerCase().replace(/[^a-z0-9]/g, '_')\n"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { RouteHandler } from "./graph.mjs";
2
+
3
+ //#region src/microsoft/mail.d.ts
4
+ /**
5
+ * `GET /users/{userId}/mailFolders/{folderId}/messages`: newest `receivedDateTime` first, one
6
+ * `$top`/`$skip` page with an opaque `@odata.nextLink` on the configured Graph origin. The folder
7
+ * is named by id (a well-known name or an unknown id is not emulated), and `$top` is required, at
8
+ * most `folderMessagesMaxTop`, as the paging fixture sends it.
9
+ */
10
+ declare const listFolderMessages: RouteHandler;
11
+ /**
12
+ * `POST /users/{userId}/messages`: a new draft in Drafts (201 with the message). Never sent. New
13
+ * drafts have no attachments (`hasAttachments: false`, as the fixtures record).
14
+ */
15
+ declare const createDraft: RouteHandler;
16
+ /**
17
+ * `PATCH /users/{userId}/messages/{messageId}`: update a draft's `subject` or `isRead`; 200 with
18
+ * the message.
19
+ */
20
+ declare const updateMessage: RouteHandler;
21
+ /**
22
+ * `POST /users/{userId}/messages/{messageId}/move` with `{ destinationId: 'deleteditems' }`, the
23
+ * only destination a fixture sends: 201 with the moved draft, which keeps its (immutable) id. Any
24
+ * other destination (another well-known name such as `inbox` or `drafts`, or a folder id) is not
25
+ * emulated (400) and moves nothing.
26
+ */
27
+ declare const moveMessage: RouteHandler;
28
+ /**
29
+ * `GET /users/{userId}/messages/{messageId}/attachments`: every attachment, inline ones included,
30
+ * in one response (the fixtures send only `$select`: no `$top`, `$skip`, or `@odata.nextLink`).
31
+ * `$select` accepts only base `attachment` properties, so a listing never carries `contentId`.
32
+ */
33
+ declare const listAttachments: RouteHandler;
34
+ /** `GET /users/{userId}/messages/{messageId}/attachments/{attachmentId}`: the file attachment. */
35
+ declare const getAttachment: RouteHandler;
36
+ /**
37
+ * `POST /$batch`: at most 20 subrequests with unique ids, each a message `permanentDelete` with
38
+ * `Prefer: IdType="ImmutableId"`, answered `{ responses: [{ id, status: 204, headers: {} }] }` as
39
+ * the fixtures record. Every subrequest is checked before any runs: anything else (another
40
+ * route, an unknown user or message, a message named twice, a message with a write in flight, a
41
+ * message that is not a draft, or one with attachments) refuses the whole batch with 400 and
42
+ * deletes nothing, because no fixture records a failed subrequest or deletes anything but a
43
+ * case-created draft (which has no attachments).
44
+ */
45
+ declare const batch: RouteHandler;
46
+ //#endregion
47
+ export { batch, createDraft, getAttachment, listAttachments, listFolderMessages, moveMessage, updateMessage };
48
+ //# sourceMappingURL=mail.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mail.d.mts","names":[],"sources":["../../src/microsoft/mail.ts"],"mappings":";;;;;AAqOA;;;;cAAa,kBAAA,EAAoB,YAiDhC;AA2HD;;;;AAAA,cAAa,WAAA,EAAa,YAgEzB;AAoBD;;;;AAAA,cAAa,aAAA,EAAe,YAqC3B;AAQD;;;;AAiDC;AAoCD;AArFA,cAAa,WAAA,EAAa,YAiDzB;;;AAoEA;AAGD;;cAnCa,eAAA,EAAiB,YAgC7B;;cAGY,aAAA,EAAe,YA2B3B;AA4ED;;;;AA+EC;;;;;AA/ED,cAAa,KAAA,EAAO,YA+EnB"}