@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,299 @@
1
+ /**
2
+ * Credential guarding and the fail-closed ledger constants (internal; imported by the stateful
3
+ * wrapper, `src/stateful-emulator.ts`, and by the `src/r2-guard.ts` port guard, which uses only the
4
+ * predicate form of the closure, `textClosureOutcome`, without the shared minimum length). No Node
5
+ * builtin, no core import, and no wrapper import, so the R2 guard imports it without the wrapper.
6
+ *
7
+ * A guarded secret (a request-carried credential value the wrapper extracted from a recognised
8
+ * request shape) must never reach the ledger, a response, or `/_emulate/*`: `scrubSecrets` removes
9
+ * it from everything recorded or answered, and the `*RepeatsSecret` checks find it in request text
10
+ * so a request repeating it can be refused. `repeatsSecret` and `jsonRepeatsSecret` (raw, once
11
+ * percent-decoded, parsed JSON keys, strings, and numbers as JavaScript prints them) serve the
12
+ * wrapper's resolved mode (Todoist, Telegram), unchanged; its fail-closed mode uses
13
+ * `textRepeatsSecret`, the fixpoint closure of two total, lexical transforms that cannot fail
14
+ * (`tolerantPercentDecode`, `tolerantJsonUnescape`), failing closed when its work cap is hit, and
15
+ * recognises only RFC 6750 `b64token` bearers whose first character completes no escape and that
16
+ * lie outside the JSON-number alphabet (`isRecognisableBearerValue`).
17
+ *
18
+ * @experimental
19
+ */
20
+ import { Predicate } from 'effect'
21
+ import type * as Schema from 'effect/Schema'
22
+
23
+ /** The ledgered path of every unrecognised request (constant: nothing from the request). */
24
+ export const unrecognisedLedgerPath = '/<unrecognised>'
25
+
26
+ const ledgerableMethods: ReadonlySet<string> = new Set([
27
+ 'GET',
28
+ 'HEAD',
29
+ 'POST',
30
+ 'PUT',
31
+ 'PATCH',
32
+ 'DELETE',
33
+ 'OPTIONS'
34
+ ])
35
+
36
+ /** The ledgered method of an unrecognised request: a standard method, or `<other>`. */
37
+ export const unrecognisedMethod = (method: string): string =>
38
+ ledgerableMethods.has(method) ? method : '<other>'
39
+
40
+ /** `value` percent-decoded once, or unchanged when it is not valid percent-encoding. */
41
+ export const decodedOrRaw = (value: string): string => {
42
+ try {
43
+ return decodeURIComponent(value)
44
+ } catch {
45
+ return value
46
+ }
47
+ }
48
+
49
+ /** Secrets shorter than this are not scrubbed (they would redact ordinary text). */
50
+ const minimumSecretLength = 4
51
+
52
+ const meaningful = (secrets: ReadonlyArray<string>): ReadonlyArray<string> =>
53
+ secrets.filter(secret => secret.length >= minimumSecretLength)
54
+
55
+ /** True when `text`, raw or percent-decoded, contains a secret. */
56
+ export const repeatsSecret = (text: string, secrets: ReadonlyArray<string>): boolean => {
57
+ const decoded = decodedOrRaw(text.replaceAll('+', ' '))
58
+
59
+ return meaningful(secrets).some(secret => text.includes(secret) || decoded.includes(secret))
60
+ }
61
+
62
+ const isJsonRecord = (value: Schema.Json): value is Schema.JsonObject =>
63
+ value !== null && Predicate.isObject(value) && !Array.isArray(value)
64
+
65
+ /**
66
+ * True when any object key, string value, or number of `value` (raw or percent-decoded) holds a
67
+ * secret. Numbers are checked as JavaScript prints them (`1.2345678e7` parses to `12345678`). Used
68
+ * by the wrapper's resolved mode (Todoist, Telegram), unchanged.
69
+ */
70
+ export const jsonRepeatsSecret = (value: Schema.Json, secrets: ReadonlyArray<string>): boolean => {
71
+ if (Predicate.isString(value)) return repeatsSecret(value, secrets)
72
+
73
+ if (Predicate.isNumber(value)) return repeatsSecret(String(value), secrets)
74
+
75
+ if (Array.isArray(value)) return value.some(item => jsonRepeatsSecret(item, secrets))
76
+
77
+ if (isJsonRecord(value)) {
78
+ return Object.entries(value).some(
79
+ ([key, item]) => repeatsSecret(key, secrets) || jsonRepeatsSecret(item, secrets)
80
+ )
81
+ }
82
+
83
+ return false
84
+ }
85
+
86
+ /**
87
+ * The JSON-number alphabet. A fail-closed bearer must hold at least one character outside it
88
+ * (`isRecognisableBearerValue`), so no number's text can ever contain or equal it: numeric forms
89
+ * (`1.2345678e7`, any precision) need no normalisation.
90
+ */
91
+ const jsonNumberAlphabet = /^[0-9.eE+-]+$/
92
+
93
+ /**
94
+ * The RFC 6750 `b64token` syntax (`1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="`),
95
+ * at least 8 characters: what every GitHub (`ghp_…`, `github_pat_…`, `gho_…`) and Google (`ya29.…`)
96
+ * token is made of. Why exactly this: it holds no escape introducer the closure's transforms
97
+ * recognise (`%`, `\`, nor `"`), so no escape can start inside a bearer; with `firstCharacter`,
98
+ * no escape starting to its left can complete with its first character either (see there).
99
+ */
100
+ const b64token = /^[A-Za-z0-9\-._~+/]+=*$/
101
+
102
+ /**
103
+ * The first character a fail-closed bearer may start with: `[G-Zg-z\-._~+/]` without the
104
+ * lower-case JSON escape letters `n`, `r`, `t`, `u`. Proof that no neighbour can then hide the
105
+ * bearer:
106
+ *
107
+ * 1. The bearer holds no escape introducer (a `b64token` has no `%`, `\`, or `"`), so no escape
108
+ * the transforms recognise can start inside it.
109
+ * 2. An escape that starts to its left (a stray `%`, `%X`, `\`, `\u`, `\u0`, `\u00`, `\u006`, or
110
+ * such an introducer produced by an earlier round, `%25` or `%5C` decoded) can only reach the
111
+ * bearer by consuming its first character. That character completes no escape form: it is no
112
+ * hex digit (so no `%X`, `%XX`, or `\uXXXX` prefix completes with it) and no short JSON escape
113
+ * letter (`b` and `f` are hex; `n`, `r`, `t`, `u` are excluded). `/` is kept: `\/` decodes to
114
+ * `/`, which leaves the character in place. When the first character is itself escaped, a stray
115
+ * `\` can pair with the escape's leading `\`: the pair `\\` decodes to `\`, followed by the rest
116
+ * of the escape, which decodes in a later step. That delays the decoding but never destroys it,
117
+ * and the closure runs to a fixpoint, so delays do not matter.
118
+ * 3. So the bearer's characters always decode in place, and the closure finds the bearer once its
119
+ * own encodings are undone.
120
+ *
121
+ * Upper-case `N`, `R`, `T`, `U` are allowed: the JSON escape letters are case-sensitive, and
122
+ * `tolerantJsonUnescape` recognises only the lower-case ones (`\N` stays as it is), so they
123
+ * complete no escape either.
124
+ */
125
+ const firstCharacter = /^[G-Zg-mo-qsv-z\-._~+/]/
126
+
127
+ /**
128
+ * True when a bearer value matches the RFC 6750 `b64token` syntax exactly (at least 8
129
+ * characters), starts with a character that completes no escape (`firstCharacter`), and holds a
130
+ * character outside the JSON-number alphabet `[0-9.eE+-]`, so it is never the text of a number
131
+ * and no escape can be shifted across its characters.
132
+ */
133
+ export const isRecognisableBearerValue = (value: string): boolean =>
134
+ value.length >= 8 &&
135
+ b64token.test(value) &&
136
+ firstCharacter.test(value) &&
137
+ !jsonNumberAlphabet.test(value)
138
+
139
+ /**
140
+ * The work caps of `textRepeatsSecret`. The closure runs to a fixpoint; these only bound the work
141
+ * of a pathological text, and hitting one counts as a credential repeat (uncertainty refuses, it
142
+ * never admits). Measured margins: every fixture converges in a few rounds and texts; a 64 KiB
143
+ * body of densely nested escapes converges in about 25 rounds, under 100 texts, and about 2.4 Mi
144
+ * characters read; a pathological 64 KiB body hits a cap in about 100 ms. The character cap is
145
+ * spent on reading too, so any part over 4 Mi characters is always refused, escapes or not (far
146
+ * above GitHub's 65 536-character body limit).
147
+ */
148
+ export const secretClosureCaps = {
149
+ /** Rounds of the breadth-first walk. */
150
+ rounds: 64,
151
+ /** Distinct texts seen. */
152
+ texts: 1024,
153
+ /** Characters the transforms read, over all texts (keeps the worst large body near 100 ms). */
154
+ characters: 8 * 1024 * 1024
155
+ } as const
156
+
157
+ /**
158
+ * Tolerant percent-decode, total: every `%XX` whose value is below `0x80` becomes its ASCII
159
+ * character; every other `%` sequence (a stray `%`, `%E9`, `%zz`) is left as it is. It never
160
+ * throws, so an encoded fragment next to `100%` or `%E9` is still decoded.
161
+ */
162
+ export const tolerantPercentDecode = (text: string): string =>
163
+ !text.includes('%')
164
+ ? text
165
+ : text.replace(/%([0-7][0-9A-Fa-f])/g, (_escape, hex: string) =>
166
+ String.fromCharCode(Number.parseInt(hex, 16))
167
+ )
168
+
169
+ /** The short JSON escapes and the characters they stand for. */
170
+ const jsonEscapes = new Map([
171
+ ['"', '"'],
172
+ ['\\', '\\'],
173
+ ['/', '/'],
174
+ ['b', '\b'],
175
+ ['f', '\f'],
176
+ ['n', '\n'],
177
+ ['r', '\r'],
178
+ ['t', '\t']
179
+ ])
180
+
181
+ /**
182
+ * Tolerant JSON-unescape, total and lexical: within any text, whether or not it parses as JSON,
183
+ * `\uXXXX` below `\u0080` and `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t` become their
184
+ * characters (one left-to-right pass); every other sequence is left as it is. Being lexical, it
185
+ * sees every string of a JSON text, duplicate keys and overwritten values included.
186
+ */
187
+ export const tolerantJsonUnescape = (text: string): string =>
188
+ !text.includes('\\')
189
+ ? text
190
+ : text.replace(
191
+ /\\(?:u([0-9A-Fa-f]{4})|(["\\/bfnrt]))/g,
192
+ (escape, hex?: string, short?: string) => {
193
+ if (hex !== undefined) {
194
+ const code = Number.parseInt(hex, 16)
195
+
196
+ return code < 0x80 ? String.fromCharCode(code) : escape
197
+ }
198
+
199
+ return jsonEscapes.get(short ?? '') ?? escape
200
+ }
201
+ )
202
+
203
+ /** What the closure of a text found: the secret, its fixpoint without the secret, or a cap. */
204
+ export type SecretClosureOutcome = 'repeats' | 'clear' | 'capped'
205
+
206
+ /** What the closure of a text found: a matching text, its fixpoint without one, or a cap. */
207
+ export type TextClosureOutcome = 'matches' | 'clear' | 'capped'
208
+
209
+ /**
210
+ * The closure of `text` against a predicate: starting from the raw text, a breadth-first walk
211
+ * applying either total transform (`tolerantPercentDecode`, `tolerantJsonUnescape`) to every text
212
+ * of the previous round, deduplicated, until no new text appears (a fixpoint); every text is
213
+ * tested with `matches`. Both transforms never lengthen a text and strictly shorten it when they
214
+ * change it, so every branch ends. `capped` when a cap of `secretClosureCaps` is hit first. No
215
+ * JSON is parsed and nothing can fail (unless `matches` throws).
216
+ */
217
+ export const textClosureOutcome = (
218
+ text: string,
219
+ matches: (candidate: string) => boolean
220
+ ): TextClosureOutcome => {
221
+ const seen = new Set([text])
222
+ let characters = 0
223
+ let frontier: ReadonlyArray<string> = [text]
224
+
225
+ for (let round = 0; frontier.length > 0; round += 1) {
226
+ if (frontier.some(matches)) return 'matches'
227
+
228
+ if (round === secretClosureCaps.rounds) return 'capped'
229
+
230
+ const next: Array<string> = []
231
+
232
+ for (const candidate of frontier) {
233
+ // The work: every character each transform reads.
234
+ characters += 2 * candidate.length
235
+
236
+ if (characters > secretClosureCaps.characters) return 'capped'
237
+
238
+ for (const transformed of [
239
+ tolerantPercentDecode(candidate),
240
+ tolerantJsonUnescape(candidate)
241
+ ]) {
242
+ if (seen.has(transformed)) continue
243
+
244
+ if (seen.size >= secretClosureCaps.texts) return 'capped'
245
+
246
+ seen.add(transformed)
247
+ next.push(transformed)
248
+ }
249
+ }
250
+
251
+ frontier = next
252
+ }
253
+
254
+ return 'clear'
255
+ }
256
+
257
+ /**
258
+ * The closure of `text` against `secrets` (`textClosureOutcome`, every text checked for a secret
259
+ * as a substring). Numbers need no handling: a recognisable bearer never fits the JSON-number
260
+ * alphabet.
261
+ */
262
+ export const secretClosureOutcome = (
263
+ text: string,
264
+ secrets: ReadonlyArray<string>
265
+ ): SecretClosureOutcome => {
266
+ const guarded = meaningful(secrets)
267
+
268
+ if (guarded.length === 0) return 'clear'
269
+
270
+ const outcome = textClosureOutcome(text, candidate =>
271
+ guarded.some(secret => candidate.includes(secret))
272
+ )
273
+
274
+ return outcome === 'matches' ? 'repeats' : outcome
275
+ }
276
+
277
+ /**
278
+ * True when `text` repeats a secret anywhere in its closure (`secretClosureOutcome`), or when the
279
+ * closure hits a work cap before its fixpoint: fail closed, uncertainty counts as a repeat.
280
+ */
281
+ export const textRepeatsSecret = (text: string, secrets: ReadonlyArray<string>): boolean =>
282
+ secretClosureOutcome(text, secrets) !== 'clear'
283
+
284
+ /**
285
+ * `text` with every secret (raw and percent-encoded) replaced by `<redacted>`; when a secret still
286
+ * shows after that (for example in another percent-encoding), the whole text is `<redacted>`.
287
+ * Applied to everything the ledger keeps or a refusal answers.
288
+ */
289
+ export const scrubSecrets = (text: string, secrets: ReadonlyArray<string>): string => {
290
+ const variants = meaningful(secrets)
291
+ .flatMap(secret => [secret, encodeURIComponent(secret)])
292
+ .sort((left, right) => right.length - left.length)
293
+
294
+ let result = text
295
+
296
+ for (const variant of variants) result = result.replaceAll(variant, '<redacted>')
297
+
298
+ return repeatsSecret(result, secrets) ? '<redacted>' : result
299
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Subscription-usage recordings the `/anthropic`, `/codex`, and `/xai` usage routes answer from.
3
+ *
4
+ * Copied as data from the committed (synthetic, unverified) conformance fixtures; never imported.
5
+ * `test/fixture-recordings.test.ts` fails when a fixture changes and this copy does not. Internal;
6
+ * not a package export.
7
+ */
8
+ import type { FixtureRecording } from './fixture-route.ts'
9
+
10
+ export const anthropicClaudeUsageRecording: FixtureRecording = {
11
+ fixtureId: 'anthropic.claude.usage.snapshot.synthetic',
12
+ caseId: 'anthropic.claude.usage.snapshot',
13
+ request: {
14
+ method: 'GET',
15
+ path: '/api/oauth/usage',
16
+ query: '',
17
+ headers: {
18
+ accept: 'application/json'
19
+ }
20
+ },
21
+ response: {
22
+ status: 200,
23
+ headers: {
24
+ 'content-type': 'application/json'
25
+ },
26
+ streamed: false,
27
+ chunks: [
28
+ '{"five_hour":{"utilization":18,"resets_at":"2026-10-01T05:00:00.000Z"},"seven_day":{"utilization":42,"resets_at":"2026-10-06T00:00:00.000Z"}}'
29
+ ]
30
+ }
31
+ }
32
+
33
+ export const codexUsageRecording: FixtureRecording = {
34
+ fixtureId: 'openai.codex.usage.snapshot.synthetic',
35
+ caseId: 'openai.codex.usage.snapshot',
36
+ request: {
37
+ method: 'GET',
38
+ path: '/backend-api/wham/usage',
39
+ query: '',
40
+ headers: {
41
+ accept: 'application/json'
42
+ }
43
+ },
44
+ response: {
45
+ status: 200,
46
+ headers: {
47
+ 'content-type': 'application/json'
48
+ },
49
+ streamed: false,
50
+ chunks: [
51
+ '{"rate_limit":{"primary_window":{"used_percent":23,"limit_window_seconds":18000,"reset_after_seconds":7200,"reset_at":1790007200},"secondary_window":{"used_percent":51,"limit_window_seconds":604800,"reset_after_seconds":259200,"reset_at":1790259200}}}'
52
+ ]
53
+ }
54
+ }
55
+
56
+ export const xAiGrokUsageRecording: FixtureRecording = {
57
+ fixtureId: 'xai.grok.usage.snapshot.synthetic',
58
+ caseId: 'xai.grok.usage.snapshot',
59
+ request: {
60
+ method: 'GET',
61
+ path: '/v1/billing',
62
+ query: '?format=credits',
63
+ headers: {
64
+ accept: 'application/json'
65
+ }
66
+ },
67
+ response: {
68
+ status: 200,
69
+ headers: {
70
+ 'content-type': 'application/json'
71
+ },
72
+ streamed: false,
73
+ chunks: [
74
+ '{"config":{"creditUsagePercent":37.5,"currentPeriod":{"type":"monthly","start":"2026-09-01T00:00:00.000Z","end":"2026-10-01T00:00:00.000Z"}}}'
75
+ ]
76
+ }
77
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Subscription-usage routes (internal; not a package export): one `GET` route per usage endpoint
3
+ * the SDK fetchers call (Claude, Codex, Grok, OpenCode Go), on the fixture-only core
4
+ * (`fixture-route.ts`). A request carrying the credential and headers the fetcher sends, on the
5
+ * recorded query, gets the recorded snapshot body; everything else answers 400 not-emulated. The
6
+ * body may be replaced (`subscriptionUsage` option, scripted `{ usage }` turn) only by a body with
7
+ * the recorded JSON shape.
8
+ *
9
+ * Usage routes live next to a model route on the same origin (`/anthropic`, `/codex`, `/xai`,
10
+ * `/opencode`), but keep their own manifest, ledger, faults, and turns, so the model route's
11
+ * manifest and coverage are unchanged; `emulator-compose.ts` dispatches to them.
12
+ */
13
+ import type * as Schema from 'effect/Schema'
14
+ import {
15
+ FixtureRouteFault,
16
+ makeFixtureRouteEmulator,
17
+ type FixtureRecording,
18
+ type FixtureRouteEmulator,
19
+ type FixtureRouteFaultKind,
20
+ type FixtureRouteHeaderRule,
21
+ type FixtureRouteLedgerEntry,
22
+ type FixtureRouteScriptedTurn
23
+ } from './fixture-route.ts'
24
+ import { parseJson } from './emulator-kernel.ts'
25
+ import type { EmulatorRouteEvidence } from './route-evidence.ts'
26
+
27
+ /** Usage-route faults: `status`, `error-after-chunks`, `truncate-after-chunks` (one-chunk body). */
28
+ export const SubscriptionUsageFault = FixtureRouteFault
29
+
30
+ export type SubscriptionUsageFault = FixtureRouteFault
31
+
32
+ export type SubscriptionUsageFaultKind = FixtureRouteFaultKind
33
+
34
+ /** A usage turn: `{ usage }` (a body with the recorded JSON shape) or `{ error }`. */
35
+ export type SubscriptionUsageScriptedTurn = FixtureRouteScriptedTurn
36
+
37
+ export type SubscriptionUsageLedgerEntry = FixtureRouteLedgerEntry
38
+
39
+ export type SubscriptionUsageEmulator = FixtureRouteEmulator
40
+
41
+ export type SubscriptionUsageEmulatorConfig = {
42
+ readonly path: string
43
+ readonly routes: ReadonlyArray<EmulatorRouteEvidence>
44
+ readonly recording: FixtureRecording
45
+ /** Headers the SDK fetcher sends besides `Authorization: Bearer` and `accept`, in order. */
46
+ readonly headers: ReadonlyArray<FixtureRouteHeaderRule>
47
+ /** Replacement default body; must have the recorded JSON shape. */
48
+ readonly subscriptionUsage: Schema.Json | undefined
49
+ readonly inputInvalid: (input: 'fault' | 'turn', reason: string) => Error
50
+ }
51
+
52
+ /** The recorded snapshot body of a usage recording, parsed. */
53
+ export const recordedUsageBody = (recording: FixtureRecording): Schema.Json =>
54
+ parseJson(recording.response.chunks.join('')) ?? null
55
+
56
+ export const makeSubscriptionUsageEmulator = (
57
+ config: SubscriptionUsageEmulatorConfig
58
+ ): SubscriptionUsageEmulator =>
59
+ makeFixtureRouteEmulator({
60
+ method: 'GET',
61
+ path: config.path,
62
+ routes: config.routes,
63
+ recordings: [config.recording],
64
+ credential: 'bearer',
65
+ headers: config.headers,
66
+ replaceableBody: { defaultBody: config.subscriptionUsage },
67
+ inputInvalid: config.inputInvalid
68
+ })