@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,321 @@
1
+ /**
2
+ * Shared pieces of the Google emulator API (internal): origins, the environment, drill knobs,
3
+ * response and error-envelope helpers, query checks, run-scoped text, and page tokens.
4
+ *
5
+ * @experimental
6
+ */
7
+ import type * as Schema from 'effect/Schema'
8
+ import {
9
+ integerIn,
10
+ isNotEmulated,
11
+ notEmulated,
12
+ type Commit,
13
+ type EmulatedRequest,
14
+ type NotEmulated,
15
+ type StatefulRoute
16
+ } from '../stateful-emulator.ts'
17
+ import type { GoogleEmulatorState } from './state.ts'
18
+
19
+ /** The origin of every Gmail route (the API and the upload endpoint), as the fixtures record. */
20
+ export const googleEmulatorGmailOrigin = 'https://gmail.googleapis.com'
21
+
22
+ /** The origin of every Calendar and Drive route, as the fixtures record. */
23
+ export const googleEmulatorApisOrigin = 'https://www.googleapis.com'
24
+
25
+ /** Drill knobs (tests only): each makes the emulator disagree with exactly one conformance case. */
26
+ export type GoogleEmulatorDrills = {
27
+ /** A later `gmail.list` page starts one message early, repeating the previous page's last. */
28
+ readonly gmailPageRepeats?: boolean
29
+ /** Attachment `data` is answered in the standard base64 alphabet instead of base64url. */
30
+ readonly attachmentStandardBase64?: boolean
31
+ /** Gmail 404 envelopes carry no `error.message`. */
32
+ readonly notFoundWithoutMessage?: boolean
33
+ /** Deleting a label leaves it on the messages that carry it. */
34
+ readonly labelDeleteKeepsOnMessages?: boolean
35
+ /** A draft update keeps the earlier subject and body (it ignores the new message). */
36
+ readonly draftUpdateKeepsContent?: boolean
37
+ /** The trash answer omits `TRASH` from `labelIds` (the state still carries it). */
38
+ readonly trashAnswerOmitsTrash?: boolean
39
+ /** The sent message reads back without its `To` header. */
40
+ readonly sentMessageWithoutTo?: boolean
41
+ /** A later `calendar.list_events` page starts one event early. */
42
+ readonly calendarPageRepeats?: boolean
43
+ /** An event PATCH answers (and keeps) the earlier summary. */
44
+ readonly eventPatchKeepsSummary?: boolean
45
+ /** Deleting a cancelled event answers 409 instead of the recorded 410. */
46
+ readonly repeatedEventDeleteConflict?: boolean
47
+ /** A later `drive.list_files` page starts one file early. */
48
+ readonly drivePageRepeats?: boolean
49
+ /** `drive.get_file` answers no `parents`. */
50
+ readonly getFileWithoutParents?: boolean
51
+ /** Folder listings include trashed children. */
52
+ readonly listIncludesTrashed?: boolean
53
+ }
54
+
55
+ export const googleEmulatorDrillKnobs: ReadonlyArray<keyof GoogleEmulatorDrills> = [
56
+ 'gmailPageRepeats',
57
+ 'attachmentStandardBase64',
58
+ 'notFoundWithoutMessage',
59
+ 'labelDeleteKeepsOnMessages',
60
+ 'draftUpdateKeepsContent',
61
+ 'trashAnswerOmitsTrash',
62
+ 'sentMessageWithoutTo',
63
+ 'calendarPageRepeats',
64
+ 'eventPatchKeepsSummary',
65
+ 'repeatedEventDeleteConflict',
66
+ 'drivePageRepeats',
67
+ 'getFileWithoutParents',
68
+ 'listIncludesTrashed'
69
+ ]
70
+
71
+ /** A page token this emulator issued: where its list continues, and the list it was issued for. */
72
+ export type GoogleIssuedPageToken = {
73
+ /** The list it continues (its key names the list and its page size). */
74
+ readonly listKey: string
75
+ readonly offset: number
76
+ /** The whole list as rendered when the token was issued; any change is not emulated. */
77
+ readonly fingerprint: string
78
+ }
79
+
80
+ export type GoogleApiEnv = {
81
+ /** Clock in epoch milliseconds (created and updated timestamps, the sent `Date` header). */
82
+ readonly now: () => number
83
+ readonly drills: Readonly<Record<keyof GoogleEmulatorDrills, boolean>>
84
+ /**
85
+ * Issued page tokens by value (runtime-only; cleared by reset and seed). A value is bound to
86
+ * exactly one list, position, and rendering, and never rebound.
87
+ */
88
+ readonly pageTokens: Map<string, GoogleIssuedPageToken>
89
+ /**
90
+ * The token generation: advanced by every reset and seed (never rewound). A page token is the
91
+ * fixture's value (`synthetic-<api>-page-<n>`) only in the generation that first issued that
92
+ * value; a later generation issues `<value>.g<generation>`, so no token value ever crosses a
93
+ * reset or seed.
94
+ */
95
+ readonly generation: { current: number }
96
+ /** The generation that first issued each token value (never cleared). */
97
+ readonly firstIssued: Map<string, number>
98
+ }
99
+
100
+ export type GoogleRoute = StatefulRoute<GoogleEmulatorState, GoogleApiEnv>
101
+
102
+ /** Route evidence of an unverified connector route on `origin`, citing `caseIds`. */
103
+ export const evidence = (
104
+ origin: string,
105
+ method: string,
106
+ path: string,
107
+ write: boolean,
108
+ caseIds: ReadonlyArray<string>,
109
+ params: Readonly<Record<string, RegExp>> = {}
110
+ ) => ({
111
+ method,
112
+ path,
113
+ kind: 'connector' as const,
114
+ write,
115
+ caseIds,
116
+ evidence: 'unverified' as const,
117
+ origin,
118
+ params
119
+ })
120
+
121
+ /** A path parameter, decoded once by the wrapper (empty when absent). */
122
+ export const param = (request: EmulatedRequest, name: string): string => request.params[name] ?? ''
123
+
124
+ /** The `internalDate` every recorded Gmail message carries. */
125
+ export const recordedInternalDate = '1790000000000'
126
+
127
+ /** The raw path segment of a Gmail message, thread, label, attachment, or draft id. */
128
+ export const gmailIdSegment = /^[A-Za-z0-9_-]{1,100}$/
129
+
130
+ // Responses, as the fixtures record them.
131
+
132
+ const googleContentType = 'application/json; charset=UTF-8'
133
+
134
+ export const googleJson = (status: number, body: Schema.Json): Response =>
135
+ new Response(JSON.stringify(body), { status, headers: { 'content-type': googleContentType } })
136
+
137
+ /** The bodiless 204 every recorded delete answers. */
138
+ export const googleNoContent = (): Response => new Response(null, { status: 204 })
139
+
140
+ /** The Google JSON error envelope, as the fixtures write it. */
141
+ export const googleErrorBody = (
142
+ code: number,
143
+ message: string,
144
+ reason: string,
145
+ status: string
146
+ ): Schema.JsonObject => ({
147
+ error: { code, message, errors: [{ message, domain: 'global', reason }], status }
148
+ })
149
+
150
+ /** A read-only commit. */
151
+ export const answer =
152
+ (response: () => Response): Commit =>
153
+ () =>
154
+ response()
155
+
156
+ // Queries.
157
+
158
+ /** A query value that must equal the recorded one. */
159
+ export const recordedValue = (
160
+ query: Readonly<Record<string, string>>,
161
+ key: string,
162
+ value: string
163
+ ): NotEmulated | undefined =>
164
+ query[key] === value ? undefined : notEmulated(`${key} other than ${value} is not emulated`)
165
+
166
+ /** A decimal page size from `minimum` to `maximum`, or not emulated. */
167
+ export const pageSize = (
168
+ raw: string | undefined,
169
+ label: string,
170
+ minimum: number,
171
+ maximum: number
172
+ ): number | NotEmulated =>
173
+ integerIn(
174
+ raw !== undefined && /^[1-9]\d{0,5}$/.test(raw) ? Number(raw) : null,
175
+ label,
176
+ minimum,
177
+ maximum
178
+ )
179
+
180
+ /** A request without query parameters, or not emulated. */
181
+ export const withoutQuery = (request: EmulatedRequest): NotEmulated | undefined =>
182
+ request.query.size > 0
183
+ ? notEmulated('query parameters are not emulated on this route')
184
+ : undefined
185
+
186
+ // Run-scoped text.
187
+
188
+ /** The run id shape of the cases (`GoogleConformanceRunId`): at most 40 characters. */
189
+ const runIdPattern = /^run-[a-z0-9]+(?:-[a-z0-9]+)*$/
190
+
191
+ /**
192
+ * The well-formed run id of `yolk-conformance <runId> <kind>` (or `... <kind>: <rest>`), or
193
+ * `undefined` when `text` has another form.
194
+ */
195
+ export const runIdOf = (text: string, kind: string, rest?: string): string | undefined => {
196
+ const prefix = 'yolk-conformance '
197
+ const suffix = rest === undefined ? ` ${kind}` : ` ${kind}: ${rest}`
198
+
199
+ if (!text.startsWith(prefix) || !text.endsWith(suffix)) return undefined
200
+
201
+ const runId = text.slice(prefix.length, text.length - suffix.length)
202
+
203
+ return runId.length <= 40 && runIdPattern.test(runId) ? runId : undefined
204
+ }
205
+
206
+ /**
207
+ * True when `text` is `yolk-conformance <runId> <kind>` (or `... <kind>: <rest>`) for a
208
+ * well-formed run id: the only latitude in the run-scoped names, subjects, and summaries.
209
+ */
210
+ export const isRunText = (text: string, kind: string, rest?: string): boolean =>
211
+ runIdOf(text, kind, rest) !== undefined
212
+
213
+ /**
214
+ * The length of the fixtures' run id (`run-synthetic`). The draft and sent messages the Gmail
215
+ * fixtures record carry a `sizeEstimate` that covers their subject, so a draft or send subject
216
+ * must carry a run id of exactly this length for that recorded value to stay true.
217
+ */
218
+ export const recordedRunIdLength = 13
219
+
220
+ // Page tokens: the fixture's value on first issuance, validated by issuance only.
221
+
222
+ /** `value` in this generation (see `GoogleApiEnv.generation`); records its first issuance. */
223
+ const generationValue = (env: GoogleApiEnv, value: string): string => {
224
+ const first = env.firstIssued.get(value)
225
+ const generation = env.generation.current
226
+
227
+ if (first === undefined) env.firstIssued.set(value, generation)
228
+
229
+ return first === undefined || first === generation ? value : `${value}.g${generation}`
230
+ }
231
+
232
+ /**
233
+ * The page token for page `page` of a list (in a commit), bound to `issued` (its list, offset, and
234
+ * fingerprint). The first issuance is the fixture's value (`synthetic-<api>-page-<n>`). Token
235
+ * values are globally unique: a value already bound is reused only for exactly the same list,
236
+ * position, and rendering, and never rebound to anything else, so another list or page size, or a
237
+ * changed list, gets a distinct value (`synthetic-<api>-page-<n>.v<k>`) and an earlier token keeps
238
+ * meaning only what it was issued for.
239
+ */
240
+ const issueToken = (
241
+ env: GoogleApiEnv,
242
+ prefix: string,
243
+ page: number,
244
+ issued: GoogleIssuedPageToken
245
+ ): string => {
246
+ const base = `synthetic-${prefix}-page-${page}`
247
+
248
+ for (let variant = 1; ; variant += 1) {
249
+ const value = generationValue(env, variant === 1 ? base : `${base}.v${variant}`)
250
+ const existing = env.pageTokens.get(value)
251
+
252
+ if (existing === undefined) {
253
+ env.pageTokens.set(value, issued)
254
+
255
+ return value
256
+ }
257
+
258
+ if (
259
+ existing.listKey === issued.listKey &&
260
+ existing.fingerprint === issued.fingerprint &&
261
+ existing.offset === issued.offset
262
+ ) {
263
+ return value
264
+ }
265
+ }
266
+ }
267
+
268
+ /** Where a page of a list starts: 0, or an issued token's offset; not emulated otherwise. */
269
+ const pageStart = (
270
+ env: GoogleApiEnv,
271
+ listKey: string,
272
+ fingerprint: string,
273
+ token: string | undefined
274
+ ): number | NotEmulated => {
275
+ if (token === undefined) return 0
276
+
277
+ const issued = env.pageTokens.get(token)
278
+
279
+ if (issued === undefined || issued.listKey !== listKey) {
280
+ return notEmulated(
281
+ 'a pageToken this emulator did not issue for this list since the last reset is not emulated'
282
+ )
283
+ }
284
+
285
+ return issued.fingerprint === fingerprint
286
+ ? issued.offset
287
+ : notEmulated('continuing a list that changed since its pageToken was issued is not emulated')
288
+ }
289
+
290
+ /**
291
+ * The commit answering one page of a list: `render(page, nextPageToken)` for the page's rendered
292
+ * items, with a next token (registered for this list) while items remain. `repeats` (a drill)
293
+ * starts a later page one item early.
294
+ */
295
+ export const listPage = (
296
+ env: GoogleApiEnv,
297
+ prefix: string,
298
+ listKey: string,
299
+ all: ReadonlyArray<Schema.Json>,
300
+ size: number,
301
+ token: string | undefined,
302
+ repeats: boolean,
303
+ render: (page: ReadonlyArray<Schema.Json>, nextPageToken: string | undefined) => Response
304
+ ): Commit | NotEmulated => {
305
+ const fingerprint = JSON.stringify(all)
306
+ const offset = pageStart(env, listKey, fingerprint, token)
307
+
308
+ if (isNotEmulated(offset)) return offset
309
+
310
+ const start = repeats && offset > 0 ? offset - 1 : offset
311
+ const end = start + size
312
+
313
+ return () => {
314
+ const next =
315
+ end < all.length
316
+ ? issueToken(env, prefix, Math.floor(end / size) + 1, { listKey, offset: end, fingerprint })
317
+ : undefined
318
+
319
+ return render(all.slice(start, end), next)
320
+ }
321
+ }