@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,364 @@
1
+ /**
2
+ * Dropbox emulator state: the typed entries, deleted-entry records, the seed input, the default
3
+ * seed, and the profiles (internal; re-exported by `src/dropbox.ts`).
4
+ *
5
+ * Entry shapes and the default entries follow the synthetic Dropbox conformance fixtures, copied
6
+ * as data (the same paths, ids, revs, sizes, content hashes, and timestamps as the fixtures and
7
+ * `dropboxConformanceFixtureSeeds`), never imported from SDK code. The parent folders the seeded
8
+ * paths need (`/Conformance`, `/Conformance/Paging`, `/Conformance/Search`, `/Conformance/Work`)
9
+ * are `implied`: no fixture shows their metadata, so they hold paths together but any answer that
10
+ * would render one is not emulated.
11
+ *
12
+ * Minted values never repeat seeded ones: the id and rev counters start above the highest seeded
13
+ * id (`id:SyntheticEntryNNNNNNNN`) and rev (`a1b2c3d4e5f6NNNN`) in the minted form.
14
+ *
15
+ * @experimental
16
+ */
17
+ import { Predicate, Result } from 'effect'
18
+ import * as Schema from 'effect/Schema'
19
+
20
+ /** A Dropbox revision: lower-case hex, at least nine digits (what the connector accepts). */
21
+ const Rev = Schema.String.check(Schema.isPattern(/^[0-9a-f]{9,}$/))
22
+
23
+ /** A Dropbox file or folder id (`id:` form). */
24
+ const EntryId = Schema.String.check(Schema.isPattern(/^id:\S+$/))
25
+
26
+ /** A Dropbox timestamp as the fixtures write it (`2026-09-20T10:00:00Z`). */
27
+ const Timestamp = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/))
28
+
29
+ /** One path component: non-empty, no `/`, not `.` or `..`. */
30
+ const EntryName = Schema.String.check(
31
+ Schema.makeFilter(name =>
32
+ name.length > 0 && !name.includes('/') && name !== '.' && name !== '..'
33
+ ? true
34
+ : 'an entry name is one non-empty path component'
35
+ )
36
+ )
37
+
38
+ /** File fields (absent on folders). */
39
+ export const DropboxEmulatorFile = Schema.Struct({
40
+ rev: Rev,
41
+ size: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
42
+ contentHash: Schema.String,
43
+ clientModified: Timestamp,
44
+ serverModified: Timestamp
45
+ })
46
+
47
+ export type DropboxEmulatorFile = typeof DropboxEmulatorFile.Type
48
+
49
+ /** A stored file or folder. Its path is its parents' names and its own, in stored casing. */
50
+ export const DropboxEmulatorEntry = Schema.Struct({
51
+ id: EntryId,
52
+ /** Parent folder id; `null` in the root folder. */
53
+ parentId: Schema.NullOr(EntryId),
54
+ name: EntryName,
55
+ /** `null` for a folder. */
56
+ file: Schema.NullOr(DropboxEmulatorFile),
57
+ /**
58
+ * `true` for an entry no fixture shows, held only because a seeded path needs it: lookups pass
59
+ * through it, but an answer that would render it is not emulated.
60
+ */
61
+ implied: Schema.Boolean
62
+ })
63
+
64
+ export type DropboxEmulatorEntry = typeof DropboxEmulatorEntry.Type
65
+
66
+ /**
67
+ * What Dropbox keeps of a deleted entry (`include_deleted` lookups answer it): its name and paths
68
+ * as they were. `delete_v2` records one only for an empty folder, the only delete whose
69
+ * `include_deleted` answer a fixture records.
70
+ */
71
+ export const DropboxEmulatorDeleted = Schema.Struct({
72
+ name: EntryName,
73
+ pathLower: Schema.String,
74
+ pathDisplay: Schema.String
75
+ })
76
+
77
+ export type DropboxEmulatorDeleted = typeof DropboxEmulatorDeleted.Type
78
+
79
+ const Counter = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
80
+
81
+ const Counters = Schema.Struct({
82
+ /** Next number in created entry ids (`id:SyntheticEntry00000001`). */
83
+ nextIdNumber: Counter,
84
+ /** Next number in minted revs (`a1b2c3d4e5f60101`). */
85
+ nextRevNumber: Counter
86
+ })
87
+
88
+ /** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
89
+ export const DropboxEmulatorStateSchema = Schema.Struct({
90
+ entries: Schema.Array(DropboxEmulatorEntry),
91
+ deleted: Schema.Array(DropboxEmulatorDeleted),
92
+ counters: Counters
93
+ })
94
+
95
+ /**
96
+ * The emulator state. The container is mutable (routes replace whole lists); every entity is
97
+ * replaced, never edited in place.
98
+ */
99
+ export type DropboxEmulatorState = {
100
+ entries: ReadonlyArray<DropboxEmulatorEntry>
101
+ deleted: ReadonlyArray<DropboxEmulatorDeleted>
102
+ counters: typeof Counters.Type
103
+ }
104
+
105
+ /** An absolute path in display casing: `/`-separated non-empty components, no trailing `/`. */
106
+ const SeedPath = Schema.String.check(Schema.isPattern(/^(?:\/[^/]+)+$/))
107
+
108
+ /**
109
+ * A seeded entry, addressed by its display path (its parent folder must be seeded too). File
110
+ * fields default to synthetic values; a folder takes none.
111
+ */
112
+ export const DropboxEmulatorEntrySeed = Schema.Struct({
113
+ path: SeedPath,
114
+ id: EntryId,
115
+ kind: Schema.Literals(['file', 'folder']),
116
+ rev: Schema.optionalKey(Rev),
117
+ size: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
118
+ contentHash: Schema.optionalKey(Schema.String),
119
+ clientModified: Schema.optionalKey(Timestamp),
120
+ serverModified: Schema.optionalKey(Timestamp),
121
+ /** An entry no fixture shows (default `false`); see `DropboxEmulatorEntry.implied`. */
122
+ implied: Schema.optionalKey(Schema.Boolean)
123
+ })
124
+
125
+ export type DropboxEmulatorEntrySeed = typeof DropboxEmulatorEntrySeed.Type
126
+
127
+ /** Account-variance profiles for the default seed. */
128
+ export const DropboxEmulatorProfile = Schema.Literals(['default', 'empty'])
129
+
130
+ export type DropboxEmulatorProfile = typeof DropboxEmulatorProfile.Type
131
+
132
+ /**
133
+ * A typed seed. Start from `profile` (default `'default'`, the fixture entries); `entries` and
134
+ * `deleted`, when given, replace that part of the profile.
135
+ */
136
+ export const DropboxEmulatorSeed = Schema.Struct({
137
+ profile: Schema.optionalKey(DropboxEmulatorProfile),
138
+ entries: Schema.optionalKey(Schema.Array(DropboxEmulatorEntrySeed)),
139
+ deleted: Schema.optionalKey(Schema.Array(DropboxEmulatorDeleted))
140
+ })
141
+
142
+ export type DropboxEmulatorSeed = typeof DropboxEmulatorSeed.Type
143
+
144
+ const strict = { onExcessProperty: 'error' } as const
145
+
146
+ const decodeSeedInput = Schema.decodeUnknownResult(DropboxEmulatorSeed, strict)
147
+
148
+ const decodeStateInput = Schema.decodeUnknownResult(DropboxEmulatorStateSchema, strict)
149
+
150
+ const issueMessage = (issue: Schema.SchemaError['issue']): string =>
151
+ new Schema.SchemaError(issue).message
152
+
153
+ /** A synthetic 64-digit content hash ending in `suffix`, as the fixtures write them. */
154
+ export const syntheticContentHash = (suffix: string): string => suffix.padStart(64, '0')
155
+
156
+ // Default entries. Fixture-derived: every file, the `paging-two` folder, and every path, id, rev,
157
+ // size, content hash, and timestamp of them. Implied (never rendered): the `/Conformance`,
158
+ // `/Conformance/Paging`, `/Conformance/Search`, and `/Conformance/Work` folders.
159
+
160
+ const file = (
161
+ path: string,
162
+ id: string,
163
+ revSuffix: string,
164
+ size: number,
165
+ hashSuffix: string,
166
+ clientModified: string,
167
+ serverModified: string = clientModified
168
+ ): DropboxEmulatorEntrySeed => ({
169
+ path,
170
+ id,
171
+ kind: 'file',
172
+ rev: `a1b2c3d4e5f6${revSuffix}`,
173
+ size,
174
+ contentHash: syntheticContentHash(hashSuffix),
175
+ clientModified,
176
+ serverModified
177
+ })
178
+
179
+ const folder = (path: string, id: string): DropboxEmulatorEntrySeed => ({
180
+ path,
181
+ id,
182
+ kind: 'folder'
183
+ })
184
+
185
+ /** A parent folder no fixture shows. */
186
+ const impliedFolder = (path: string, id: string): DropboxEmulatorEntrySeed => ({
187
+ ...folder(path, id),
188
+ implied: true
189
+ })
190
+
191
+ const defaultEntries: ReadonlyArray<DropboxEmulatorEntrySeed> = [
192
+ impliedFolder('/Conformance', 'id:SyntheticConformanceFolder'),
193
+ impliedFolder('/Conformance/Paging', 'id:SyntheticPagingFolder0000'),
194
+ file(
195
+ '/Conformance/Paging/paging-one.txt',
196
+ 'id:SyntheticPagingFile0001',
197
+ '0001',
198
+ 12,
199
+ '1',
200
+ '2026-09-20T10:00:00Z'
201
+ ),
202
+ folder('/Conformance/Paging/paging-two', 'id:SyntheticPagingFolder0002'),
203
+ file(
204
+ '/Conformance/Paging/paging-three.txt',
205
+ 'id:SyntheticPagingFile0003',
206
+ '0003',
207
+ 14,
208
+ '3',
209
+ '2026-09-20T10:05:00Z'
210
+ ),
211
+ file(
212
+ '/Conformance/Mixed Case Notes.txt',
213
+ 'id:SyntheticMixedCaseFile01',
214
+ '0010',
215
+ 20,
216
+ '10',
217
+ '2026-09-20T11:00:00Z'
218
+ ),
219
+ impliedFolder('/Conformance/Search', 'id:SyntheticSearchFolder0000'),
220
+ file(
221
+ '/Conformance/Search/yolk-search-probe-1.txt',
222
+ 'id:SyntheticSearchFile0001',
223
+ '0021',
224
+ 16,
225
+ '21',
226
+ '2026-09-20T12:00:00Z'
227
+ ),
228
+ file(
229
+ '/Conformance/Search/yolk-search-probe-2.txt',
230
+ 'id:SyntheticSearchFile0002',
231
+ '0022',
232
+ 16,
233
+ '22',
234
+ '2026-09-20T12:00:00Z'
235
+ ),
236
+ impliedFolder('/Conformance/Work', 'id:SyntheticWorkFolder00000'),
237
+ file(
238
+ '/Conformance/copy-source.txt',
239
+ 'id:SyntheticCopySource001',
240
+ '0030',
241
+ 42,
242
+ '42',
243
+ '2026-09-20T13:00:00Z',
244
+ '2026-09-29T13:00:00Z'
245
+ )
246
+ ]
247
+
248
+ const profileEntries = (
249
+ profile: DropboxEmulatorProfile
250
+ ): ReadonlyArray<DropboxEmulatorEntrySeed> => (profile === 'default' ? defaultEntries : [])
251
+
252
+ const defaultTimestamp = '2026-09-20T10:00:00Z'
253
+
254
+ /** The form of minted ids (`id:SyntheticEntry00000001`) and revs (`a1b2c3d4e5f60101`). */
255
+ const mintedIdPattern = /^id:SyntheticEntry(\d+)$/
256
+
257
+ const mintedRevPattern = /^a1b2c3d4e5f6(\d+)$/
258
+
259
+ /** The highest number among `values` in a minted form (0 when none is). */
260
+ const highestMinted = (values: ReadonlyArray<string>, pattern: RegExp): number =>
261
+ Math.max(0, ...values.map(value => Number(pattern.exec(value)?.[1] ?? 0)))
262
+
263
+ const duplicate = (values: ReadonlyArray<string>): string | undefined =>
264
+ values.find((value, index) => values.indexOf(value) !== index)
265
+
266
+ const parentPath = (path: string): string => path.slice(0, path.lastIndexOf('/'))
267
+
268
+ const lastName = (path: string): string => path.slice(path.lastIndexOf('/') + 1)
269
+
270
+ /** Build the emulator state for a decoded seed; a string is an integrity problem. */
271
+ const stateFromSeed = (seed: DropboxEmulatorSeed): DropboxEmulatorState | string => {
272
+ const seeded = seed.entries ?? profileEntries(seed.profile ?? 'default')
273
+
274
+ const duplicates: ReadonlyArray<readonly [string, string | undefined]> = [
275
+ ['entry id', duplicate(seeded.map(entry => entry.id))],
276
+ ['entry path', duplicate(seeded.map(entry => entry.path.toLowerCase()))]
277
+ ]
278
+
279
+ for (const [label, value] of duplicates) {
280
+ if (value !== undefined) return `duplicate ${label} ${value}`
281
+ }
282
+
283
+ const byPath = new Map(seeded.map(entry => [entry.path.toLowerCase(), entry]))
284
+ const entries: Array<DropboxEmulatorEntry> = []
285
+
286
+ for (const entry of seeded) {
287
+ const name = lastName(entry.path)
288
+
289
+ if (name === '.' || name === '..') return `entry ${entry.id} has an invalid path`
290
+
291
+ const parent = parentPath(entry.path)
292
+ const parentEntry = parent === '' ? undefined : byPath.get(parent.toLowerCase())
293
+
294
+ if (parent !== '' && (parentEntry === undefined || parentEntry.kind !== 'folder')) {
295
+ return `entry ${entry.path} needs a seeded parent folder ${parent}`
296
+ }
297
+
298
+ const hasFileFields = [
299
+ entry.rev,
300
+ entry.size,
301
+ entry.contentHash,
302
+ entry.clientModified,
303
+ entry.serverModified
304
+ ].some(Predicate.isNotUndefined)
305
+
306
+ if (entry.kind === 'folder' && hasFileFields) {
307
+ return `folder ${entry.path} cannot take file fields`
308
+ }
309
+
310
+ const clientModified = entry.clientModified ?? defaultTimestamp
311
+
312
+ entries.push({
313
+ id: entry.id,
314
+ parentId: parentEntry?.id ?? null,
315
+ name,
316
+ implied: entry.implied ?? false,
317
+ file:
318
+ entry.kind === 'folder'
319
+ ? null
320
+ : {
321
+ rev: entry.rev ?? 'a1b2c3d4e5f60000',
322
+ size: entry.size ?? 0,
323
+ contentHash: entry.contentHash ?? syntheticContentHash('0'),
324
+ clientModified,
325
+ serverModified: entry.serverModified ?? clientModified
326
+ }
327
+ })
328
+ }
329
+
330
+ return {
331
+ entries,
332
+ deleted: seed.deleted ?? [],
333
+ counters: {
334
+ nextIdNumber:
335
+ highestMinted(
336
+ entries.map(entry => entry.id),
337
+ mintedIdPattern
338
+ ) + 1,
339
+ nextRevNumber: Math.max(
340
+ 101,
341
+ highestMinted(
342
+ entries.flatMap(entry => entry.file?.rev ?? []),
343
+ mintedRevPattern
344
+ ) + 1
345
+ )
346
+ }
347
+ }
348
+ }
349
+
350
+ /** Decode and build a seed; a string is the reason it is invalid. */
351
+ export const buildSeedState = (input: unknown): DropboxEmulatorState | string => {
352
+ const decoded = decodeSeedInput(input)
353
+
354
+ return Result.isFailure(decoded)
355
+ ? issueMessage(decoded.failure.issue)
356
+ : stateFromSeed(decoded.success)
357
+ }
358
+
359
+ /** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
360
+ export const decodeState = (input: unknown): DropboxEmulatorState | string => {
361
+ const decoded = decodeStateInput(input)
362
+
363
+ return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : { ...decoded.success }
364
+ }
package/src/dropbox.ts ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Stateful Dropbox emulator (the `/2` RPC routes on `https://api.dropboxapi.com` and the upload
3
+ * route on `https://content.dropboxapi.com`), built on the upstream `@emulators/core` custom
4
+ * runtime, with a request ledger, status faults, and an `/_emulate/*` control plane.
5
+ *
6
+ * It never imports SDK code: its wire shapes, error envelopes, and default seed are copied as data
7
+ * from the synthetic Dropbox conformance fixtures, and every route names the conformance cases it
8
+ * follows in `dropboxEmulatorRoutes`. Only the routes those eight cases (with their cleanup) send
9
+ * are emulated, each on the origin its fixtures record. Response behaviour comes only from the
10
+ * fixtures: anything they do not show answers one ledgered 400 not-emulated, writes nothing, and
11
+ * uses up no fault. That includes a listing of an empty folder, so the read-only leftover lookup
12
+ * fails against the default seed's empty work folder (the live runner prints its lookup-failed
13
+ * WARN).
14
+ *
15
+ * Request-shape latitude (`/dropbox`, the only accepted deviations): any bearer value (never
16
+ * checked or stored); extra request headers; JSON key order; `content-type` media-type
17
+ * parameters; any path and search query (looked up in the state); any `list_folder` `limit` from
18
+ * 1 to 2000 and any `search_v2` `options.max_results` from 1 to 1000; and any upload body bytes.
19
+ * Everything else (other keys, booleans, modes, query parameters, another origin, ids or revs
20
+ * where the fixtures send paths, the root folder, and cursors not issued since the last reset or
21
+ * whose listing changed) is not emulated.
22
+ *
23
+ * Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by
24
+ * `makeDropboxEmulator` (importing this module has no side effects).
25
+ *
26
+ * @experimental
27
+ */
28
+ import { Data, Predicate } from 'effect'
29
+ import {
30
+ dropboxApiRoutes,
31
+ dropboxEmulatorDrillKnobs,
32
+ type DropboxApiEnv,
33
+ type DropboxCursor,
34
+ type DropboxEmulatorDrills
35
+ } from './dropbox/api.ts'
36
+ import {
37
+ buildSeedState,
38
+ decodeState,
39
+ type DropboxEmulatorSeed,
40
+ type DropboxEmulatorState
41
+ } from './dropbox/state.ts'
42
+ import type { EmulatorRouteEvidence } from './route-evidence.ts'
43
+ import {
44
+ StatefulFault,
45
+ StatefulFaultMatch,
46
+ checkBooleanDrills,
47
+ makeStatefulEmulator,
48
+ routeEvidence,
49
+ type StatefulCoverage,
50
+ type StatefulEmulatorApi,
51
+ type StatefulFaultState,
52
+ type StatefulInputKind,
53
+ type StatefulLedgerEntry
54
+ } from './stateful-emulator.ts'
55
+ import { statefulCoreRuntime } from './stateful-core.ts'
56
+
57
+ export type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'
58
+
59
+ export { emulatorEvidenceHeader } from './route-evidence.ts'
60
+
61
+ export {
62
+ dropboxEmulatorApiOrigin,
63
+ dropboxEmulatorBasePath,
64
+ dropboxEmulatorContentOrigin,
65
+ dropboxEmulatorErrorBodies,
66
+ type DropboxEmulatorDrills
67
+ } from './dropbox/api.ts'
68
+
69
+ export {
70
+ DropboxEmulatorDeleted,
71
+ DropboxEmulatorEntry,
72
+ DropboxEmulatorEntrySeed,
73
+ DropboxEmulatorFile,
74
+ DropboxEmulatorProfile,
75
+ DropboxEmulatorSeed,
76
+ DropboxEmulatorStateSchema,
77
+ type DropboxEmulatorState
78
+ } from './dropbox/state.ts'
79
+
80
+ /**
81
+ * Route evidence manifest: every emulated route, whether it writes, and the conformance cases
82
+ * whose (currently synthetic, unverified) wire claims it follows. Kept in sync with the handlers
83
+ * by construction (both come from one route table).
84
+ */
85
+ export const dropboxEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> =
86
+ dropboxApiRoutes.map(routeEvidence)
87
+
88
+ /**
89
+ * Optional fault filter; an omitted field matches every request. `method` is the HTTP method;
90
+ * `path` is the raw request path (ending in `*`, a prefix); `route` is a route template of the
91
+ * manifest (another value is rejected when the fault is added).
92
+ */
93
+ export const DropboxFaultMatch = StatefulFaultMatch
94
+
95
+ export type DropboxFaultMatch = typeof StatefulFaultMatch.Type
96
+
97
+ /**
98
+ * A status fault (400-599): answer matching requests with this status, headers, and body instead
99
+ * of the route's write, so nothing is written. The body defaults to
100
+ * `{ error: { type: 'emulator_fault', message } }`; `count` limits how many requests it answers.
101
+ * Only a request the emulator would answer reaches a fault: a request that is not emulated, by
102
+ * its shape or by the state, never uses one up.
103
+ */
104
+ export const DropboxFault = StatefulFault
105
+
106
+ export type DropboxFault = StatefulFault
107
+
108
+ export type DropboxFaultState = StatefulFaultState
109
+
110
+ export type DropboxLedgerEntry = StatefulLedgerEntry
111
+
112
+ export type DropboxCoverage = StatefulCoverage
113
+
114
+ /** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */
115
+ export class DropboxEmulatorInputInvalid extends Data.TaggedError('DropboxEmulatorInputInvalid')<{
116
+ readonly input: StatefulInputKind
117
+ readonly reason: string
118
+ }> {
119
+ override get message(): string {
120
+ return `Invalid Dropbox emulator ${this.input}: ${this.reason}`
121
+ }
122
+ }
123
+
124
+ /** A cursor as reported by `cursors()` and `/_emulate/state`. */
125
+ export type DropboxCursorState = {
126
+ readonly cursor: string
127
+ readonly kind: DropboxCursor['kind']
128
+ /** Position of the next page in the listing or search. */
129
+ readonly offset: number
130
+ }
131
+
132
+ export type DropboxEmulatorOptions = {
133
+ /** Typed seed; defaults to the fixture entries (`profile: 'default'`). */
134
+ readonly seed?: DropboxEmulatorSeed
135
+ /** Clock in epoch milliseconds (upload timestamps). Defaults to `Date.now`. */
136
+ readonly now?: () => number
137
+ /** Drill knobs (tests only): make the emulator disagree with one conformance claim. */
138
+ readonly drills?: DropboxEmulatorDrills
139
+ }
140
+
141
+ export type DropboxEmulator = StatefulEmulatorApi<DropboxEmulatorState, DropboxEmulatorSeed> & {
142
+ /** The list and search cursors issued since the last reset or seed (runtime data). */
143
+ readonly cursors: () => ReadonlyArray<DropboxCursorState>
144
+ }
145
+
146
+ const inputInvalid = (input: StatefulInputKind, reason: string) =>
147
+ new DropboxEmulatorInputInvalid({ input, reason })
148
+
149
+ /**
150
+ * Create a stateful Dropbox emulator on the `@emulators/core` custom runtime. Each call has its
151
+ * own state, ledger, faults, and cursors. Rejects with `DropboxEmulatorInputInvalid` for an
152
+ * invalid seed or option. See `src/stateful-emulator.ts` for the request precedence.
153
+ */
154
+ export const makeDropboxEmulator = async (
155
+ options: DropboxEmulatorOptions = {}
156
+ ): Promise<DropboxEmulator> => {
157
+ const initial = buildSeedState(options.seed ?? {})
158
+
159
+ if (Predicate.isString(initial)) {
160
+ throw inputInvalid('seed', initial)
161
+ }
162
+
163
+ checkBooleanDrills(options.drills, dropboxEmulatorDrillKnobs, inputInvalid)
164
+
165
+ const drills = options.drills ?? {}
166
+
167
+ const env: DropboxApiEnv = {
168
+ now: options.now ?? (() => Date.now()),
169
+ drills: {
170
+ listFolderSinglePage: drills.listFolderSinglePage === true,
171
+ getMetadataCaseSensitive: drills.getMetadataCaseSensitive === true,
172
+ searchRepeatsMatches: drills.searchRepeatsMatches === true,
173
+ notFoundAsPathLookup: drills.notFoundAsPathLookup === true,
174
+ folderConflictAsFile: drills.folderConflictAsFile === true,
175
+ deleteLeavesNoTombstone: drills.deleteLeavesNoTombstone === true,
176
+ moveMintsNewId: drills.moveMintsNewId === true,
177
+ uploadIgnoresRev: drills.uploadIgnoresRev === true
178
+ },
179
+ cursors: new Map(),
180
+ cursorCounters: { list: 1, search: 1 }
181
+ }
182
+
183
+ const cursors = (): ReadonlyArray<DropboxCursorState> =>
184
+ [...env.cursors].map(([cursor, state]) => ({ cursor, kind: state.kind, offset: state.offset }))
185
+
186
+ const api = await makeStatefulEmulator<DropboxEmulatorState, DropboxApiEnv, DropboxEmulatorSeed>(
187
+ {
188
+ routes: dropboxApiRoutes,
189
+ env,
190
+ initial,
191
+ buildSeed: buildSeedState,
192
+ recordHeaders: [{ name: 'dropbox-api-arg', json: true }],
193
+ // Cursor numbers keep advancing, so a cursor issued before a reset is never issued again.
194
+ clearRuntime: () => env.cursors.clear(),
195
+ runtimeState: () => ({ cursors: cursors() }),
196
+ seedSummary: state => ({ entries: state.entries.length }),
197
+ inputInvalid
198
+ },
199
+ statefulCoreRuntime({ name: 'dropbox', initial, decodeState, inputInvalid })
200
+ )
201
+
202
+ return { ...api, cursors }
203
+ }