@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,541 @@
1
+ /**
2
+ * Microsoft Graph emulator OneDrive routes (internal): item read, children listing, folder
3
+ * create, delete, and the asynchronous copy with its monitor URL. Wire shapes follow the
4
+ * synthetic OneDrive conformance fixtures.
5
+ *
6
+ * Deleted items are removed with their subtree (no recycle bin is emulated). Folder create and
7
+ * copy take `@microsoft.graph.conflictBehavior` `fail` only, as the fixtures send it, and only for
8
+ * a name that is free: no fixture records a name conflict, so one fails closed (400). Children
9
+ * listings need `$top` and answer one page (no `$skip`, no `@odata.nextLink`), as the fixtures
10
+ * record them. A copy takes what the copy fixture sends (a file, `parentReference { driveId, id }`
11
+ * on the same drive, no new `name`; anything else is not emulated) and answers 202 with one
12
+ * monitor `Location` on the SharePoint origin; the monitor needs no credentials, answers `inProgress` (202) for `copyInProgressPolls` polls (default 0), then runs
13
+ * the copy and answers `completed` (200) with the new item's `resourceId`, as the fixture's first
14
+ * poll does. No fixture records a failed copy: when the copy can no longer run (source or
15
+ * destination gone, or the name taken), the monitor answers 400 not emulated.
16
+ *
17
+ * @experimental
18
+ */
19
+ import { Predicate } from 'effect'
20
+ import type * as Schema from 'effect/Schema'
21
+ import {
22
+ bodyObject,
23
+ codes,
24
+ collection,
25
+ emptyResponse,
26
+ entity,
27
+ invalidValue,
28
+ isJsonObject,
29
+ jsonResponse,
30
+ metadataContext,
31
+ nestedObject,
32
+ notEmulated,
33
+ nowTimestamp,
34
+ odataKey,
35
+ padded,
36
+ personalSite,
37
+ project,
38
+ selectSuffix,
39
+ selectedFields,
40
+ singlePage,
41
+ type CopyMonitor,
42
+ type MicrosoftApiEnv,
43
+ type RouteHandler,
44
+ type RouteRequest
45
+ } from './graph.ts'
46
+ import type { MicrosoftEmulatorDriveItem, MicrosoftEmulatorState } from './state.ts'
47
+
48
+ const itemFields: ReadonlyArray<string> = [
49
+ 'id',
50
+ 'name',
51
+ 'size',
52
+ 'webUrl',
53
+ 'createdDateTime',
54
+ 'lastModifiedDateTime',
55
+ 'eTag',
56
+ 'cTag',
57
+ 'parentReference',
58
+ 'file',
59
+ 'folder',
60
+ 'package',
61
+ 'remoteItem',
62
+ 'shared',
63
+ 'deleted'
64
+ ]
65
+
66
+ const notFound = (request: RouteRequest): Response =>
67
+ request.error(404, codes.driveItemNotFound, 'The resource could not be found.')
68
+
69
+ const childrenOf = (state: MicrosoftEmulatorState, id: string) =>
70
+ state.driveItems.filter(item => item.parentId === id)
71
+
72
+ /** The item and its descendants. */
73
+ const subtreeOf = (
74
+ state: MicrosoftEmulatorState,
75
+ root: MicrosoftEmulatorDriveItem
76
+ ): ReadonlyArray<MicrosoftEmulatorDriveItem> => [
77
+ root,
78
+ ...childrenOf(state, root.id).flatMap(child => subtreeOf(state, child))
79
+ ]
80
+
81
+ /** Folders report the size of their contents. */
82
+ const sizeOf = (state: MicrosoftEmulatorState, item: MicrosoftEmulatorDriveItem): number =>
83
+ item.kind === 'file'
84
+ ? item.size
85
+ : childrenOf(state, item.id).reduce((sum, child) => sum + sizeOf(state, child), 0)
86
+
87
+ /** Names from below the root down to the item (empty for the root). */
88
+ const pathNames = (
89
+ state: MicrosoftEmulatorState,
90
+ item: MicrosoftEmulatorDriveItem
91
+ ): ReadonlyArray<string> => {
92
+ const parent =
93
+ item.parentId === null
94
+ ? undefined
95
+ : state.driveItems.find(candidate => candidate.id === item.parentId)
96
+
97
+ return parent === undefined ? [] : [...pathNames(state, parent), item.name]
98
+ }
99
+
100
+ /** A stable synthetic GUID for an item's `eTag`/`cTag` (FNV-1a over the id). */
101
+ const itemGuid = (id: string): string => {
102
+ let hash = 0x811c9dc5
103
+
104
+ for (const character of id) {
105
+ hash = Math.imul(hash ^ (character.codePointAt(0) ?? 0), 0x01000193) >>> 0
106
+ }
107
+
108
+ return `${hash.toString(16).padStart(8, '0')}-0000-4000-8000-000000000000`
109
+ }
110
+
111
+ /** The full item, in the fixture key order. */
112
+ const renderItem = (
113
+ state: MicrosoftEmulatorState,
114
+ env: MicrosoftApiEnv,
115
+ item: MicrosoftEmulatorDriveItem
116
+ ): Schema.JsonObject => {
117
+ const site = personalSite(state.user)
118
+ const names = pathNames(state, item)
119
+ const parentPath = names.slice(0, -1)
120
+
121
+ const parentReference: Schema.JsonObject =
122
+ item.parentId === null
123
+ ? { driveType: state.drive.driveType, driveId: state.drive.id }
124
+ : {
125
+ driveType: state.drive.driveType,
126
+ driveId: state.drive.id,
127
+ id: item.parentId,
128
+ path: `/drive/root:${parentPath.map(name => `/${name}`).join('')}`
129
+ }
130
+
131
+ const base = {
132
+ id: item.id,
133
+ name: item.name,
134
+ size: sizeOf(state, item),
135
+ webUrl: `${env.sharePointOrigin}/personal/${site}/Documents${names.map(name => `/${encodeURIComponent(name)}`).join('')}`,
136
+ createdDateTime: item.createdDateTime,
137
+ lastModifiedDateTime: item.lastModifiedDateTime,
138
+ eTag: `"{${itemGuid(item.id)}},1"`,
139
+ cTag: `"c:{${itemGuid(item.id)}},0"`,
140
+ parentReference
141
+ }
142
+
143
+ if (item.kind === 'folder') {
144
+ return { ...base, folder: { childCount: childrenOf(state, item.id).length } }
145
+ }
146
+
147
+ return {
148
+ ...base,
149
+ file: {
150
+ mimeType: item.mimeType ?? 'application/octet-stream',
151
+ hashes: item.quickXorHash === null ? {} : { quickXorHash: item.quickXorHash }
152
+ }
153
+ }
154
+ }
155
+
156
+ /** `drives('{driveId}')`, the context prefix of every OneDrive route. */
157
+ const driveContext = (request: RouteRequest): string =>
158
+ `drives${odataKey(request.params.driveId ?? '')}`
159
+
160
+ /** `drives('{driveId}')/items('{itemId}')/children`. */
161
+ const childrenContext = (request: RouteRequest): string =>
162
+ `${driveContext(request)}/items${odataKey(request.params.itemId ?? '')}/children`
163
+
164
+ /** The drive of the `{driveId}` segment, or 404. */
165
+ const driveProblem = (state: MicrosoftEmulatorState, request: RouteRequest) =>
166
+ request.params.driveId === state.drive.id ? undefined : notFound(request)
167
+
168
+ /** The item of an id segment (`root` names the drive root). */
169
+ const findItem = (
170
+ state: MicrosoftEmulatorState,
171
+ id: string
172
+ ): MicrosoftEmulatorDriveItem | undefined => {
173
+ const resolved = id.toLowerCase() === 'root' ? state.drive.rootId : id
174
+
175
+ return state.driveItems.find(item => item.id === resolved)
176
+ }
177
+
178
+ /** `GET /drives/{driveId}/items/{itemId}` (`$select`). */
179
+ export const getItem: RouteHandler = (state, request, env) => {
180
+ const drive = driveProblem(state, request)
181
+
182
+ if (drive !== undefined) return drive
183
+
184
+ const fields = selectedFields(request, itemFields)
185
+
186
+ if (fields instanceof Response) return fields
187
+
188
+ const item = findItem(state, request.params.itemId ?? '')
189
+
190
+ const context = metadataContext(
191
+ env,
192
+ `${driveContext(request)}/items${selectSuffix(fields)}/$entity`
193
+ )
194
+
195
+ return item === undefined
196
+ ? notFound(request)
197
+ : jsonResponse(200, entity(context, project(renderItem(state, env, item), fields)))
198
+ }
199
+
200
+ const byName = (left: MicrosoftEmulatorDriveItem, right: MicrosoftEmulatorDriveItem) =>
201
+ left.name.localeCompare(right.name, 'en', { sensitivity: 'base' }) ||
202
+ left.id.localeCompare(right.id)
203
+
204
+ /** The largest children `$top` a fixture sends (`$top=200`, create-folder and copy cases). */
205
+ const childrenMaxTop = 200
206
+
207
+ /**
208
+ * `GET /drives/{driveId}/items/{itemId}/children`: by name, at most `$top` (one page only;
209
+ * `$top` required, at most `childrenMaxTop`).
210
+ */
211
+ export const listChildren: RouteHandler = (state, request, env) => {
212
+ const drive = driveProblem(state, request)
213
+
214
+ if (drive !== undefined) return drive
215
+
216
+ const fields = selectedFields(request, itemFields)
217
+
218
+ if (fields instanceof Response) return fields
219
+
220
+ const parent = findItem(state, request.params.itemId ?? '')
221
+
222
+ if (parent === undefined) return notFound(request)
223
+
224
+ if (parent.kind !== 'folder') return notEmulated(request, 'children of a file are not emulated.')
225
+
226
+ const page = singlePage([...childrenOf(state, parent.id)].sort(byName), request, childrenMaxTop)
227
+
228
+ if (page instanceof Response) return page
229
+
230
+ return jsonResponse(
231
+ 200,
232
+ collection(
233
+ metadataContext(env, `${childrenContext(request)}${selectSuffix(fields)}`),
234
+ page.map(item => project(renderItem(state, env, item), fields)),
235
+ undefined
236
+ )
237
+ )
238
+ }
239
+
240
+ const invalidNamePattern = /["*:<>?/\\|]/
241
+
242
+ const nameProblem = (request: RouteRequest, name: unknown): string | Response =>
243
+ Predicate.isString(name) &&
244
+ name.trim() !== '' &&
245
+ name.trim() === name &&
246
+ !invalidNamePattern.test(name) &&
247
+ name !== '.' &&
248
+ name !== '..' &&
249
+ !name.endsWith('.')
250
+ ? name
251
+ : invalidValue(request, 'the item name is empty or has characters OneDrive does not allow.')
252
+
253
+ const nameTaken = (state: MicrosoftEmulatorState, parentId: string, name: string): boolean =>
254
+ childrenOf(state, parentId).some(child => child.name.toLowerCase() === name.toLowerCase())
255
+
256
+ const nextItemId = (state: MicrosoftEmulatorState): string => {
257
+ let number = state.counters.nextItemNumber
258
+ let id = `01SYNTHETICITEM${padded(number, 17)}`
259
+
260
+ while (state.driveItems.some(item => item.id === id)) {
261
+ number += 1
262
+ id = `01SYNTHETICITEM${padded(number, 17)}`
263
+ }
264
+
265
+ state.counters = { ...state.counters, nextItemNumber: number + 1 }
266
+
267
+ return id
268
+ }
269
+
270
+ const nameConflict = (request: RouteRequest): Response =>
271
+ notEmulated(request, 'name conflicts are not emulated (the name is already taken).')
272
+
273
+ /** Only `fail`, as the fixtures send it; anything else (or none) is not emulated. */
274
+ const conflictBehaviorProblem = (request: RouteRequest, value: unknown): Response | undefined =>
275
+ value === 'fail'
276
+ ? undefined
277
+ : notEmulated(
278
+ request,
279
+ '@microsoft.graph.conflictBehavior must be fail (other or missing values are not emulated).'
280
+ )
281
+
282
+ /**
283
+ * `POST /drives/{driveId}/items/{itemId}/children` with a `folder` facet and
284
+ * `@microsoft.graph.conflictBehavior: fail`: 201 with the folder. A taken name, and creating
285
+ * files here, are not emulated.
286
+ */
287
+ export const createFolder: RouteHandler = (state, request, env) => {
288
+ const drive = driveProblem(state, request)
289
+
290
+ if (drive !== undefined) return drive
291
+
292
+ const parent = findItem(state, request.params.itemId ?? '')
293
+
294
+ if (parent === undefined) return notFound(request)
295
+
296
+ if (parent.kind !== 'folder') return invalidValue(request, 'the parent item is not a folder.')
297
+
298
+ const fields = bodyObject(request, ['name', 'folder', '@microsoft.graph.conflictBehavior'])
299
+
300
+ if (fields instanceof Response) return fields
301
+
302
+ if (fields.folder === undefined) {
303
+ return notEmulated(request, 'only folder creation (a folder facet) is emulated here.')
304
+ }
305
+
306
+ const folderFacet = fields.folder
307
+
308
+ if (!isJsonObject(folderFacet) || Object.keys(folderFacet).length > 0) {
309
+ return notEmulated(request, 'the folder facet must be {}.')
310
+ }
311
+
312
+ const name = nameProblem(request, fields.name)
313
+
314
+ if (name instanceof Response) return name
315
+
316
+ const behavior = conflictBehaviorProblem(request, fields['@microsoft.graph.conflictBehavior'])
317
+
318
+ if (behavior !== undefined) return behavior
319
+
320
+ if (nameTaken(state, parent.id, name)) return nameConflict(request)
321
+
322
+ const now = nowTimestamp(env)
323
+
324
+ const folder: MicrosoftEmulatorDriveItem = {
325
+ id: nextItemId(state),
326
+ parentId: parent.id,
327
+ name,
328
+ kind: 'folder',
329
+ size: 0,
330
+ mimeType: null,
331
+ quickXorHash: null,
332
+ createdDateTime: now,
333
+ lastModifiedDateTime: now
334
+ }
335
+
336
+ state.driveItems = [...state.driveItems, folder]
337
+
338
+ return jsonResponse(
339
+ 201,
340
+ entity(
341
+ metadataContext(env, `${childrenContext(request)}/$entity`),
342
+ renderItem(state, env, folder)
343
+ )
344
+ )
345
+ }
346
+
347
+ /** `DELETE /drives/{driveId}/items/{itemId}`: 204; the item and its subtree are removed. */
348
+ export const deleteItem: RouteHandler = (state, request) => {
349
+ const drive = driveProblem(state, request)
350
+
351
+ if (drive !== undefined) return drive
352
+
353
+ const item = findItem(state, request.params.itemId ?? '')
354
+
355
+ if (item === undefined) return notFound(request)
356
+
357
+ if (item.parentId === null)
358
+ return notEmulated(request, 'deleting the drive root is not emulated.')
359
+
360
+ const removed = new Set(subtreeOf(state, item).map(entry => entry.id))
361
+
362
+ state.driveItems = state.driveItems.filter(entry => !removed.has(entry.id))
363
+
364
+ return emptyResponse(204)
365
+ }
366
+
367
+ const monitorPath = (state: MicrosoftEmulatorState, id: string) =>
368
+ `/personal/${personalSite(state.user)}/_api/v2.0/monitor/${id}`
369
+
370
+ /**
371
+ * `POST /drives/{driveId}/items/{itemId}/copy` (`{ parentReference: { driveId, id } }`, query
372
+ * `@microsoft.graph.conflictBehavior=fail`): 202 with an empty body and one monitor `Location`.
373
+ * The copy itself runs when the monitor reports completion. Only what the copy fixture sends is
374
+ * emulated: a file source, a destination on the same drive named by `driveId` and `id`, and the
375
+ * source's name (a new `name`, a folder source, or a missing `driveId` fail closed).
376
+ */
377
+ export const copyItem: RouteHandler = (state, request, env) => {
378
+ const drive = driveProblem(state, request)
379
+
380
+ if (drive !== undefined) return drive
381
+
382
+ const source = findItem(state, request.params.itemId ?? '')
383
+
384
+ if (source === undefined) return notFound(request)
385
+
386
+ if (source.kind !== 'file') {
387
+ return notEmulated(request, 'copying a folder is not emulated (the fixture copies a file).')
388
+ }
389
+
390
+ const behavior = conflictBehaviorProblem(
391
+ request,
392
+ request.query.get('@microsoft.graph.conflictBehavior') ?? undefined
393
+ )
394
+
395
+ if (behavior !== undefined) return behavior
396
+
397
+ // A new `name` fails here too: the fixture's copy keeps the source's name.
398
+ const fields = bodyObject(request, ['parentReference'])
399
+
400
+ if (fields instanceof Response) return fields
401
+
402
+ const reference = nestedObject(
403
+ request,
404
+ fields.parentReference,
405
+ ['driveId', 'id'],
406
+ 'parentReference { driveId, id }'
407
+ )
408
+
409
+ if (reference instanceof Response) return reference
410
+
411
+ if (reference.driveId === undefined) {
412
+ return notEmulated(request, 'copies without parentReference.driveId are not emulated.')
413
+ }
414
+
415
+ if (!Predicate.isString(reference.id) || !Predicate.isString(reference.driveId)) {
416
+ return invalidValue(request, 'parentReference must be { driveId, id } with string values.')
417
+ }
418
+
419
+ if (reference.driveId !== state.drive.id) {
420
+ return notEmulated(request, 'copies to another drive are not emulated.')
421
+ }
422
+
423
+ const destination = findItem(state, reference.id)
424
+
425
+ if (destination === undefined) return notFound(request)
426
+
427
+ if (destination.kind !== 'folder') {
428
+ return invalidValue(request, 'the destination is not a folder.')
429
+ }
430
+
431
+ const number = env.monitorCounter.next
432
+
433
+ env.monitorCounter.next += 1
434
+
435
+ const monitor: CopyMonitor = {
436
+ id: `00000000-0000-4000-8000-${padded(number, 12)}`,
437
+ sourceId: source.id,
438
+ destinationParentId: destination.id,
439
+ pollsLeft: env.copyInProgressPolls,
440
+ resourceId: undefined
441
+ }
442
+
443
+ env.monitors.set(monitor.id, monitor)
444
+
445
+ return emptyResponse(202, {
446
+ location: `${env.sharePointOrigin}${monitorPath(state, monitor.id)}`
447
+ })
448
+ }
449
+
450
+ /** Copy the file `item` under `parentId` with a fresh id and its name; returns the new id. */
451
+ const copyFile = (
452
+ state: MicrosoftEmulatorState,
453
+ env: MicrosoftApiEnv,
454
+ item: MicrosoftEmulatorDriveItem,
455
+ parentId: string
456
+ ): string => {
457
+ const now = nowTimestamp(env)
458
+
459
+ const copy: MicrosoftEmulatorDriveItem = {
460
+ ...item,
461
+ id: nextItemId(state),
462
+ parentId,
463
+ createdDateTime: now,
464
+ lastModifiedDateTime: now
465
+ }
466
+
467
+ state.driveItems = [...state.driveItems, copy]
468
+
469
+ return copy.id
470
+ }
471
+
472
+ /**
473
+ * Run a monitored copy: its `resourceId`, or why it cannot run (the source or destination is
474
+ * gone, or the name is taken; no fixture records a failed copy).
475
+ */
476
+ const runCopy = (
477
+ state: MicrosoftEmulatorState,
478
+ env: MicrosoftApiEnv,
479
+ monitor: CopyMonitor
480
+ ): string | { readonly problem: string } => {
481
+ const source = findItem(state, monitor.sourceId)
482
+ const destination = findItem(state, monitor.destinationParentId)
483
+
484
+ if (source === undefined || destination === undefined || destination.kind !== 'folder') {
485
+ return { problem: 'the copy source or destination no longer exists' }
486
+ }
487
+
488
+ if (nameTaken(state, destination.id, source.name)) {
489
+ return { problem: 'the copy name is already taken in the destination' }
490
+ }
491
+
492
+ return copyFile(state, env, source, destination.id)
493
+ }
494
+
495
+ const monitorContentType = 'application/json;odata.metadata=minimal;odata.streaming=true'
496
+
497
+ /**
498
+ * `GET /personal/{site}/_api/v2.0/monitor/{monitorId}` on the SharePoint origin (no credentials
499
+ * needed, like the real capability URL): `inProgress` (202) while polls remain, then the copy runs
500
+ * once and every later poll answers `completed` (200, `resourceId`). A copy that cannot run
501
+ * answers 400 not emulated (failed copies are not emulated) and stays pending.
502
+ */
503
+ export const copyMonitor: RouteHandler = (state, request, env) => {
504
+ const monitor = env.monitors.get(request.params.monitorId ?? '')
505
+
506
+ if (monitor === undefined || request.params.site !== personalSite(state.user)) {
507
+ return notFound(request)
508
+ }
509
+
510
+ const context = `${env.sharePointOrigin}/personal/${personalSite(state.user)}/_api/v2.0/$metadata#oneDrive.asynchronousOperationStatus`
511
+ const headers = { 'content-type': monitorContentType }
512
+
513
+ if (monitor.resourceId === undefined && monitor.pollsLeft > 0) {
514
+ monitor.pollsLeft -= 1
515
+
516
+ return jsonResponse(
517
+ 202,
518
+ { '@odata.context': context, percentageComplete: 0, status: 'inProgress' },
519
+ headers
520
+ )
521
+ }
522
+
523
+ const resourceId = monitor.resourceId ?? runCopy(state, env, monitor)
524
+
525
+ if (!Predicate.isString(resourceId)) {
526
+ return notEmulated(request, `${resourceId.problem} (failed copies are not emulated).`)
527
+ }
528
+
529
+ monitor.resourceId = resourceId
530
+
531
+ return jsonResponse(
532
+ 200,
533
+ {
534
+ '@odata.context': context,
535
+ percentageComplete: 100,
536
+ resourceId,
537
+ status: 'completed'
538
+ },
539
+ headers
540
+ )
541
+ }