@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,686 @@
1
+ /**
2
+ * Google emulator state: the Gmail messages, labels, attachments, and drafts, the Calendar
3
+ * calendars and events, and the Drive files, the seed input, the default seed, and the profiles
4
+ * (internal; re-exported by `src/google.ts`).
5
+ *
6
+ * Entity shapes and the default entities follow the synthetic Google conformance fixtures and
7
+ * `googleConformanceFixtureSeeds`, copied as data (the same ids, labels, payloads, timestamps, and
8
+ * links), never imported from SDK code. Entities a fixture only names are implied: the paging
9
+ * label (`impliedLabelIds`), the five messages its listing names by id (`impliedMessages`, which
10
+ * only ever render as `{ id, threadId }` list entries), and the practice Drive folder
11
+ * (`impliedFolderIds`, a parent whose own metadata no fixture shows). References resolve through
12
+ * them, but no answer renders them.
13
+ *
14
+ * A stored Gmail message keeps only the renderings a fixture records for it: `minimal` (no
15
+ * payload), and the `metadata` and `full` payloads. A `format` with no recorded rendering is not
16
+ * emulated. Created labels and drafts are never seeded (they exist only once created here).
17
+ *
18
+ * Minted ids never collide with seeded ones: draft and sent message ids (`18f…d<n>`, `18f…e<n>`),
19
+ * which a created draft also uses as its thread id, event ids (`syntheticconformance<nnnn>`), and
20
+ * folder ids (`synthetic-conformance-folder-<nnnn>`) have reserved forms that no seeded message id,
21
+ * thread id, event id, or Drive id may use. Created label ids (`Label_<n>`, `n` from 1 to
22
+ * 999999999 without leading zeros) start above every seeded label number (at least `Label_9101`,
23
+ * the value the label fixture records); a seeded `Label_<digits>` id must have that form, and a
24
+ * create when no number is left is not emulated.
25
+ *
26
+ * @experimental
27
+ */
28
+ import { Result } from 'effect'
29
+ import * as Schema from 'effect/Schema'
30
+ import { recordedInternalDate } from './shared.ts'
31
+
32
+ const JsonObject = Schema.Record(Schema.String, Schema.Json)
33
+
34
+ /** A Gmail message, thread, label, or attachment id as the fixtures use them. */
35
+ const GmailId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{1,100}$/))
36
+
37
+ const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
38
+
39
+ /**
40
+ * The practice account's own address: exactly one plain `local@domain` (the shape the send case's
41
+ * `GooglePracticeAddress` seed takes). The fixtures use it as the sender, the only recipient, and
42
+ * the event creator. A seed may set another one, but the send answers only while it is the
43
+ * recorded `practice@example.test` (the recorded `sizeEstimate` of the sent message covers it).
44
+ */
45
+ export const GoogleEmulatorPracticeAddress = Schema.String.check(
46
+ Schema.isMaxLength(254),
47
+ Schema.isPattern(
48
+ /^[A-Za-z0-9_%+-]+(?:\.[A-Za-z0-9_%+-]+)*@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/
49
+ ),
50
+ Schema.makeFilter(value => value.indexOf('@') <= 64)
51
+ )
52
+
53
+ /**
54
+ * A stored Gmail message with the renderings a fixture records for it: `minimal` (true when a
55
+ * fixture records `format=minimal`), and the `metadata` and `full` payloads (null when none is
56
+ * recorded). The head fields are answered as stored.
57
+ */
58
+ export const GoogleEmulatorGmailMessage = Schema.Struct({
59
+ id: GmailId,
60
+ threadId: GmailId,
61
+ labelIds: Schema.Array(Schema.String),
62
+ snippet: Schema.String,
63
+ sizeEstimate: NonNegativeInt,
64
+ historyId: Schema.String,
65
+ internalDate: Schema.String,
66
+ minimal: Schema.Boolean,
67
+ metadataPayload: Schema.NullOr(JsonObject),
68
+ fullPayload: Schema.NullOr(JsonObject)
69
+ })
70
+
71
+ export type GoogleEmulatorGmailMessage = typeof GoogleEmulatorGmailMessage.Type
72
+
73
+ /** A message a fixture only names in a listing (`{ id, threadId }`), with the labels it carries. */
74
+ export const GoogleEmulatorImpliedMessage = Schema.Struct({
75
+ id: GmailId,
76
+ threadId: GmailId,
77
+ labelIds: Schema.Array(Schema.String)
78
+ })
79
+
80
+ export type GoogleEmulatorImpliedMessage = typeof GoogleEmulatorImpliedMessage.Type
81
+
82
+ /** A label created here (`POST .../labels`), answered as the label fixture records it. */
83
+ export const GoogleEmulatorGmailLabel = Schema.Struct({
84
+ id: GmailId,
85
+ name: Schema.String,
86
+ messageListVisibility: Schema.String,
87
+ labelListVisibility: Schema.String,
88
+ type: Schema.String
89
+ })
90
+
91
+ export type GoogleEmulatorGmailLabel = typeof GoogleEmulatorGmailLabel.Type
92
+
93
+ /** An attachment Gmail stores apart from its message: `size` bytes as base64url `data`. */
94
+ export const GoogleEmulatorAttachment = Schema.Struct({
95
+ messageId: GmailId,
96
+ attachmentId: GmailId,
97
+ size: NonNegativeInt,
98
+ data: Schema.String
99
+ })
100
+
101
+ export type GoogleEmulatorAttachment = typeof GoogleEmulatorAttachment.Type
102
+
103
+ /** A draft created here and its current message. */
104
+ export const GoogleEmulatorDraft = Schema.Struct({
105
+ id: Schema.String,
106
+ messageId: GmailId
107
+ })
108
+
109
+ export type GoogleEmulatorDraft = typeof GoogleEmulatorDraft.Type
110
+
111
+ /** A calendar the events live in, with the fields a listing page answers. */
112
+ export const GoogleEmulatorCalendar = Schema.Struct({
113
+ id: Schema.String,
114
+ summary: Schema.String,
115
+ timeZone: Schema.String,
116
+ accessRole: Schema.String
117
+ })
118
+
119
+ export type GoogleEmulatorCalendar = typeof GoogleEmulatorCalendar.Type
120
+
121
+ /** An event boundary: exactly one of `dateTime` (with an optional `timeZone`) or `date`. */
122
+ export const GoogleEmulatorEventBoundary = Schema.Union([
123
+ Schema.Struct({ dateTime: Schema.String, timeZone: Schema.optionalKey(Schema.String) }),
124
+ Schema.Struct({ date: Schema.String })
125
+ ])
126
+
127
+ export type GoogleEmulatorEventBoundary = typeof GoogleEmulatorEventBoundary.Type
128
+
129
+ /** A stored calendar event with its wire fields (creator and organizer derive from the state). */
130
+ export const GoogleEmulatorCalendarEvent = Schema.Struct({
131
+ calendarId: Schema.String,
132
+ id: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{1,1024}$/)),
133
+ etag: Schema.String,
134
+ status: Schema.Literals(['confirmed', 'cancelled']),
135
+ htmlLink: Schema.String,
136
+ created: Schema.String,
137
+ updated: Schema.String,
138
+ summary: Schema.String,
139
+ description: Schema.optionalKey(Schema.String),
140
+ start: GoogleEmulatorEventBoundary,
141
+ end: GoogleEmulatorEventBoundary,
142
+ iCalUID: Schema.String,
143
+ sequence: NonNegativeInt
144
+ })
145
+
146
+ export type GoogleEmulatorCalendarEvent = typeof GoogleEmulatorCalendarEvent.Type
147
+
148
+ /** A Drive file or folder with the fields the connector's `fields` selection answers. */
149
+ export const GoogleEmulatorDriveFile = Schema.Struct({
150
+ id: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{10,200}$/)),
151
+ name: Schema.String,
152
+ mimeType: Schema.String,
153
+ starred: Schema.Boolean,
154
+ trashed: Schema.Boolean,
155
+ explicitlyTrashed: Schema.Boolean,
156
+ trashedTime: Schema.optionalKey(Schema.String),
157
+ parents: Schema.Array(Schema.String),
158
+ spaces: Schema.Array(Schema.String),
159
+ version: Schema.String,
160
+ webViewLink: Schema.String,
161
+ iconLink: Schema.String,
162
+ hasThumbnail: Schema.Boolean,
163
+ viewedByMe: Schema.Boolean,
164
+ createdTime: Schema.String,
165
+ modifiedTime: Schema.String,
166
+ ownedByMe: Schema.Boolean,
167
+ shared: Schema.Boolean,
168
+ writersCanShare: Schema.Boolean,
169
+ capabilities: JsonObject
170
+ })
171
+
172
+ export type GoogleEmulatorDriveFile = typeof GoogleEmulatorDriveFile.Type
173
+
174
+ const Counter = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
175
+
176
+ const Counters = Schema.Struct({
177
+ /** Next created label number (`Label_<n>`). */
178
+ nextLabelNumber: Counter,
179
+ /** Next created draft number (`r-8<n, 18 digits>`). */
180
+ nextDraftNumber: Counter,
181
+ /** Next draft message number (`18f…d<n>`). */
182
+ nextDraftMessageNumber: Counter,
183
+ /** Next sent message number (`18f…e<n>`, and the `Message-ID` header). */
184
+ nextSentNumber: Counter,
185
+ /** Next created event number (`syntheticconformance<nnnn>`). */
186
+ nextEventNumber: Counter,
187
+ /** Next created folder number (`synthetic-conformance-folder-<nnnn>`). */
188
+ nextFolderNumber: Counter
189
+ })
190
+
191
+ /** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
192
+ export const GoogleEmulatorStateSchema = Schema.Struct({
193
+ practiceAddress: GoogleEmulatorPracticeAddress,
194
+ messages: Schema.Array(GoogleEmulatorGmailMessage),
195
+ impliedMessages: Schema.Array(GoogleEmulatorImpliedMessage),
196
+ impliedLabelIds: Schema.Array(GmailId),
197
+ labels: Schema.Array(GoogleEmulatorGmailLabel),
198
+ attachments: Schema.Array(GoogleEmulatorAttachment),
199
+ drafts: Schema.Array(GoogleEmulatorDraft),
200
+ calendars: Schema.Array(GoogleEmulatorCalendar),
201
+ events: Schema.Array(GoogleEmulatorCalendarEvent),
202
+ files: Schema.Array(GoogleEmulatorDriveFile),
203
+ impliedFolderIds: Schema.Array(Schema.String),
204
+ counters: Counters
205
+ })
206
+
207
+ /**
208
+ * The emulator state. The container is mutable (routes replace whole lists); every entity is
209
+ * replaced, never edited in place.
210
+ */
211
+ export type GoogleEmulatorState = {
212
+ practiceAddress: string
213
+ messages: ReadonlyArray<GoogleEmulatorGmailMessage>
214
+ impliedMessages: ReadonlyArray<GoogleEmulatorImpliedMessage>
215
+ impliedLabelIds: ReadonlyArray<string>
216
+ labels: ReadonlyArray<GoogleEmulatorGmailLabel>
217
+ attachments: ReadonlyArray<GoogleEmulatorAttachment>
218
+ drafts: ReadonlyArray<GoogleEmulatorDraft>
219
+ calendars: ReadonlyArray<GoogleEmulatorCalendar>
220
+ events: ReadonlyArray<GoogleEmulatorCalendarEvent>
221
+ files: ReadonlyArray<GoogleEmulatorDriveFile>
222
+ impliedFolderIds: ReadonlyArray<string>
223
+ counters: typeof Counters.Type
224
+ }
225
+
226
+ /** Account-variance profiles for the default seed. */
227
+ export const GoogleEmulatorProfile = Schema.Literals(['default', 'empty'])
228
+
229
+ export type GoogleEmulatorProfile = typeof GoogleEmulatorProfile.Type
230
+
231
+ /**
232
+ * A typed seed. Start from `profile` (default `'default'`, the fixture entities); every other key,
233
+ * when given, replaces that part of the profile. Created labels and drafts are never seeded.
234
+ */
235
+ export const GoogleEmulatorSeed = Schema.Struct({
236
+ profile: Schema.optionalKey(GoogleEmulatorProfile),
237
+ practiceAddress: Schema.optionalKey(GoogleEmulatorPracticeAddress),
238
+ messages: Schema.optionalKey(Schema.Array(GoogleEmulatorGmailMessage)),
239
+ impliedMessages: Schema.optionalKey(Schema.Array(GoogleEmulatorImpliedMessage)),
240
+ impliedLabelIds: Schema.optionalKey(Schema.Array(GmailId)),
241
+ attachments: Schema.optionalKey(Schema.Array(GoogleEmulatorAttachment)),
242
+ calendars: Schema.optionalKey(Schema.Array(GoogleEmulatorCalendar)),
243
+ events: Schema.optionalKey(Schema.Array(GoogleEmulatorCalendarEvent)),
244
+ files: Schema.optionalKey(Schema.Array(GoogleEmulatorDriveFile)),
245
+ impliedFolderIds: Schema.optionalKey(Schema.Array(Schema.String))
246
+ })
247
+
248
+ export type GoogleEmulatorSeed = typeof GoogleEmulatorSeed.Type
249
+
250
+ const strict = { onExcessProperty: 'error' } as const
251
+
252
+ const decodeSeedInput = Schema.decodeUnknownResult(GoogleEmulatorSeed, strict)
253
+
254
+ const decodeStateInput = Schema.decodeUnknownResult(GoogleEmulatorStateSchema, strict)
255
+
256
+ const issueMessage = (issue: Schema.SchemaError['issue']): string =>
257
+ new Schema.SchemaError(issue).message
258
+
259
+ // Minted forms (reserved: a seed may not use them).
260
+
261
+ export const pad = (value: number, width: number): string => String(value).padStart(width, '0')
262
+
263
+ /** A minted message id: `18f`, then `d<n>` (draft) or `e<n>` (sent), zero-padded to 16. */
264
+ export const mintedMessageId = (kind: 'd' | 'e', number: number): string =>
265
+ `18f${`${kind}${number}`.padStart(13, '0')}`
266
+
267
+ export const mintedMessageIdPattern = /^18f0*[de]\d+$/
268
+
269
+ /** A minted draft id: `r-8` and 18 digits (`r-8000000000000000001` first). */
270
+ export const mintedDraftId = (number: number): string => `r-8${pad(number, 18)}`
271
+
272
+ export const mintedEventId = (number: number): string => `syntheticconformance${pad(number, 4)}`
273
+
274
+ export const mintedEventIdPattern = /^syntheticconformance\d+$/
275
+
276
+ export const mintedFolderId = (number: number): string =>
277
+ `synthetic-conformance-folder-${pad(number, 4)}`
278
+
279
+ export const mintedFolderIdPattern = /^synthetic-conformance-folder-\d+$/
280
+
281
+ /** The label number the label fixture records for the first created label. */
282
+ const firstLabelNumber = 9101
283
+
284
+ /** The highest label number a created (minted) label id may carry: `Label_999999999`. */
285
+ export const lastLabelNumber = 999_999_999
286
+
287
+ /**
288
+ * The minted label id form, `Label_<1 to 999999999>` without leading zeros. A seeded label id of
289
+ * the `Label_<digits>` shape must have this form, so the created-label counter, which starts above
290
+ * every seeded label number, never mints an id a seed holds.
291
+ */
292
+ export const mintedLabelIdPattern = /^Label_([1-9]\d{0,8})$/
293
+
294
+ const labelDigitsPattern = /^Label_\d+$/
295
+
296
+ /** A Gmail system label (upper case), as opposed to a user label id such as `Label_9001`. */
297
+ export const isSystemLabel = (id: string): boolean => /^[A-Z][A-Z_]*$/.test(id)
298
+
299
+ // Default entities, copied from the fixtures and `googleConformanceFixtureSeeds`.
300
+
301
+ /** The practice account (`practice@example.test`) of every fixture. */
302
+ export const googleEmulatorDefaultPracticeAddress = 'practice@example.test'
303
+
304
+ /** Base64url without padding of UTF-8 text, as the fixtures encode message parts. */
305
+ export const base64UrlOfText = (text: string): string => {
306
+ let binary = ''
307
+
308
+ for (const byte of new TextEncoder().encode(text)) {
309
+ binary += String.fromCharCode(byte)
310
+ }
311
+
312
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
313
+ }
314
+
315
+ /** The seeded work message (`format=minimal`, as the label and trash fixtures read it). */
316
+ const workMessage: GoogleEmulatorGmailMessage = {
317
+ id: '18f00000000000b1',
318
+ threadId: '18f00000000000b1',
319
+ labelIds: ['INBOX', 'IMPORTANT'],
320
+ snippet: 'Synthetic practice message.',
321
+ sizeEstimate: 2048,
322
+ historyId: '900001',
323
+ internalDate: recordedInternalDate,
324
+ minimal: true,
325
+ metadataPayload: null,
326
+ fullPayload: null
327
+ }
328
+
329
+ const attachmentText = 'Synthetic practice message with an attachment.'
330
+
331
+ const attachmentId = 'ANGjdJ_synthetic_attachment_0001'
332
+
333
+ /** The seeded attachment message (`format=full`, as the attachment fixture reads it). */
334
+ const attachmentMessage: GoogleEmulatorGmailMessage = {
335
+ id: '18f00000000000a1',
336
+ threadId: '18f00000000000a1',
337
+ labelIds: ['INBOX'],
338
+ snippet: attachmentText,
339
+ sizeEstimate: 4096,
340
+ historyId: '900010',
341
+ internalDate: recordedInternalDate,
342
+ minimal: false,
343
+ metadataPayload: null,
344
+ fullPayload: {
345
+ partId: '',
346
+ mimeType: 'multipart/mixed',
347
+ filename: '',
348
+ headers: [
349
+ { name: 'Subject', value: 'Synthetic practice attachment' },
350
+ { name: 'Content-Type', value: 'multipart/mixed; boundary="synthetic"' }
351
+ ],
352
+ body: { size: 0 },
353
+ parts: [
354
+ {
355
+ partId: '0',
356
+ mimeType: 'text/plain',
357
+ filename: '',
358
+ headers: [{ name: 'Content-Type', value: 'text/plain; charset="UTF-8"' }],
359
+ body: { size: attachmentText.length, data: base64UrlOfText(attachmentText) }
360
+ },
361
+ {
362
+ partId: '1',
363
+ mimeType: 'application/octet-stream',
364
+ filename: 'synthetic.bin',
365
+ headers: [
366
+ { name: 'Content-Type', value: 'application/octet-stream; name="synthetic.bin"' },
367
+ { name: 'Content-Disposition', value: 'attachment; filename="synthetic.bin"' },
368
+ { name: 'Content-Transfer-Encoding', value: 'base64' }
369
+ ],
370
+ body: { attachmentId, size: 6 }
371
+ }
372
+ ]
373
+ }
374
+ }
375
+
376
+ const pagingLabelId = 'Label_9001'
377
+
378
+ /** The five messages the paging fixture names (listing entries only). */
379
+ const pagingMessages: ReadonlyArray<GoogleEmulatorImpliedMessage> = [1, 2, 3, 4, 5].map(index => ({
380
+ id: `18f00000000000c${index}`,
381
+ threadId: `18f00000000000c${index}`,
382
+ labelIds: [pagingLabelId]
383
+ }))
384
+
385
+ const calendarId = 'practice-calendar@example.test'
386
+
387
+ const practiceCalendar: GoogleEmulatorCalendar = {
388
+ id: calendarId,
389
+ summary: 'Synthetic practice calendar',
390
+ timeZone: 'UTC',
391
+ accessRole: 'owner'
392
+ }
393
+
394
+ const seededAt = '2026-09-30T12:00:00.000Z'
395
+
396
+ /** An event with the fixture's derived fields (`etag`, `htmlLink`, `iCalUID`). */
397
+ export const eventFields = (
398
+ id: string
399
+ ): Pick<GoogleEmulatorCalendarEvent, 'etag' | 'htmlLink' | 'iCalUID'> => ({
400
+ etag: `"3${id.length}000000000000"`,
401
+ htmlLink: `https://www.google.com/calendar/event?eid=${id}`,
402
+ iCalUID: `${id}@google.com`
403
+ })
404
+
405
+ const rangeEvent = (
406
+ id: string,
407
+ summary: string,
408
+ start: GoogleEmulatorEventBoundary,
409
+ end: GoogleEmulatorEventBoundary
410
+ ): GoogleEmulatorCalendarEvent => ({
411
+ calendarId,
412
+ id,
413
+ ...eventFields(id),
414
+ status: 'confirmed',
415
+ created: seededAt,
416
+ updated: seededAt,
417
+ summary,
418
+ start,
419
+ end,
420
+ sequence: 0
421
+ })
422
+
423
+ const utc = (dateTime: string): GoogleEmulatorEventBoundary => ({ dateTime, timeZone: 'UTC' })
424
+
425
+ /** The five events of the range fixture (one all-day, two crossing the range edges). */
426
+ const rangeEvents: ReadonlyArray<GoogleEmulatorCalendarEvent> = [
427
+ rangeEvent(
428
+ 'syntheticrange0001',
429
+ 'Synthetic overnight',
430
+ utc('2026-08-31T23:30:00Z'),
431
+ utc('2026-09-01T00:30:00Z')
432
+ ),
433
+ rangeEvent(
434
+ 'syntheticrange0002',
435
+ 'Synthetic standup',
436
+ utc('2026-09-02T09:00:00Z'),
437
+ utc('2026-09-02T09:15:00Z')
438
+ ),
439
+ rangeEvent(
440
+ 'syntheticrange0003',
441
+ 'Synthetic review',
442
+ utc('2026-09-03T14:00:00Z'),
443
+ utc('2026-09-03T15:00:00Z')
444
+ ),
445
+ rangeEvent(
446
+ 'syntheticrange0004',
447
+ 'Synthetic all-day',
448
+ { date: '2026-09-05' },
449
+ { date: '2026-09-06' }
450
+ ),
451
+ rangeEvent(
452
+ 'syntheticrange0005',
453
+ 'Synthetic late',
454
+ utc('2026-09-07T23:00:00Z'),
455
+ utc('2026-09-08T01:00:00Z')
456
+ )
457
+ ]
458
+
459
+ /** The folder mime type Drive answers. */
460
+ export const driveFolderMimeType = 'application/vnd.google-apps.folder'
461
+
462
+ const practiceFolderId = 'synthetic-practice-folder-0001'
463
+
464
+ /** A Drive item with the fixture's derived and constant fields. */
465
+ export const driveFile = (fields: {
466
+ readonly id: string
467
+ readonly name: string
468
+ readonly mimeType: string
469
+ readonly parents: ReadonlyArray<string>
470
+ readonly createdTime: string
471
+ }): GoogleEmulatorDriveFile => ({
472
+ id: fields.id,
473
+ name: fields.name,
474
+ mimeType: fields.mimeType,
475
+ starred: false,
476
+ trashed: false,
477
+ explicitlyTrashed: false,
478
+ parents: [...fields.parents],
479
+ spaces: ['drive'],
480
+ version: '3',
481
+ webViewLink: `https://drive.google.com/drive/folders/${fields.id}`,
482
+ iconLink: `https://drive-thirdparty.googleusercontent.com/16/type/${fields.mimeType}`,
483
+ hasThumbnail: false,
484
+ viewedByMe: true,
485
+ createdTime: fields.createdTime,
486
+ modifiedTime: fields.createdTime,
487
+ ownedByMe: true,
488
+ shared: false,
489
+ writersCanShare: true,
490
+ capabilities: {
491
+ canAddChildren: fields.mimeType === driveFolderMimeType,
492
+ canDelete: true,
493
+ canEdit: true,
494
+ canRename: true,
495
+ canTrash: true,
496
+ canUntrash: true
497
+ }
498
+ })
499
+
500
+ const child = (id: string, name: string, mimeType: string) =>
501
+ driveFile({ id, name, mimeType, parents: [practiceFolderId], createdTime: seededAt })
502
+
503
+ /** The five children of the practice folder (the first is the seeded practice file). */
504
+ const practiceChildren: ReadonlyArray<GoogleEmulatorDriveFile> = [
505
+ child('synthetic-practice-file-0001', 'Synthetic practice notes.txt', 'text/plain'),
506
+ child('synthetic-practice-file-0002', 'Synthetic practice sheet.csv', 'text/csv'),
507
+ child('synthetic-practice-file-0003', 'Synthetic practice image.png', 'image/png'),
508
+ child('synthetic-practice-folder-0002', 'Synthetic practice subfolder', driveFolderMimeType),
509
+ child('synthetic-practice-file-0004', 'Synthetic practice report.pdf', 'application/pdf')
510
+ ]
511
+
512
+ type ProfileEntities = Omit<GoogleEmulatorState, 'counters' | 'labels' | 'drafts'>
513
+
514
+ const profileEntities = (profile: GoogleEmulatorProfile): ProfileEntities =>
515
+ profile === 'default'
516
+ ? {
517
+ practiceAddress: googleEmulatorDefaultPracticeAddress,
518
+ messages: [attachmentMessage, workMessage],
519
+ impliedMessages: pagingMessages,
520
+ impliedLabelIds: [pagingLabelId],
521
+ attachments: [{ messageId: attachmentMessage.id, attachmentId, size: 6, data: '-_-_Pj_-' }],
522
+ calendars: [practiceCalendar],
523
+ events: rangeEvents,
524
+ files: practiceChildren,
525
+ impliedFolderIds: [practiceFolderId]
526
+ }
527
+ : {
528
+ practiceAddress: googleEmulatorDefaultPracticeAddress,
529
+ messages: [],
530
+ impliedMessages: [],
531
+ impliedLabelIds: [],
532
+ attachments: [],
533
+ calendars: [],
534
+ events: [],
535
+ files: [],
536
+ impliedFolderIds: []
537
+ }
538
+
539
+ const duplicate = (values: ReadonlyArray<string>): string | undefined =>
540
+ values.find((value, index) => values.indexOf(value) !== index)
541
+
542
+ /** Integrity problems a decoded seed can still have (duplicates, minted forms, references). */
543
+ const seedProblem = (entities: ProfileEntities): string | undefined => {
544
+ const messageIds = [
545
+ ...entities.messages.map(message => message.id),
546
+ ...entities.impliedMessages.map(message => message.id)
547
+ ]
548
+
549
+ const fileIds = entities.files.map(file => file.id)
550
+
551
+ const duplicates: ReadonlyArray<readonly [string, string | undefined]> = [
552
+ ['message id', duplicate(messageIds)],
553
+ ['label id', duplicate(entities.impliedLabelIds)],
554
+ [
555
+ 'attachment',
556
+ duplicate(entities.attachments.map(item => `${item.messageId} ${item.attachmentId}`))
557
+ ],
558
+ ['calendar id', duplicate(entities.calendars.map(calendar => calendar.id))],
559
+ ['event', duplicate(entities.events.map(event => `${event.calendarId} ${event.id}`))],
560
+ ['Drive id', duplicate([...fileIds, ...entities.impliedFolderIds])]
561
+ ]
562
+
563
+ for (const [label, value] of duplicates) {
564
+ if (value !== undefined) return `duplicate ${label} ${value}`
565
+ }
566
+
567
+ const minted =
568
+ messageIds.find(id => mintedMessageIdPattern.test(id)) ??
569
+ entities.events.map(event => event.id).find(id => mintedEventIdPattern.test(id)) ??
570
+ [...fileIds, ...entities.impliedFolderIds].find(id => mintedFolderIdPattern.test(id))
571
+
572
+ if (minted !== undefined) return `seed id ${minted} uses a form reserved for minted ids`
573
+
574
+ // A thread id shares the minted message id forms (a created draft is its own thread).
575
+ const mintedThread = [...entities.messages, ...entities.impliedMessages]
576
+ .map(message => message.threadId)
577
+ .find(id => mintedMessageIdPattern.test(id))
578
+
579
+ if (mintedThread !== undefined) {
580
+ return `seed thread id ${mintedThread} uses a form reserved for minted ids`
581
+ }
582
+
583
+ const outsideRange = [
584
+ ...entities.impliedLabelIds,
585
+ ...[...entities.messages, ...entities.impliedMessages].flatMap(message => message.labelIds)
586
+ ].find(id => labelDigitsPattern.test(id) && !mintedLabelIdPattern.test(id))
587
+
588
+ if (outsideRange !== undefined) {
589
+ return `seed label id ${outsideRange} is outside the Label_<1 to ${lastLabelNumber}> form`
590
+ }
591
+
592
+ const knownLabels = new Set(entities.impliedLabelIds)
593
+
594
+ for (const message of [...entities.messages, ...entities.impliedMessages]) {
595
+ const unknown = message.labelIds.find(id => !isSystemLabel(id) && !knownLabels.has(id))
596
+
597
+ if (unknown !== undefined) {
598
+ return `message ${message.id} carries label ${unknown}, which is not in impliedLabelIds`
599
+ }
600
+ }
601
+
602
+ const attachment = entities.attachments.find(
603
+ item => !entities.messages.some(message => message.id === item.messageId)
604
+ )
605
+
606
+ if (attachment !== undefined) {
607
+ return `attachment ${attachment.attachmentId} references missing message ${attachment.messageId}`
608
+ }
609
+
610
+ const event = entities.events.find(
611
+ item => !entities.calendars.some(calendar => calendar.id === item.calendarId)
612
+ )
613
+
614
+ if (event !== undefined) {
615
+ return `event ${event.id} references missing calendar ${event.calendarId}`
616
+ }
617
+
618
+ const folders = new Set([
619
+ ...entities.files.filter(file => file.mimeType === driveFolderMimeType).map(file => file.id),
620
+ ...entities.impliedFolderIds
621
+ ])
622
+
623
+ const orphan = entities.files.find(file => file.parents.some(parent => !folders.has(parent)))
624
+
625
+ return orphan === undefined
626
+ ? undefined
627
+ : `Drive item ${orphan.id} references a missing parent folder`
628
+ }
629
+
630
+ /** Build the emulator state for a decoded seed; a string is an integrity problem. */
631
+ const stateFromSeed = (seed: GoogleEmulatorSeed): GoogleEmulatorState | string => {
632
+ const profile = profileEntities(seed.profile ?? 'default')
633
+
634
+ const entities: ProfileEntities = {
635
+ practiceAddress: seed.practiceAddress ?? profile.practiceAddress,
636
+ messages: seed.messages ?? profile.messages,
637
+ impliedMessages: seed.impliedMessages ?? profile.impliedMessages,
638
+ impliedLabelIds: seed.impliedLabelIds ?? profile.impliedLabelIds,
639
+ attachments: seed.attachments ?? profile.attachments,
640
+ calendars: seed.calendars ?? profile.calendars,
641
+ events: seed.events ?? profile.events,
642
+ files: seed.files ?? profile.files,
643
+ impliedFolderIds: seed.impliedFolderIds ?? profile.impliedFolderIds
644
+ }
645
+
646
+ const problem = seedProblem(entities)
647
+
648
+ if (problem !== undefined) return problem
649
+
650
+ // Above every seeded label number and never below the recorded one (each at most nine digits,
651
+ // checked in `seedProblem`).
652
+ const highestLabel = Math.max(
653
+ firstLabelNumber - 1,
654
+ ...entities.impliedLabelIds.map(id => Number(mintedLabelIdPattern.exec(id)?.[1] ?? 0))
655
+ )
656
+
657
+ return {
658
+ ...entities,
659
+ labels: [],
660
+ drafts: [],
661
+ counters: {
662
+ nextLabelNumber: highestLabel + 1,
663
+ nextDraftNumber: 1,
664
+ nextDraftMessageNumber: 1,
665
+ nextSentNumber: 1,
666
+ nextEventNumber: 1,
667
+ nextFolderNumber: 1
668
+ }
669
+ }
670
+ }
671
+
672
+ /** Decode and build a seed; a string is the reason it is invalid. */
673
+ export const buildSeedState = (input: unknown): GoogleEmulatorState | string => {
674
+ const decoded = decodeSeedInput(input)
675
+
676
+ return Result.isFailure(decoded)
677
+ ? issueMessage(decoded.failure.issue)
678
+ : stateFromSeed(decoded.success)
679
+ }
680
+
681
+ /** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
682
+ export const decodeState = (input: unknown): GoogleEmulatorState | string => {
683
+ const decoded = decodeStateInput(input)
684
+
685
+ return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : { ...decoded.success }
686
+ }