@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,147 @@
1
+ /**
2
+ * Composition of several kernel-built emulators behind one origin (internal; not a package
3
+ * export).
4
+ *
5
+ * The router takes one route per origin, so every route of one service origin must be answered by
6
+ * one fetch handler. When an origin carries routes of different wire cores (OpenCode Go serves
7
+ * Chat Completions, Messages, Responses, and usage under `https://opencode.ai`; Anthropic, Codex,
8
+ * and Grok serve their subscription-usage route next to their model route), each core keeps its
9
+ * own manifest, ledger, faults, turns, and control plane, and this module dispatches by path:
10
+ *
11
+ * - an API request whose path is a part's route path goes to that part; any other API request goes
12
+ * to the fallback part (which fails it closed as an unknown route and records it);
13
+ * - `/_emulate/<part>/...` is that part's control plane (`/_emulate/...` inside the part);
14
+ * - `POST /_emulate/reset` resets every part.
15
+ *
16
+ * Runtime-portable Web APIs only; no Node builtins and no SDK imports.
17
+ */
18
+ import { controlError, jsonResponse, type EmulatorCoverage } from './emulator-kernel.ts'
19
+
20
+ /** What composition needs from each emulator. */
21
+ export type ComposedPart = {
22
+ readonly fetch: (request: Request) => Promise<Response>
23
+ readonly reset: () => void
24
+ readonly coverage: () => EmulatorCoverage
25
+ }
26
+
27
+ export type ComposedRoute = {
28
+ /** Control-plane name: `/_emulate/<name>/...`. */
29
+ readonly name: string
30
+ /** API paths this part answers (its manifest paths). */
31
+ readonly paths: ReadonlyArray<string>
32
+ readonly part: ComposedPart
33
+ }
34
+
35
+ const requestPath = (request: Request): string =>
36
+ URL.canParse(request.url) ? new URL(request.url).pathname : '/'
37
+
38
+ /** `/_emulate/<name>[/rest]` as the part's own control-plane path, or undefined. */
39
+ const partControlPath = (path: string, name: string): string | undefined => {
40
+ const prefix = `/_emulate/${name}`
41
+
42
+ if (path === prefix) return '/_emulate'
43
+
44
+ return path.startsWith(`${prefix}/`) ? `/_emulate${path.slice(prefix.length)}` : undefined
45
+ }
46
+
47
+ /** Re-send a control-plane request to a part under its own control-plane path. */
48
+ const forwardControl = async (
49
+ request: Request,
50
+ part: ComposedPart,
51
+ path: string
52
+ ): Promise<Response> => {
53
+ const url = new URL(request.url)
54
+
55
+ url.pathname = path
56
+
57
+ const body =
58
+ request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.text()
59
+
60
+ return part.fetch(new Request(url, { method: request.method, headers: request.headers, body }))
61
+ }
62
+
63
+ export const isControlPlanePath = (path: string): boolean =>
64
+ path === '/_emulate' || path.startsWith('/_emulate/')
65
+
66
+ /** Every part's coverage in route order, with the unknown-route counts summed. */
67
+ export const combinedCoverage = (parts: ReadonlyArray<ComposedPart>): EmulatorCoverage => {
68
+ const coverages = parts.map(part => part.coverage())
69
+
70
+ return {
71
+ routes: coverages.flatMap(coverage => coverage.routes),
72
+ unknownRouteRequests: coverages.reduce(
73
+ (total, coverage) => total + coverage.unknownRouteRequests,
74
+ 0
75
+ )
76
+ }
77
+ }
78
+
79
+ export type ComposedFetchOptions = {
80
+ readonly routes: ReadonlyArray<ComposedRoute>
81
+ /** Answers API requests on no part's path (and fails them closed). */
82
+ readonly fallback: ComposedPart
83
+ /**
84
+ * Answers the remaining `/_emulate/*` requests (not a part's prefix, not `POST
85
+ * /_emulate/reset`). Defaults to a 404 control error.
86
+ */
87
+ readonly control?: (request: Request, path: string) => Promise<Response>
88
+ }
89
+
90
+ /** One fetch handler over several parts. Never rejects. */
91
+ export const composeFetch =
92
+ (options: ComposedFetchOptions) =>
93
+ (request: Request): Promise<Response> => {
94
+ const handle = async (): Promise<Response> => {
95
+ const path = requestPath(request)
96
+
97
+ if (!isControlPlanePath(path)) {
98
+ const route = options.routes.find(candidate => candidate.paths.includes(path))
99
+
100
+ return (route?.part ?? options.fallback).fetch(request)
101
+ }
102
+
103
+ for (const route of options.routes) {
104
+ const partPath = partControlPath(path, route.name)
105
+
106
+ if (partPath !== undefined) return forwardControl(request, route.part, partPath)
107
+ }
108
+
109
+ if (path === '/_emulate/reset' && request.method === 'POST') {
110
+ options.fallback.reset()
111
+
112
+ for (const route of options.routes) route.part.reset()
113
+
114
+ return jsonResponse(200, { reset: true })
115
+ }
116
+
117
+ return options.control === undefined
118
+ ? controlError(404, 'unknown control-plane route')
119
+ : options.control(request, path)
120
+ }
121
+
122
+ return handle().catch(() => controlError(500, 'emulator failed to handle the request'))
123
+ }
124
+
125
+ /**
126
+ * A wire-core emulator with its subscription-usage route on the same origin: `path` goes to
127
+ * `usage`, `/_emulate/usage/...` is the usage control plane, `POST /_emulate/reset` and `reset()`
128
+ * reset both, and everything else (other API paths, other `/_emulate/*` routes) is the primary
129
+ * emulator's, unchanged. The primary's ledger, faults, script, and coverage stay its own.
130
+ */
131
+ export const withSubscriptionUsage = <Primary extends ComposedPart, Usage extends ComposedPart>(
132
+ primary: Primary,
133
+ usage: Usage,
134
+ path: string
135
+ ): Primary & { readonly usage: Usage } => ({
136
+ ...primary,
137
+ fetch: composeFetch({
138
+ routes: [{ name: 'usage', paths: [path], part: usage }],
139
+ fallback: primary,
140
+ control: request => primary.fetch(request)
141
+ }),
142
+ reset: () => {
143
+ primary.reset()
144
+ usage.reset()
145
+ },
146
+ usage
147
+ })
@@ -0,0 +1,184 @@
1
+ /**
2
+ * HTTP rules shared by the emulators (internal): which statuses and headers a fault or scripted
3
+ * error may answer with, which request header and query parameter names carry credentials, and the
4
+ * marker a stateful core route uses to report a handler that threw.
5
+ *
6
+ * Faults and scripted errors always carry a body and never redirect, so 1xx, 204, 205, and every
7
+ * 3xx (including 304) are rejected when the fault or turn is added, as are invalid header names or
8
+ * values, a `location` header, and the framing headers the server sets itself. Route statuses
9
+ * (for example a fixture's bodiless 204 or its 202 with a `Location`) follow the fixtures instead.
10
+ *
11
+ * Runtime-portable (no Node builtins): the emulators are plain Web fetch handlers.
12
+ */
13
+ import { Predicate } from 'effect'
14
+ import * as Schema from 'effect/Schema'
15
+
16
+ /** Why a status cannot answer an emulated request, or `undefined` when it can. */
17
+ const emulatorStatusProblem = (status: number): string | undefined =>
18
+ status >= 300 && status <= 399
19
+ ? 'emulators never redirect: 3xx statuses are not allowed'
20
+ : status === 204 || status === 205
21
+ ? 'a 204 or 205 response cannot carry a body'
22
+ : undefined
23
+
24
+ /** Statuses a fault or scripted error may answer with: 200-599 without 204, 205, and 3xx. */
25
+ export const EmulatorResponseStatus = Schema.Int.check(
26
+ Schema.isBetween({ minimum: 200, maximum: 599 }),
27
+ Schema.makeFilter(emulatorStatusProblem)
28
+ )
29
+
30
+ // RFC 9110 token characters for header names.
31
+ const headerNamePattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/
32
+
33
+ // Visible ASCII, space, tab, and obs-text: what both web `Headers` and Node's HTTP server accept.
34
+ const headerValuePattern = /^[\t\x20-\x7e\x80-\xff]*$/
35
+
36
+ const framingHeaders: ReadonlySet<string> = new Set([
37
+ 'content-length',
38
+ 'transfer-encoding',
39
+ 'connection',
40
+ 'keep-alive',
41
+ 'upgrade'
42
+ ])
43
+
44
+ /** Why a header record cannot be sent by an emulator, or `undefined` when it can. */
45
+ const emulatorHeaderRecordProblem = (
46
+ headers: Readonly<Record<string, string>>
47
+ ): string | undefined => {
48
+ for (const [name, value] of Object.entries(headers)) {
49
+ if (!headerNamePattern.test(name)) {
50
+ return `header name ${JSON.stringify(name)} is not a valid HTTP token`
51
+ }
52
+
53
+ if (name.toLowerCase() === 'location') {
54
+ return 'emulators never redirect: a location header is not allowed'
55
+ }
56
+
57
+ // The server frames the body itself; a hand-set framing header would corrupt the response.
58
+ if (framingHeaders.has(name.toLowerCase())) {
59
+ return `header ${name} is set by the server and cannot be scripted`
60
+ }
61
+
62
+ if (!headerValuePattern.test(value)) {
63
+ return `header ${name} has a value with control or non-Latin-1 characters`
64
+ }
65
+ }
66
+
67
+ return undefined
68
+ }
69
+
70
+ /** Response headers for a fault or scripted error: valid names and values, and no `location`. */
71
+ export const EmulatorHeaderRecord = Schema.Record(Schema.String, Schema.String).check(
72
+ Schema.makeFilter(emulatorHeaderRecordProblem)
73
+ )
74
+
75
+ // Header names that carry credentials or session state: the same rule as the conformance wire
76
+ // helpers (`isCredentialHeaderName` in `@yolk-sdk/conformance`), copied because emulator source
77
+ // never imports SDK packages.
78
+ const credentialHeaderNames: ReadonlySet<string> = new Set([
79
+ 'authorization',
80
+ 'proxy-authorization',
81
+ 'cookie',
82
+ 'set-cookie',
83
+ 'x-api-key',
84
+ 'api-key',
85
+ 'x-goog-api-key',
86
+ 'x-auth-token',
87
+ 'x-access-token',
88
+ 'x-amz-security-token',
89
+ 'x-vercel-oidc-token',
90
+ 'x-csrf-token'
91
+ ])
92
+
93
+ const credentialHeaderPattern = /(api[-_]?key|secret|password|cookie|authorization)/i
94
+
95
+ const credentialHeaderSegmentPattern = /(^|[-_])(token|key|auth)([-_]|$)/i
96
+
97
+ /** True for header names that carry credentials or session state. */
98
+ export const isCredentialHeaderName = (name: string): boolean => {
99
+ const lower = name.toLowerCase()
100
+
101
+ return (
102
+ credentialHeaderNames.has(lower) ||
103
+ credentialHeaderPattern.test(lower) ||
104
+ credentialHeaderSegmentPattern.test(lower)
105
+ )
106
+ }
107
+
108
+ /** What a redacted credential value becomes in a ledger. */
109
+ export const redactedCredentialValue = '<redacted>'
110
+
111
+ const isJsonRecord = (value: Schema.Json): value is Schema.JsonObject =>
112
+ value !== null && Predicate.isObject(value) && !Array.isArray(value)
113
+
114
+ /**
115
+ * A JSON value safe to keep in a ledger: every object key that `isCredentialHeaderName` accepts
116
+ * (for example `requests[].headers.Authorization` in a Graph `$batch` body), at any depth, has
117
+ * its value replaced by `<redacted>`. Other values are copied unchanged.
118
+ */
119
+ export const redactCredentialFields = (value: Schema.Json): Schema.Json => {
120
+ if (Array.isArray(value)) return value.map(redactCredentialFields)
121
+
122
+ if (!isJsonRecord(value)) return value
123
+
124
+ return Object.fromEntries(
125
+ Object.entries(value).map(([key, entry]) => [
126
+ key,
127
+ isCredentialHeaderName(key) ? redactedCredentialValue : redactCredentialFields(entry)
128
+ ])
129
+ )
130
+ }
131
+
132
+ // Query parameter names that carry credentials: the same list as the conformance fixture scan
133
+ // (`credentialParamPattern` in `@yolk-sdk/conformance`, reported as `credential_query_param`),
134
+ // copied because emulator source never imports SDK packages. It adds the presigned-URL
135
+ // `x-amz-signature` and `x-amz-credential`, which the header rule does not cover.
136
+ const credentialQueryParamPattern =
137
+ /^(api[_-]?key|key|token|access[_-]?token|refresh[_-]?token|id[_-]?token|auth|secret|password|client[_-]?secret|x-amz-signature|x-amz-credential|x-amz-security-token)$/i
138
+
139
+ /**
140
+ * True for query parameter names that carry credentials: every name the conformance fixture scan
141
+ * reports as a credential query parameter, plus every name `isCredentialHeaderName` accepts.
142
+ */
143
+ export const isCredentialQueryKey = (name: string): boolean =>
144
+ credentialQueryParamPattern.test(name) || isCredentialHeaderName(name)
145
+
146
+ /**
147
+ * Query parameters safe to keep in a ledger: the value of every key that `isCredentialQueryKey`
148
+ * accepts (for example `access_token`, `api_key`, or `X-Amz-Signature`) is replaced by
149
+ * `<redacted>`.
150
+ */
151
+ export const redactCredentialQuery = (query: URLSearchParams): Readonly<Record<string, string>> =>
152
+ Object.fromEntries(
153
+ [...query].map(([key, value]) => [
154
+ key,
155
+ isCredentialQueryKey(key) ? redactedCredentialValue : value
156
+ ])
157
+ )
158
+
159
+ /**
160
+ * Internal response header a stateful core route sets on the bodiless 500 it answers when its
161
+ * handler throws. The wrapper never forwards it: it answers its own error envelope 500 instead
162
+ * and records `responseError` in the ledger.
163
+ */
164
+ export const handlerFailedHeader = 'x-emulator-handler-failed'
165
+
166
+ /** The core route's answer when its handler threw (see `handlerFailedHeader`). */
167
+ export const handlerFailedResponse = (): Response =>
168
+ new Response(null, { status: 500, headers: { [handlerFailedHeader]: '1' } })
169
+
170
+ /**
171
+ * Internal request header a stateful wrapper sets on the requests it forwards to its core: the
172
+ * id of the wrapper's job for that request (the fault decision the core route asks for, and what
173
+ * the route decided). Ids come from a counter that never resets (the ledger sequence does), so a
174
+ * ledger clear never makes two jobs share one. Clients cannot set it: wrappers build the core
175
+ * request headers themselves.
176
+ */
177
+ export const emulatorJobHeader = 'x-emulator-job-id'
178
+
179
+ /**
180
+ * What a core route answers when the wrapper returns the real answer itself (a fault, or in the
181
+ * shared wrapper's resolved mode a refusal), so that answer never depends on the core runtime's
182
+ * lifecycle: a reset or a close before it is read never cancels it.
183
+ */
184
+ export const answeredOutsideCore = (): Response => new Response(null, { status: 204 })