@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,426 @@
1
+ /**
2
+ * Microsoft Graph emulator API: the route table (with its evidence and query allowlist), route
3
+ * matching, and the registration of the stateful handlers on the `@emulators/core` app
4
+ * (internal; re-exported by `src/microsoft.ts`).
5
+ *
6
+ * Only the routes the eleven Microsoft conformance cases need are emulated, plus the copy monitor
7
+ * URL they require; everything else fails closed. Each route lists the cases whose claims it
8
+ * follows.
9
+ *
10
+ * @experimental
11
+ */
12
+ import type { Hono } from '@emulators/core'
13
+ import { answeredOutsideCore, emulatorJobHeader, handlerFailedResponse } from '../emulator-http.ts'
14
+ import type { EmulatorRouteEvidence } from '../route-evidence.ts'
15
+ import {
16
+ calendarView,
17
+ cancelEvent,
18
+ createEvent,
19
+ deleteEvent,
20
+ getEvent,
21
+ updateEvent
22
+ } from './calendar.ts'
23
+ import { copyItem, copyMonitor, createFolder, deleteItem, getItem, listChildren } from './drive.ts'
24
+ import {
25
+ codes,
26
+ decodeSegment,
27
+ errorContext,
28
+ graphError,
29
+ microsoftEmulatorBasePath,
30
+ parseJsonText,
31
+ parsePreferences,
32
+ type MicrosoftApiEnv,
33
+ type RouteHandler,
34
+ type RouteRequest
35
+ } from './graph.ts'
36
+ import {
37
+ batch,
38
+ createDraft,
39
+ getAttachment,
40
+ listAttachments,
41
+ listFolderMessages,
42
+ moveMessage,
43
+ updateMessage
44
+ } from './mail.ts'
45
+ import type { MicrosoftEmulatorState } from './state.ts'
46
+
47
+ export { microsoftEmulatorBasePath } from './graph.ts'
48
+
49
+ type MicrosoftApiRoute = EmulatorRouteEvidence & {
50
+ /**
51
+ * Query parameters the route emulates; any other key answers 400 before the handler runs, so
52
+ * a rejected request never writes. Empty by default.
53
+ */
54
+ readonly queryKeys: ReadonlyArray<string>
55
+ /** `false` only for the copy monitor, a pre-authenticated capability URL. */
56
+ readonly auth: boolean
57
+ readonly handler: RouteHandler
58
+ }
59
+
60
+ const listRangeCase = 'microsoft.calendar.list-range-returns-events'
61
+
62
+ const precisionCase = 'microsoft.calendar.timestamp-precision'
63
+
64
+ const createEventCase = 'microsoft.calendar.create-returns-event-id'
65
+
66
+ const cancelCase = 'microsoft.calendar.cancel-semantics'
67
+
68
+ const attachmentsCase = 'microsoft.outlook.attachments-listing'
69
+
70
+ const contentIdCase = 'microsoft.outlook.attachment-content-id'
71
+
72
+ const pagingCase = 'microsoft.outlook.paging-next-link'
73
+
74
+ const immutableIdCase = 'microsoft.outlook.immutable-id-survives-move'
75
+
76
+ const concurrentCase = 'microsoft.outlook.concurrent-writes-same-message'
77
+
78
+ const folderCase = 'microsoft.onedrive.create-folder-roundtrip'
79
+
80
+ const copyCase = 'microsoft.onedrive.copy-accepted-monitor'
81
+
82
+ const route = (
83
+ method: string,
84
+ path: string,
85
+ write: boolean,
86
+ caseIds: ReadonlyArray<string>,
87
+ handler: RouteHandler,
88
+ queryKeys: ReadonlyArray<string> = [],
89
+ auth = true
90
+ ): MicrosoftApiRoute => ({
91
+ method,
92
+ path,
93
+ kind: 'connector',
94
+ write,
95
+ caseIds,
96
+ evidence: 'unverified',
97
+ queryKeys,
98
+ auth,
99
+ handler
100
+ })
101
+
102
+ const graph = (path: string) => `${microsoftEmulatorBasePath}${path}`
103
+
104
+ /** Folder message listing: the paging fixture's `$select`, `$top`, and `$skip`. */
105
+ const paging: ReadonlyArray<string> = ['$select', '$top', '$skip']
106
+
107
+ /** Calendar views and children listings: their fixtures send `$select` and `$top`, never `$skip`. */
108
+ const onePage: ReadonlyArray<string> = ['$select', '$top']
109
+
110
+ /** The route table: evidence plus handler. `microsoftEmulatorRoutes` is its evidence part. */
111
+ export const microsoftApiRoutes: ReadonlyArray<MicrosoftApiRoute> = [
112
+ route(
113
+ 'GET',
114
+ graph('/users/{userId}/calendars/{calendarId}/calendarView'),
115
+ false,
116
+ [listRangeCase, precisionCase],
117
+ calendarView,
118
+ ['startDateTime', 'endDateTime', ...onePage]
119
+ ),
120
+ route(
121
+ 'POST',
122
+ graph('/users/{userId}/calendars/{calendarId}/events'),
123
+ true,
124
+ [createEventCase, cancelCase],
125
+ createEvent
126
+ ),
127
+ route(
128
+ 'GET',
129
+ graph('/users/{userId}/events/{eventId}'),
130
+ false,
131
+ [precisionCase, createEventCase, cancelCase],
132
+ getEvent,
133
+ ['$select']
134
+ ),
135
+ route('PATCH', graph('/users/{userId}/events/{eventId}'), true, [createEventCase], updateEvent),
136
+ route(
137
+ 'DELETE',
138
+ graph('/users/{userId}/events/{eventId}'),
139
+ true,
140
+ [createEventCase, cancelCase],
141
+ deleteEvent
142
+ ),
143
+ route('POST', graph('/users/{userId}/events/{eventId}/cancel'), true, [cancelCase], cancelEvent),
144
+ route(
145
+ 'GET',
146
+ graph('/users/{userId}/mailFolders/{folderId}/messages'),
147
+ false,
148
+ [pagingCase],
149
+ listFolderMessages,
150
+ paging
151
+ ),
152
+ route(
153
+ 'POST',
154
+ graph('/users/{userId}/messages'),
155
+ true,
156
+ [immutableIdCase, concurrentCase],
157
+ createDraft
158
+ ),
159
+ route(
160
+ 'PATCH',
161
+ graph('/users/{userId}/messages/{messageId}'),
162
+ true,
163
+ [immutableIdCase, concurrentCase],
164
+ updateMessage
165
+ ),
166
+ route(
167
+ 'POST',
168
+ graph('/users/{userId}/messages/{messageId}/move'),
169
+ true,
170
+ [immutableIdCase],
171
+ moveMessage
172
+ ),
173
+ route(
174
+ 'GET',
175
+ graph('/users/{userId}/messages/{messageId}/attachments'),
176
+ false,
177
+ [attachmentsCase, contentIdCase],
178
+ listAttachments,
179
+ ['$select']
180
+ ),
181
+ route(
182
+ 'GET',
183
+ graph('/users/{userId}/messages/{messageId}/attachments/{attachmentId}'),
184
+ false,
185
+ [attachmentsCase, contentIdCase],
186
+ getAttachment
187
+ ),
188
+ route('POST', graph('/$batch'), true, [immutableIdCase, concurrentCase], batch),
189
+ route('GET', graph('/drives/{driveId}/items/{itemId}'), false, [folderCase, copyCase], getItem, [
190
+ '$select'
191
+ ]),
192
+ route(
193
+ 'GET',
194
+ graph('/drives/{driveId}/items/{itemId}/children'),
195
+ false,
196
+ [folderCase, copyCase],
197
+ listChildren,
198
+ onePage
199
+ ),
200
+ route(
201
+ 'POST',
202
+ graph('/drives/{driveId}/items/{itemId}/children'),
203
+ true,
204
+ [folderCase, copyCase],
205
+ createFolder
206
+ ),
207
+ route(
208
+ 'DELETE',
209
+ graph('/drives/{driveId}/items/{itemId}'),
210
+ true,
211
+ [folderCase, copyCase],
212
+ deleteItem
213
+ ),
214
+ route('POST', graph('/drives/{driveId}/items/{itemId}/copy'), true, [copyCase], copyItem, [
215
+ '@microsoft.graph.conflictBehavior'
216
+ ]),
217
+ route(
218
+ 'GET',
219
+ '/personal/{site}/_api/v2.0/monitor/{monitorId}',
220
+ false,
221
+ [copyCase],
222
+ copyMonitor,
223
+ [],
224
+ false
225
+ )
226
+ ]
227
+
228
+ const templatePattern = (template: string): RegExp =>
229
+ new RegExp(
230
+ `^${template
231
+ .split(/(\{[A-Za-z]+\})/)
232
+ .map(part =>
233
+ /^\{[A-Za-z]+\}$/.test(part) ? '([^/]+)' : part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
234
+ )
235
+ .join('')}$`
236
+ )
237
+
238
+ const templateNames = (template: string): ReadonlyArray<string> =>
239
+ [...template.matchAll(/\{([A-Za-z]+)\}/g)].map(match => match[1] ?? '')
240
+
241
+ const compiledRoutes = microsoftApiRoutes.map(candidate => ({
242
+ route: candidate,
243
+ pattern: templatePattern(candidate.path),
244
+ names: templateNames(candidate.path)
245
+ }))
246
+
247
+ export type MatchedRoute = {
248
+ readonly route: MicrosoftApiRoute
249
+ /** Decoded path parameters. */
250
+ readonly params: Readonly<Record<string, string>>
251
+ }
252
+
253
+ /**
254
+ * The route answering `method` + raw (percent-encoded) `path` with its decoded parameters, or
255
+ * `undefined` (fail closed; also for a parameter that is not valid percent-encoding).
256
+ */
257
+ export const matchMicrosoftRoute = (method: string, path: string): MatchedRoute | undefined => {
258
+ for (const candidate of compiledRoutes) {
259
+ if (candidate.route.method !== method.toUpperCase()) continue
260
+
261
+ const match = candidate.pattern.exec(path)
262
+
263
+ if (match === null) continue
264
+
265
+ const params: Record<string, string> = {}
266
+
267
+ for (const [index, name] of candidate.names.entries()) {
268
+ const value = decodeSegment(match[index + 1] ?? '')
269
+
270
+ if (value === undefined) return undefined
271
+
272
+ params[name] = value
273
+ }
274
+
275
+ return { route: candidate.route, params }
276
+ }
277
+
278
+ return undefined
279
+ }
280
+
281
+ /** Header the wrapper sets on core requests: the ledger sequence number (for error ids). */
282
+ export const requestSeqHeader = 'x-emulator-request-seq'
283
+
284
+ /**
285
+ * The wrapper's fault decision for the core request of job `jobId` (its `emulatorJobHeader`),
286
+ * asked only once the route would answer successfully: `true` when a fault answers instead (it is
287
+ * used up, and the wrapper sends its answer), `false` when none applies.
288
+ */
289
+ export type MicrosoftFaultDecision = (jobId: string | null) => boolean
290
+
291
+ const delay = (ms: number): Promise<void> =>
292
+ new Promise(resolve => {
293
+ setTimeout(resolve, ms)
294
+ })
295
+
296
+ /** The first query key the route does not emulate, or `undefined`. */
297
+ const unsupportedQueryKey = (
298
+ query: URLSearchParams,
299
+ allowed: ReadonlyArray<string>
300
+ ): string | undefined => [...query.keys()].find(candidate => !allowed.includes(candidate))
301
+
302
+ /** `{Name}` path templates become `:Name` core route parameters. */
303
+ const corePath = (template: string): string => template.replace(/\{([A-Za-z]+)\}/g, ':$1')
304
+
305
+ /**
306
+ * One core request: re-match, check the query allowlist and `If-Match`, then plan, fault, and
307
+ * commit. The handler runs on drafts: a shallow copy of the state (handlers replace whole lists
308
+ * and counters, never edit them in place) and copies of the copy monitors and their counter. A
309
+ * refusal (any answer that is not 2xx) is sent as is and uses up no fault; a successful answer asks
310
+ * `decideFault`, and only an unfaulted one commits the drafts (and then holds a written message
311
+ * for `conflictWindowMs`). Plan, fault decision, and commit run synchronously together, so no
312
+ * other request interleaves.
313
+ */
314
+ const handle = async (
315
+ raw: Request,
316
+ state: MicrosoftEmulatorState,
317
+ env: MicrosoftApiEnv,
318
+ decideFault: MicrosoftFaultDecision
319
+ ): Promise<Response> => {
320
+ const url = new URL(raw.url)
321
+ const headers = raw.headers
322
+ const seq = Number(headers.get(requestSeqHeader) ?? '0')
323
+
324
+ // Built on demand, so the clock is read only when an error is answered.
325
+ const error = (status: number, code: string, message: string) =>
326
+ graphError(
327
+ errorContext(
328
+ env.now(),
329
+ Number.isSafeInteger(seq) ? seq : 0,
330
+ headers.get('client-request-id')
331
+ ),
332
+ status,
333
+ code,
334
+ message
335
+ )
336
+
337
+ const matched = matchMicrosoftRoute(raw.method, url.pathname)
338
+
339
+ if (matched === undefined) {
340
+ return error(404, codes.unknownRoute, 'Synthetic: no emulated Microsoft Graph route.')
341
+ }
342
+
343
+ const unsupported = unsupportedQueryKey(url.searchParams, matched.route.queryKeys)
344
+
345
+ if (unsupported !== undefined) {
346
+ return error(
347
+ 400,
348
+ codes.unsupportedQuery,
349
+ `Synthetic: query parameter ${unsupported} is not emulated on this route.`
350
+ )
351
+ }
352
+
353
+ if (headers.has('if-match')) {
354
+ return error(
355
+ 400,
356
+ codes.unsupportedValue,
357
+ 'Synthetic: conditional requests (If-Match) are not emulated.'
358
+ )
359
+ }
360
+
361
+ const text = await raw.text()
362
+
363
+ const request: RouteRequest = {
364
+ params: matched.params,
365
+ path: url.pathname,
366
+ query: url.searchParams,
367
+ body: text === '' ? undefined : parseJsonText(text),
368
+ prefer: parsePreferences(headers.get('prefer')),
369
+ error
370
+ }
371
+
372
+ const draft: MicrosoftEmulatorState = { ...state }
373
+ const monitors = new Map([...env.monitors].map(([id, monitor]) => [id, { ...monitor }]))
374
+ const monitorCounter = { next: env.monitorCounter.next }
375
+ const answer = matched.route.handler(draft, request, { ...env, monitors, monitorCounter })
376
+ const response = answer instanceof Response ? answer : answer.response
377
+
378
+ if (!response.ok) return response
379
+
380
+ if (decideFault(headers.get(emulatorJobHeader))) return answeredOutsideCore()
381
+
382
+ Object.assign(state, draft)
383
+ env.monitors.clear()
384
+
385
+ for (const [id, monitor] of monitors) env.monitors.set(id, monitor)
386
+
387
+ env.monitorCounter.next = monitorCounter.next
388
+
389
+ if (answer instanceof Response) return answer
390
+
391
+ env.messageLocks.add(answer.holds)
392
+
393
+ try {
394
+ if (env.conflictWindowMs > 0) await delay(env.conflictWindowMs)
395
+
396
+ return answer.response
397
+ } finally {
398
+ env.messageLocks.delete(answer.holds)
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Register every route of the table on the core app, over the generation's state. Each handler
404
+ * re-matches the raw request path against the same table the wrapper ledgers (so parameters are
405
+ * decoded once, failing closed on invalid percent-encoding), checks the route's query allowlist
406
+ * and refuses `If-Match` (no fixture sends one) before the handler runs, then plans, faults, and
407
+ * commits (see `handle`). A handler that throws answers `handlerFailedResponse()` (nothing
408
+ * written), which the wrapper turns into its Graph error envelope 500 with `responseError` in the
409
+ * ledger.
410
+ */
411
+ export const registerMicrosoftApi = (
412
+ app: Hono,
413
+ state: MicrosoftEmulatorState,
414
+ env: MicrosoftApiEnv,
415
+ decideFault: MicrosoftFaultDecision
416
+ ): void => {
417
+ for (const apiRoute of microsoftApiRoutes) {
418
+ app.on(apiRoute.method, corePath(apiRoute.path), async context => {
419
+ try {
420
+ return await handle(context.req.raw, state, env, decideFault)
421
+ } catch {
422
+ return handlerFailedResponse()
423
+ }
424
+ })
425
+ }
426
+ }