@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,822 @@
1
+ /**
2
+ * Microsoft Graph emulator state: the typed entities, the seed input, the default seed, and the
3
+ * profiles (internal; re-exported by `src/microsoft.ts`).
4
+ *
5
+ * Entity shapes and the default entities follow the synthetic Microsoft conformance fixtures,
6
+ * copied as data (the same mailbox, calendar, event, message, folder, drive, and item ids as
7
+ * `microsoftConformanceFixtureSeeds`), never imported from SDK code. Where no fixture records a
8
+ * value, the default is synthesized and says so below: the user id, the well-known mail folders
9
+ * other than Drafts and Deleted Items, the attachment message itself, and the drive root and
10
+ * `Sources` folder have no fixture.
11
+ *
12
+ * @experimental
13
+ */
14
+ import { Result, Struct } from 'effect'
15
+ import * as Schema from 'effect/Schema'
16
+
17
+ const Timestamp = Schema.String
18
+
19
+ /** A recipient as stored: `name` falls back to the address when Graph has none. */
20
+ export const MicrosoftEmulatorRecipient = Schema.Struct({
21
+ name: Schema.NullOr(Schema.String),
22
+ address: Schema.String
23
+ })
24
+
25
+ export type MicrosoftEmulatorRecipient = typeof MicrosoftEmulatorRecipient.Type
26
+
27
+ /** The mailbox owner every `/users/{userId}` route answers for (id, `mail`, or UPN). */
28
+ export const MicrosoftEmulatorUser = Schema.Struct({
29
+ id: Schema.String,
30
+ displayName: Schema.String,
31
+ mail: Schema.String,
32
+ userPrincipalName: Schema.String
33
+ })
34
+
35
+ export type MicrosoftEmulatorUser = typeof MicrosoftEmulatorUser.Type
36
+
37
+ const WellKnownFolder = Schema.Literals(['inbox', 'drafts', 'deleteditems'])
38
+
39
+ /** A mail folder; `wellKnownName` lets routes address it by name (`deleteditems`). */
40
+ export const MicrosoftEmulatorMailFolder = Schema.Struct({
41
+ id: Schema.String,
42
+ displayName: Schema.String,
43
+ wellKnownName: Schema.NullOr(WellKnownFolder)
44
+ })
45
+
46
+ export type MicrosoftEmulatorMailFolder = typeof MicrosoftEmulatorMailFolder.Type
47
+
48
+ const BodyContentType = Schema.Literals(['text', 'html'])
49
+
50
+ const messageFields = {
51
+ /** The immutable id (`Prefer: IdType="ImmutableId"`): stable across moves. */
52
+ id: Schema.String,
53
+ parentFolderId: Schema.String,
54
+ subject: Schema.String,
55
+ bodyContentType: BodyContentType,
56
+ bodyContent: Schema.String,
57
+ from: Schema.NullOr(MicrosoftEmulatorRecipient),
58
+ sender: Schema.NullOr(MicrosoftEmulatorRecipient),
59
+ toRecipients: Schema.Array(MicrosoftEmulatorRecipient),
60
+ ccRecipients: Schema.Array(MicrosoftEmulatorRecipient),
61
+ bccRecipients: Schema.Array(MicrosoftEmulatorRecipient),
62
+ replyTo: Schema.Array(MicrosoftEmulatorRecipient),
63
+ isRead: Schema.Boolean,
64
+ isDraft: Schema.Boolean,
65
+ importance: Schema.Literals(['low', 'normal', 'high']),
66
+ flagStatus: Schema.Literals(['notFlagged', 'flagged', 'complete']),
67
+ categories: Schema.Array(Schema.String),
68
+ /** Answered as stored (not derived from the attachments; no fixture shows the rule). */
69
+ hasAttachments: Schema.Boolean,
70
+ createdDateTime: Timestamp,
71
+ lastModifiedDateTime: Timestamp,
72
+ receivedDateTime: Timestamp,
73
+ sentDateTime: Timestamp,
74
+ conversationId: Schema.String,
75
+ internetMessageId: Schema.String,
76
+ changeKey: Schema.String
77
+ }
78
+
79
+ /** A stored message. Every message is addressed by its immutable id. */
80
+ export const MicrosoftEmulatorMessage = Schema.Struct(messageFields)
81
+
82
+ export type MicrosoftEmulatorMessage = typeof MicrosoftEmulatorMessage.Type
83
+
84
+ const attachmentFields = {
85
+ id: Schema.String,
86
+ /** Immutable id of the owning message. */
87
+ messageId: Schema.String,
88
+ name: Schema.String,
89
+ contentType: Schema.String,
90
+ size: Schema.Int,
91
+ isInline: Schema.Boolean,
92
+ contentId: Schema.NullOr(Schema.String),
93
+ contentLocation: Schema.NullOr(Schema.String),
94
+ contentBytes: Schema.String,
95
+ lastModifiedDateTime: Timestamp
96
+ }
97
+
98
+ /** A stored file attachment (`#microsoft.graph.fileAttachment`). */
99
+ export const MicrosoftEmulatorAttachment = Schema.Struct(attachmentFields)
100
+
101
+ export type MicrosoftEmulatorAttachment = typeof MicrosoftEmulatorAttachment.Type
102
+
103
+ export const MicrosoftEmulatorCalendar = Schema.Struct({
104
+ id: Schema.String,
105
+ name: Schema.String
106
+ })
107
+
108
+ export type MicrosoftEmulatorCalendar = typeof MicrosoftEmulatorCalendar.Type
109
+
110
+ /** Graph local date-time in UTC with seven fractional digits (`2026-09-23T12:00:00.0000000`). */
111
+ export const microsoftEmulatorDateTimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}$/
112
+
113
+ const UtcDateTime = Schema.String.check(Schema.isPattern(microsoftEmulatorDateTimePattern))
114
+
115
+ const eventFields = {
116
+ id: Schema.String,
117
+ calendarId: Schema.String,
118
+ subject: Schema.String,
119
+ bodyContentType: BodyContentType,
120
+ bodyContent: Schema.String,
121
+ /** Start in UTC, seven fractional digits. */
122
+ start: UtcDateTime,
123
+ /** End in UTC, seven fractional digits. */
124
+ end: UtcDateTime,
125
+ isCancelled: Schema.Boolean,
126
+ isReminderOn: Schema.Boolean,
127
+ showAs: Schema.Literals(['free', 'tentative', 'busy', 'oof', 'workingElsewhere', 'unknown']),
128
+ changeKey: Schema.String,
129
+ createdDateTime: Timestamp,
130
+ lastModifiedDateTime: Timestamp
131
+ }
132
+
133
+ /** A stored single-instance calendar event (no attendees, no recurrence). */
134
+ export const MicrosoftEmulatorEvent = Schema.Struct(eventFields)
135
+
136
+ export type MicrosoftEmulatorEvent = typeof MicrosoftEmulatorEvent.Type
137
+
138
+ /** The one emulated drive. */
139
+ export const MicrosoftEmulatorDrive = Schema.Struct({
140
+ id: Schema.String,
141
+ driveType: Schema.Literals(['business', 'personal']),
142
+ rootId: Schema.String
143
+ })
144
+
145
+ export type MicrosoftEmulatorDrive = typeof MicrosoftEmulatorDrive.Type
146
+
147
+ const driveItemFields = {
148
+ id: Schema.String,
149
+ /** `null` only for the drive root. */
150
+ parentId: Schema.NullOr(Schema.String),
151
+ name: Schema.String,
152
+ kind: Schema.Literals(['folder', 'file']),
153
+ /** File size in bytes; folders report the sum of their contents instead. */
154
+ size: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
155
+ mimeType: Schema.NullOr(Schema.String),
156
+ quickXorHash: Schema.NullOr(Schema.String),
157
+ createdDateTime: Timestamp,
158
+ lastModifiedDateTime: Timestamp
159
+ }
160
+
161
+ /** A stored drive item (file or folder). Deleted items are removed (no recycle bin). */
162
+ export const MicrosoftEmulatorDriveItem = Schema.Struct(driveItemFields)
163
+
164
+ export type MicrosoftEmulatorDriveItem = typeof MicrosoftEmulatorDriveItem.Type
165
+
166
+ const Counter = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
167
+
168
+ const Counters = Schema.Struct({
169
+ /** Next number in created event ids (`AAMkAGI2-synthetic-event-0101=`). */
170
+ nextEventNumber: Counter,
171
+ /** Next number in created message immutable ids. */
172
+ nextMessageNumber: Counter,
173
+ /** Next change key number (message and event writes). */
174
+ nextChangeKeyNumber: Counter,
175
+ /** Next number in created drive item ids. */
176
+ nextItemNumber: Counter
177
+ })
178
+
179
+ /** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
180
+ export const MicrosoftEmulatorStateSchema = Schema.Struct({
181
+ user: MicrosoftEmulatorUser,
182
+ mailFolders: Schema.Array(MicrosoftEmulatorMailFolder),
183
+ messages: Schema.Array(MicrosoftEmulatorMessage),
184
+ attachments: Schema.Array(MicrosoftEmulatorAttachment),
185
+ calendars: Schema.Array(MicrosoftEmulatorCalendar),
186
+ events: Schema.Array(MicrosoftEmulatorEvent),
187
+ drive: MicrosoftEmulatorDrive,
188
+ driveItems: Schema.Array(MicrosoftEmulatorDriveItem),
189
+ counters: Counters
190
+ })
191
+
192
+ /**
193
+ * The emulator state. The container is mutable (route handlers replace whole entity lists);
194
+ * every entity is replaced, never edited in place.
195
+ */
196
+ export type MicrosoftEmulatorState = {
197
+ user: MicrosoftEmulatorUser
198
+ mailFolders: ReadonlyArray<MicrosoftEmulatorMailFolder>
199
+ messages: ReadonlyArray<MicrosoftEmulatorMessage>
200
+ attachments: ReadonlyArray<MicrosoftEmulatorAttachment>
201
+ calendars: ReadonlyArray<MicrosoftEmulatorCalendar>
202
+ events: ReadonlyArray<MicrosoftEmulatorEvent>
203
+ drive: MicrosoftEmulatorDrive
204
+ driveItems: ReadonlyArray<MicrosoftEmulatorDriveItem>
205
+ counters: typeof Counters.Type
206
+ }
207
+
208
+ /** A seeded message: `id`, `parentFolderId`, and `subject` are required. */
209
+ export const MicrosoftEmulatorMessageSeed = Schema.Struct(
210
+ Struct.mapOmit(messageFields, ['id', 'parentFolderId', 'subject'], Schema.optionalKey)
211
+ )
212
+
213
+ export type MicrosoftEmulatorMessageSeed = typeof MicrosoftEmulatorMessageSeed.Type
214
+
215
+ /** A seeded attachment: `id`, `messageId`, and `name` are required. */
216
+ export const MicrosoftEmulatorAttachmentSeed = Schema.Struct(
217
+ Struct.mapOmit(attachmentFields, ['id', 'messageId', 'name'], Schema.optionalKey)
218
+ )
219
+
220
+ export type MicrosoftEmulatorAttachmentSeed = typeof MicrosoftEmulatorAttachmentSeed.Type
221
+
222
+ /**
223
+ * A seeded event: `id`, `calendarId`, `subject`, `start`, and `end` are required. `start` and
224
+ * `end` are UTC: either an ISO instant ending in `Z` or a seven-digit local date-time.
225
+ */
226
+ export const MicrosoftEmulatorEventSeed = Schema.Struct({
227
+ ...Struct.mapOmit(
228
+ eventFields,
229
+ ['id', 'calendarId', 'subject', 'start', 'end'],
230
+ Schema.optionalKey
231
+ ),
232
+ start: Schema.String,
233
+ end: Schema.String
234
+ })
235
+
236
+ export type MicrosoftEmulatorEventSeed = typeof MicrosoftEmulatorEventSeed.Type
237
+
238
+ /** A seeded drive item: `id`, `parentId`, `name`, and `kind` are required. */
239
+ export const MicrosoftEmulatorDriveItemSeed = Schema.Struct(
240
+ Struct.mapOmit(driveItemFields, ['id', 'parentId', 'name', 'kind'], Schema.optionalKey)
241
+ )
242
+
243
+ export type MicrosoftEmulatorDriveItemSeed = typeof MicrosoftEmulatorDriveItemSeed.Type
244
+
245
+ /** Account-variance profiles for the default seed. */
246
+ export const MicrosoftEmulatorProfile = Schema.Literals(['default', 'empty'])
247
+
248
+ export type MicrosoftEmulatorProfile = typeof MicrosoftEmulatorProfile.Type
249
+
250
+ /**
251
+ * A typed seed. Start from `profile` (default `'default'`, the fixture entities); every other
252
+ * key, when given, replaces that part of the profile.
253
+ */
254
+ export const MicrosoftEmulatorSeed = Schema.Struct({
255
+ profile: Schema.optionalKey(MicrosoftEmulatorProfile),
256
+ user: Schema.optionalKey(MicrosoftEmulatorUser),
257
+ mailFolders: Schema.optionalKey(Schema.Array(MicrosoftEmulatorMailFolder)),
258
+ messages: Schema.optionalKey(Schema.Array(MicrosoftEmulatorMessageSeed)),
259
+ attachments: Schema.optionalKey(Schema.Array(MicrosoftEmulatorAttachmentSeed)),
260
+ calendars: Schema.optionalKey(Schema.Array(MicrosoftEmulatorCalendar)),
261
+ events: Schema.optionalKey(Schema.Array(MicrosoftEmulatorEventSeed)),
262
+ drive: Schema.optionalKey(MicrosoftEmulatorDrive),
263
+ driveItems: Schema.optionalKey(Schema.Array(MicrosoftEmulatorDriveItemSeed))
264
+ })
265
+
266
+ export type MicrosoftEmulatorSeed = typeof MicrosoftEmulatorSeed.Type
267
+
268
+ const strict = { onExcessProperty: 'error' } as const
269
+
270
+ const decodeSeedInput = Schema.decodeUnknownResult(MicrosoftEmulatorSeed, strict)
271
+
272
+ const decodeStateInput = Schema.decodeUnknownResult(MicrosoftEmulatorStateSchema, strict)
273
+
274
+ const issueMessage = (issue: Schema.SchemaError['issue']): string =>
275
+ new Schema.SchemaError(issue).message
276
+
277
+ // Instants: 100-nanosecond ticks since the epoch (BigInt, so seven fractional digits stay exact).
278
+
279
+ const ticksPerMilli = BigInt(10_000)
280
+
281
+ const ticksPerSecond = BigInt(10_000_000)
282
+
283
+ const instantPattern =
284
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?(Z|[+-]\d{2}:\d{2})?$/
285
+
286
+ /**
287
+ * Ticks of an ISO-8601 date-time. `zone` `'required'` needs `Z` or an offset; `'forbidden'`
288
+ * needs none (a Graph local date-time, read as UTC); `'optional'` reads a missing one as UTC.
289
+ */
290
+ export const parseInstant = (
291
+ value: string,
292
+ zone: 'required' | 'forbidden' | 'optional'
293
+ ): bigint | undefined => {
294
+ const match = instantPattern.exec(value)
295
+
296
+ if (match === null) return undefined
297
+
298
+ const [, year, month, day, hour, minute, second, fraction = '', offset] = match
299
+
300
+ if (
301
+ (zone === 'required' && offset === undefined) ||
302
+ (zone === 'forbidden' && offset !== undefined)
303
+ ) {
304
+ return undefined
305
+ }
306
+
307
+ const wholeMillis = Date.UTC(
308
+ Number(year),
309
+ Number(month) - 1,
310
+ Number(day),
311
+ Number(hour),
312
+ Number(minute),
313
+ Number(second)
314
+ )
315
+
316
+ const check = new Date(wholeMillis)
317
+
318
+ // Reject rollovers such as 2026-02-30 or 25:00.
319
+ if (
320
+ !Number.isFinite(wholeMillis) ||
321
+ check.getUTCFullYear() !== Number(year) ||
322
+ check.getUTCMonth() !== Number(month) - 1 ||
323
+ check.getUTCDate() !== Number(day) ||
324
+ check.getUTCHours() !== Number(hour) ||
325
+ check.getUTCMinutes() !== Number(minute)
326
+ ) {
327
+ return undefined
328
+ }
329
+
330
+ const offsetMinutes =
331
+ offset === undefined || offset === 'Z'
332
+ ? 0
333
+ : (offset.startsWith('-') ? -1 : 1) *
334
+ (Number(offset.slice(1, 3)) * 60 + Number(offset.slice(4, 6)))
335
+
336
+ return (
337
+ BigInt(wholeMillis - offsetMinutes * 60_000) * ticksPerMilli + BigInt(fraction.padEnd(7, '0'))
338
+ )
339
+ }
340
+
341
+ const pad = (value: number, width: number): string => String(value).padStart(width, '0')
342
+
343
+ /** A UTC local date-time with `digits` fractional digits (Graph uses seven). */
344
+ export const formatLocalDateTime = (ticks: bigint, digits: number): string => {
345
+ const millis = Number(ticks / ticksPerMilli)
346
+ const date = new Date(millis)
347
+ const fraction = pad(Number(((ticks % ticksPerSecond) + ticksPerSecond) % ticksPerSecond), 7)
348
+
349
+ const whole = `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(
350
+ date.getUTCDate(),
351
+ 2
352
+ )}T${pad(date.getUTCHours(), 2)}:${pad(date.getUTCMinutes(), 2)}:${pad(date.getUTCSeconds(), 2)}`
353
+
354
+ return digits === 0 ? whole : `${whole}.${fraction.slice(0, digits)}`
355
+ }
356
+
357
+ // Default entities. Fixture-derived: the mailbox, the calendar and its two events, the attachment
358
+ // message's two attachments, the paging folder's three messages, the Drafts and Deleted Items
359
+ // folder ids, the drive, the `Conformance` parent folder with its existing file, and the copy
360
+ // source file. Synthesized: the user id, the Inbox folder, the attachment message's own fields,
361
+ // the drive root, the `Sources` folder, and every default the `*FromSeed` builders fill in.
362
+
363
+ const ada: MicrosoftEmulatorUser = {
364
+ id: '00000000-0000-4000-8000-00000000ada1',
365
+ displayName: 'Ada Example',
366
+ mail: 'ada@example.test',
367
+ userPrincipalName: 'ada@example.test'
368
+ }
369
+
370
+ const grace: MicrosoftEmulatorRecipient = { name: 'Grace Example', address: 'grace@example.test' }
371
+
372
+ const adaRecipient: MicrosoftEmulatorRecipient = {
373
+ name: 'Ada Example',
374
+ address: 'ada@example.test'
375
+ }
376
+
377
+ const inboxId = 'AAMkAGI2-synthetic-inbox-folder='
378
+
379
+ const pagingFolderId = 'AAMkAGI2-synthetic-folder-0001='
380
+
381
+ const defaultMailFolders: ReadonlyArray<MicrosoftEmulatorMailFolder> = [
382
+ { id: inboxId, displayName: 'Inbox', wellKnownName: 'inbox' },
383
+ { id: 'AAMkAGI2-synthetic-drafts-folder=', displayName: 'Drafts', wellKnownName: 'drafts' },
384
+ {
385
+ id: 'AAMkAGI2-synthetic-deleteditems-folder=',
386
+ displayName: 'Deleted Items',
387
+ wellKnownName: 'deleteditems'
388
+ },
389
+ { id: pagingFolderId, displayName: 'Synthetic updates', wellKnownName: null }
390
+ ]
391
+
392
+ const pagingMessage = (number: number, hour: number): MicrosoftEmulatorMessageSeed => {
393
+ const id = `AAMkAGI2-synthetic-message-${pad(number, 4)}=`
394
+ const at = `2026-09-22T${pad(hour, 2)}:00:00Z`
395
+
396
+ return {
397
+ id,
398
+ parentFolderId: pagingFolderId,
399
+ subject: `Synthetic update ${number - 100}`,
400
+ bodyContent: 'Synthetic message body.',
401
+ from: grace,
402
+ sender: grace,
403
+ toRecipients: [adaRecipient],
404
+ isRead: true,
405
+ isDraft: false,
406
+ receivedDateTime: at,
407
+ sentDateTime: at,
408
+ createdDateTime: at,
409
+ lastModifiedDateTime: at,
410
+ conversationId: `AAQkAGI2-synthetic-conversation-${pad(number, 4)}=`,
411
+ internetMessageId: `<synthetic-${pad(number, 4)}@example.test>`,
412
+ changeKey: `CQAAABYAAAAsynthetic${pad(number, 4)}`
413
+ }
414
+ }
415
+
416
+ const attachmentMessageId = 'AAMkAGI2-synthetic-message-0001='
417
+
418
+ const defaultMessages: ReadonlyArray<MicrosoftEmulatorMessageSeed> = [
419
+ {
420
+ id: attachmentMessageId,
421
+ parentFolderId: inboxId,
422
+ subject: 'Synthetic message with attachments',
423
+ bodyContentType: 'html',
424
+ bodyContent: '<p>Synthetic body <img src="cid:image001.png@01DD2E00.00000000"></p>',
425
+ from: grace,
426
+ sender: grace,
427
+ toRecipients: [adaRecipient],
428
+ isRead: false,
429
+ isDraft: false,
430
+ // Synthesized (no fixture lists this message): it has a regular (non-inline) attachment.
431
+ hasAttachments: true,
432
+ receivedDateTime: '2026-09-22T08:15:00Z',
433
+ sentDateTime: '2026-09-22T08:15:00Z',
434
+ createdDateTime: '2026-09-22T08:15:00Z',
435
+ lastModifiedDateTime: '2026-09-22T08:15:00Z',
436
+ conversationId: 'AAQkAGI2-synthetic-conversation-0001=',
437
+ internetMessageId: '<synthetic-0001@example.test>',
438
+ changeKey: 'CQAAABYAAAAsynthetic0001'
439
+ },
440
+ pagingMessage(101, 9),
441
+ pagingMessage(102, 8),
442
+ pagingMessage(103, 7)
443
+ ]
444
+
445
+ const defaultAttachments: ReadonlyArray<MicrosoftEmulatorAttachmentSeed> = [
446
+ {
447
+ id: 'AAMkAGI2-synthetic-attachment-0001=',
448
+ messageId: attachmentMessageId,
449
+ name: 'image001.png',
450
+ contentType: 'image/png',
451
+ size: 1024,
452
+ isInline: true,
453
+ contentId: 'image001.png@01DD2E00.00000000',
454
+ contentBytes: 'iVBORw0KGgo=',
455
+ lastModifiedDateTime: '2026-09-22T08:15:00Z'
456
+ },
457
+ {
458
+ id: 'AAMkAGI2-synthetic-attachment-0002=',
459
+ messageId: attachmentMessageId,
460
+ name: 'synthetic-report.pdf',
461
+ contentType: 'application/pdf',
462
+ size: 2048,
463
+ isInline: false,
464
+ contentId: null,
465
+ contentBytes: 'JVBERi0xLjQK',
466
+ lastModifiedDateTime: '2026-09-22T08:15:00Z'
467
+ }
468
+ ]
469
+
470
+ const calendarId = 'AAMkAGI2-synthetic-calendar-0001='
471
+
472
+ const defaultCalendars: ReadonlyArray<MicrosoftEmulatorCalendar> = [
473
+ { id: calendarId, name: 'Calendar' }
474
+ ]
475
+
476
+ const defaultEvents: ReadonlyArray<MicrosoftEmulatorEventSeed> = [
477
+ {
478
+ id: 'AAMkAGI2-synthetic-event-0001=',
479
+ calendarId,
480
+ subject: 'Synthetic planning session',
481
+ start: '2026-09-23T12:00:00Z',
482
+ end: '2026-09-23T13:00:00Z',
483
+ changeKey: 'DwAAABYAAAAsynthetic0001'
484
+ },
485
+ {
486
+ id: 'AAMkAGI2-synthetic-event-0002=',
487
+ calendarId,
488
+ subject: 'Synthetic review',
489
+ start: '2026-09-24T08:30:00Z',
490
+ end: '2026-09-24T09:00:00Z',
491
+ changeKey: 'DwAAABYAAAAsynthetic0002'
492
+ }
493
+ ]
494
+
495
+ const defaultDrive: MicrosoftEmulatorDrive = {
496
+ id: 'b!synthetic-drive-0001',
497
+ driveType: 'business',
498
+ rootId: '01SYNTHETICROOTFOLDER00000000001'
499
+ }
500
+
501
+ const quickXorHash = 'AAAAAAAAAAAAAAAAAAAAAAAAAAA='
502
+
503
+ const conformanceFolderId = '01SYNTHETICPARENTFOLDER0000000001'
504
+
505
+ const sourcesFolderId = '01SYNTHETICSOURCEPARENT000000001'
506
+
507
+ const rootItem: MicrosoftEmulatorDriveItemSeed = {
508
+ id: defaultDrive.rootId,
509
+ parentId: null,
510
+ name: 'root',
511
+ kind: 'folder'
512
+ }
513
+
514
+ const defaultDriveItems: ReadonlyArray<MicrosoftEmulatorDriveItemSeed> = [
515
+ rootItem,
516
+ { id: conformanceFolderId, parentId: defaultDrive.rootId, name: 'Conformance', kind: 'folder' },
517
+ {
518
+ id: '01SYNTHETICEXISTINGFILE000000001',
519
+ parentId: conformanceFolderId,
520
+ name: 'synthetic-existing.txt',
521
+ kind: 'file',
522
+ size: 12,
523
+ mimeType: 'text/plain',
524
+ quickXorHash
525
+ },
526
+ { id: sourcesFolderId, parentId: defaultDrive.rootId, name: 'Sources', kind: 'folder' },
527
+ {
528
+ id: '01SYNTHETICSOURCEFILE00000000001',
529
+ parentId: sourcesFolderId,
530
+ name: 'synthetic-notes.txt',
531
+ kind: 'file',
532
+ size: 24,
533
+ mimeType: 'text/plain',
534
+ quickXorHash
535
+ }
536
+ ]
537
+
538
+ type ProfileEntities = {
539
+ readonly user: MicrosoftEmulatorUser
540
+ readonly mailFolders: ReadonlyArray<MicrosoftEmulatorMailFolder>
541
+ readonly messages: ReadonlyArray<MicrosoftEmulatorMessageSeed>
542
+ readonly attachments: ReadonlyArray<MicrosoftEmulatorAttachmentSeed>
543
+ readonly calendars: ReadonlyArray<MicrosoftEmulatorCalendar>
544
+ readonly events: ReadonlyArray<MicrosoftEmulatorEventSeed>
545
+ readonly drive: MicrosoftEmulatorDrive
546
+ readonly driveItems: ReadonlyArray<MicrosoftEmulatorDriveItemSeed>
547
+ }
548
+
549
+ const profileEntities = (profile: MicrosoftEmulatorProfile): ProfileEntities => {
550
+ switch (profile) {
551
+ case 'default':
552
+ return {
553
+ user: ada,
554
+ mailFolders: defaultMailFolders,
555
+ messages: defaultMessages,
556
+ attachments: defaultAttachments,
557
+ calendars: defaultCalendars,
558
+ events: defaultEvents,
559
+ drive: defaultDrive,
560
+ driveItems: defaultDriveItems
561
+ }
562
+ case 'empty':
563
+ return {
564
+ user: ada,
565
+ mailFolders: defaultMailFolders.filter(folder => folder.wellKnownName !== null),
566
+ messages: [],
567
+ attachments: [],
568
+ calendars: defaultCalendars,
569
+ events: [],
570
+ drive: defaultDrive,
571
+ driveItems: [rootItem]
572
+ }
573
+ }
574
+ }
575
+
576
+ const defaultTimestamp = '2026-09-29T10:00:00Z'
577
+
578
+ const messageFromSeed = (
579
+ seed: MicrosoftEmulatorMessageSeed,
580
+ user: MicrosoftEmulatorUser
581
+ ): MicrosoftEmulatorMessage => {
582
+ const owner: MicrosoftEmulatorRecipient = { name: user.displayName, address: user.mail }
583
+ const created = seed.createdDateTime ?? defaultTimestamp
584
+
585
+ return {
586
+ id: seed.id,
587
+ parentFolderId: seed.parentFolderId,
588
+ subject: seed.subject,
589
+ bodyContentType: seed.bodyContentType ?? 'text',
590
+ bodyContent: seed.bodyContent ?? '',
591
+ from: seed.from === undefined ? owner : seed.from,
592
+ sender: seed.sender === undefined ? owner : seed.sender,
593
+ toRecipients: seed.toRecipients ?? [],
594
+ ccRecipients: seed.ccRecipients ?? [],
595
+ bccRecipients: seed.bccRecipients ?? [],
596
+ replyTo: seed.replyTo ?? [],
597
+ isRead: seed.isRead ?? true,
598
+ isDraft: seed.isDraft ?? false,
599
+ importance: seed.importance ?? 'normal',
600
+ flagStatus: seed.flagStatus ?? 'notFlagged',
601
+ categories: seed.categories ?? [],
602
+ hasAttachments: seed.hasAttachments ?? false,
603
+ createdDateTime: created,
604
+ lastModifiedDateTime: seed.lastModifiedDateTime ?? created,
605
+ receivedDateTime: seed.receivedDateTime ?? created,
606
+ sentDateTime: seed.sentDateTime ?? created,
607
+ conversationId: seed.conversationId ?? `AAQkAGI2-synthetic-conversation-${seed.id}`,
608
+ internetMessageId: seed.internetMessageId ?? `<${seed.id}@example.test>`,
609
+ changeKey: seed.changeKey ?? 'CQAAABYAAAAsynthetic0000'
610
+ }
611
+ }
612
+
613
+ /** Decoded size of a base64 string (no validation beyond the padding count). */
614
+ const base64Size = (value: string): number =>
615
+ Math.max(0, Math.floor((value.length * 3) / 4) - (value.match(/=+$/)?.[0].length ?? 0))
616
+
617
+ const attachmentFromSeed = (seed: MicrosoftEmulatorAttachmentSeed): MicrosoftEmulatorAttachment => {
618
+ const contentBytes = seed.contentBytes ?? ''
619
+
620
+ return {
621
+ id: seed.id,
622
+ messageId: seed.messageId,
623
+ name: seed.name,
624
+ contentType: seed.contentType ?? 'application/octet-stream',
625
+ size: seed.size ?? base64Size(contentBytes),
626
+ isInline: seed.isInline ?? false,
627
+ contentId: seed.contentId ?? null,
628
+ contentLocation: seed.contentLocation ?? null,
629
+ contentBytes,
630
+ lastModifiedDateTime: seed.lastModifiedDateTime ?? defaultTimestamp
631
+ }
632
+ }
633
+
634
+ /** A seeded start/end as a seven-digit UTC local date-time, or `undefined` when unreadable. */
635
+ const seedDateTime = (value: string): string | undefined => {
636
+ const ticks = value.endsWith('Z')
637
+ ? parseInstant(value, 'required')
638
+ : parseInstant(value, 'forbidden')
639
+
640
+ return ticks === undefined ? undefined : formatLocalDateTime(ticks, 7)
641
+ }
642
+
643
+ const eventFromSeed = (
644
+ seed: MicrosoftEmulatorEventSeed,
645
+ start: string,
646
+ end: string
647
+ ): MicrosoftEmulatorEvent => ({
648
+ id: seed.id,
649
+ calendarId: seed.calendarId,
650
+ subject: seed.subject,
651
+ bodyContentType: seed.bodyContentType ?? 'text',
652
+ bodyContent: seed.bodyContent ?? '',
653
+ start,
654
+ end,
655
+ isCancelled: seed.isCancelled ?? false,
656
+ isReminderOn: seed.isReminderOn ?? false,
657
+ showAs: seed.showAs ?? 'busy',
658
+ changeKey: seed.changeKey ?? 'DwAAABYAAAAsynthetic0000',
659
+ createdDateTime: seed.createdDateTime ?? defaultTimestamp,
660
+ lastModifiedDateTime: seed.lastModifiedDateTime ?? seed.createdDateTime ?? defaultTimestamp
661
+ })
662
+
663
+ const driveItemFromSeed = (seed: MicrosoftEmulatorDriveItemSeed): MicrosoftEmulatorDriveItem => ({
664
+ id: seed.id,
665
+ parentId: seed.parentId,
666
+ name: seed.name,
667
+ kind: seed.kind,
668
+ size: seed.kind === 'folder' ? 0 : (seed.size ?? 0),
669
+ mimeType: seed.kind === 'folder' ? null : (seed.mimeType ?? 'application/octet-stream'),
670
+ quickXorHash: seed.kind === 'folder' ? null : (seed.quickXorHash ?? null),
671
+ createdDateTime: seed.createdDateTime ?? defaultTimestamp,
672
+ lastModifiedDateTime: seed.lastModifiedDateTime ?? seed.createdDateTime ?? defaultTimestamp
673
+ })
674
+
675
+ const duplicate = (values: ReadonlyArray<string>): string | undefined =>
676
+ values.find((value, index) => values.indexOf(value) !== index)
677
+
678
+ /** Integrity problems a decoded seed can still have (duplicates, dangling references, cycles). */
679
+ const seedProblem = (entities: ProfileEntities): string | undefined => {
680
+ const folderIds = entities.mailFolders.map(folder => folder.id)
681
+ const messageIds = entities.messages.map(message => message.id)
682
+ const calendarIds = entities.calendars.map(calendar => calendar.id)
683
+ const itemIds = entities.driveItems.map(item => item.id)
684
+
685
+ const duplicates: ReadonlyArray<readonly [string, string | undefined]> = [
686
+ ['mail folder id', duplicate(folderIds)],
687
+ [
688
+ 'well-known folder',
689
+ duplicate(entities.mailFolders.flatMap(folder => folder.wellKnownName ?? []))
690
+ ],
691
+ ['message id', duplicate(messageIds)],
692
+ ['attachment id', duplicate(entities.attachments.map(attachment => attachment.id))],
693
+ ['calendar id', duplicate(calendarIds)],
694
+ ['event id', duplicate(entities.events.map(event => event.id))],
695
+ ['drive item id', duplicate(itemIds)]
696
+ ]
697
+
698
+ for (const [label, value] of duplicates) {
699
+ if (value !== undefined) return `duplicate ${label} ${value}`
700
+ }
701
+
702
+ for (const wellKnown of ['drafts', 'deleteditems'] as const) {
703
+ if (!entities.mailFolders.some(folder => folder.wellKnownName === wellKnown)) {
704
+ return `the mail folders need a ${wellKnown} folder`
705
+ }
706
+ }
707
+
708
+ const message = entities.messages.find(candidate => !folderIds.includes(candidate.parentFolderId))
709
+
710
+ if (message !== undefined) {
711
+ return `message ${message.id} references missing folder ${message.parentFolderId}`
712
+ }
713
+
714
+ const attachment = entities.attachments.find(
715
+ candidate => !messageIds.includes(candidate.messageId)
716
+ )
717
+
718
+ if (attachment !== undefined) {
719
+ return `attachment ${attachment.id} references missing message ${attachment.messageId}`
720
+ }
721
+
722
+ const event = entities.events.find(candidate => !calendarIds.includes(candidate.calendarId))
723
+
724
+ if (event !== undefined) {
725
+ return `event ${event.id} references missing calendar ${event.calendarId}`
726
+ }
727
+
728
+ const roots = entities.driveItems.filter(item => item.parentId === null)
729
+
730
+ if (roots.length !== 1 || roots[0]?.id !== entities.drive.rootId || roots[0].kind !== 'folder') {
731
+ return `the drive items need exactly one root folder with id ${entities.drive.rootId}`
732
+ }
733
+
734
+ const byId = new Map(entities.driveItems.map(item => [item.id, item]))
735
+
736
+ for (const item of entities.driveItems) {
737
+ let parentId = item.parentId
738
+ let steps = 0
739
+
740
+ while (parentId !== null) {
741
+ const parent = byId.get(parentId)
742
+
743
+ if (parent === undefined || parent.kind !== 'folder') {
744
+ return `drive item ${item.id} has a missing or non-folder parent ${parentId}`
745
+ }
746
+
747
+ steps += 1
748
+
749
+ if (steps > entities.driveItems.length) return `drive item ${item.id} is in a parent cycle`
750
+
751
+ parentId = parent.parentId
752
+ }
753
+ }
754
+
755
+ return undefined
756
+ }
757
+
758
+ /** Build the emulator state for a decoded seed; a string is an integrity problem. */
759
+ const stateFromSeed = (seed: MicrosoftEmulatorSeed): MicrosoftEmulatorState | string => {
760
+ const profile = profileEntities(seed.profile ?? 'default')
761
+
762
+ const entities: ProfileEntities = {
763
+ user: seed.user ?? profile.user,
764
+ mailFolders: seed.mailFolders ?? profile.mailFolders,
765
+ messages: seed.messages ?? profile.messages,
766
+ attachments: seed.attachments ?? profile.attachments,
767
+ calendars: seed.calendars ?? profile.calendars,
768
+ events: seed.events ?? profile.events,
769
+ drive: seed.drive ?? profile.drive,
770
+ driveItems: seed.driveItems ?? profile.driveItems
771
+ }
772
+
773
+ const problem = seedProblem(entities)
774
+
775
+ if (problem !== undefined) return problem
776
+
777
+ const events: Array<MicrosoftEmulatorEvent> = []
778
+
779
+ for (const event of entities.events) {
780
+ const start = seedDateTime(event.start)
781
+ const end = seedDateTime(event.end)
782
+
783
+ if (start === undefined || end === undefined || end < start) {
784
+ return `event ${event.id} needs readable UTC start and end, with end not before start`
785
+ }
786
+
787
+ events.push(eventFromSeed(event, start, end))
788
+ }
789
+
790
+ return {
791
+ user: entities.user,
792
+ mailFolders: entities.mailFolders,
793
+ messages: entities.messages.map(message => messageFromSeed(message, entities.user)),
794
+ attachments: entities.attachments.map(attachmentFromSeed),
795
+ calendars: entities.calendars,
796
+ events,
797
+ drive: entities.drive,
798
+ driveItems: entities.driveItems.map(driveItemFromSeed),
799
+ counters: {
800
+ nextEventNumber: 101,
801
+ nextMessageNumber: 1,
802
+ nextChangeKeyNumber: 1001,
803
+ nextItemNumber: 1
804
+ }
805
+ }
806
+ }
807
+
808
+ /** Decode and build a seed; a string is the reason it is invalid. */
809
+ export const buildSeedState = (input: unknown): MicrosoftEmulatorState | string => {
810
+ const decoded = decodeSeedInput(input)
811
+
812
+ return Result.isFailure(decoded)
813
+ ? issueMessage(decoded.failure.issue)
814
+ : stateFromSeed(decoded.success)
815
+ }
816
+
817
+ /** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
818
+ export const decodeState = (input: unknown): MicrosoftEmulatorState | string => {
819
+ const decoded = decodeStateInput(input)
820
+
821
+ return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : { ...decoded.success }
822
+ }