@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.
- package/LICENSE +21 -0
- package/README.md +2135 -0
- package/dist/anthropic.d.mts +269 -0
- package/dist/anthropic.d.mts.map +1 -0
- package/dist/anthropic.mjs +177 -0
- package/dist/anthropic.mjs.map +1 -0
- package/dist/chat-completions.d.mts +288 -0
- package/dist/chat-completions.d.mts.map +1 -0
- package/dist/chat-completions.mjs +451 -0
- package/dist/chat-completions.mjs.map +1 -0
- package/dist/codex.d.mts +265 -0
- package/dist/codex.d.mts.map +1 -0
- package/dist/codex.mjs +199 -0
- package/dist/codex.mjs.map +1 -0
- package/dist/dropbox/api.d.mts +63 -0
- package/dist/dropbox/api.d.mts.map +1 -0
- package/dist/dropbox/api.mjs +565 -0
- package/dist/dropbox/api.mjs.map +1 -0
- package/dist/dropbox/state.d.mts +136 -0
- package/dist/dropbox/state.d.mts.map +1 -0
- package/dist/dropbox/state.mjs +209 -0
- package/dist/dropbox/state.mjs.map +1 -0
- package/dist/dropbox.d.mts +79 -0
- package/dist/dropbox.d.mts.map +1 -0
- package/dist/dropbox.mjs +124 -0
- package/dist/dropbox.mjs.map +1 -0
- package/dist/email-fixtures.d.mts +36 -0
- package/dist/email-fixtures.d.mts.map +1 -0
- package/dist/email-fixtures.mjs +1080 -0
- package/dist/email-fixtures.mjs.map +1 -0
- package/dist/email.d.mts +160 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +608 -0
- package/dist/email.mjs.map +1 -0
- package/dist/emulator-compose.d.mts +40 -0
- package/dist/emulator-compose.d.mts.map +1 -0
- package/dist/emulator-compose.mjs +91 -0
- package/dist/emulator-compose.mjs.map +1 -0
- package/dist/emulator-http.d.mts +53 -0
- package/dist/emulator-http.d.mts.map +1 -0
- package/dist/emulator-http.mjs +117 -0
- package/dist/emulator-http.mjs.map +1 -0
- package/dist/emulator-kernel.d.mts +176 -0
- package/dist/emulator-kernel.d.mts.map +1 -0
- package/dist/emulator-kernel.mjs +413 -0
- package/dist/emulator-kernel.mjs.map +1 -0
- package/dist/fixture-route.d.mts +129 -0
- package/dist/fixture-route.d.mts.map +1 -0
- package/dist/fixture-route.mjs +257 -0
- package/dist/fixture-route.mjs.map +1 -0
- package/dist/fortnox/api.d.mts +92 -0
- package/dist/fortnox/api.d.mts.map +1 -0
- package/dist/fortnox/api.mjs +750 -0
- package/dist/fortnox/api.mjs.map +1 -0
- package/dist/fortnox/state.d.mts +533 -0
- package/dist/fortnox/state.d.mts.map +1 -0
- package/dist/fortnox/state.mjs +612 -0
- package/dist/fortnox/state.mjs.map +1 -0
- package/dist/fortnox.d.mts +128 -0
- package/dist/fortnox.d.mts.map +1 -0
- package/dist/fortnox.mjs +403 -0
- package/dist/fortnox.mjs.map +1 -0
- package/dist/gateway-evaluate-recordings.d.mts +12 -0
- package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
- package/dist/gateway-evaluate-recordings.mjs +128 -0
- package/dist/gateway-evaluate-recordings.mjs.map +1 -0
- package/dist/gateway.d.mts +272 -0
- package/dist/gateway.d.mts.map +1 -0
- package/dist/gateway.mjs +400 -0
- package/dist/gateway.mjs.map +1 -0
- package/dist/github/api.d.mts +63 -0
- package/dist/github/api.d.mts.map +1 -0
- package/dist/github/api.mjs +527 -0
- package/dist/github/api.mjs.map +1 -0
- package/dist/github/state.d.mts +272 -0
- package/dist/github/state.d.mts.map +1 -0
- package/dist/github/state.mjs +303 -0
- package/dist/github/state.mjs.map +1 -0
- package/dist/github.d.mts +77 -0
- package/dist/github.d.mts.map +1 -0
- package/dist/github.mjs +180 -0
- package/dist/github.mjs.map +1 -0
- package/dist/google/calendar.d.mts +10 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +252 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/drive.d.mts +8 -0
- package/dist/google/drive.d.mts.map +1 -0
- package/dist/google/drive.mjs +252 -0
- package/dist/google/drive.mjs.map +1 -0
- package/dist/google/gmail.d.mts +10 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +591 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/shared.d.mts +107 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +146 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/google/state.d.mts +385 -0
- package/dist/google/state.d.mts.map +1 -0
- package/dist/google/state.mjs +496 -0
- package/dist/google/state.mjs.map +1 -0
- package/dist/google.d.mts +75 -0
- package/dist/google.d.mts.map +1 -0
- package/dist/google.mjs +210 -0
- package/dist/google.mjs.map +1 -0
- package/dist/linkedin-search/api.d.mts +45 -0
- package/dist/linkedin-search/api.d.mts.map +1 -0
- package/dist/linkedin-search/api.mjs +181 -0
- package/dist/linkedin-search/api.mjs.map +1 -0
- package/dist/linkedin-search/state.d.mts +123 -0
- package/dist/linkedin-search/state.d.mts.map +1 -0
- package/dist/linkedin-search/state.mjs +261 -0
- package/dist/linkedin-search/state.mjs.map +1 -0
- package/dist/linkedin-search.d.mts +69 -0
- package/dist/linkedin-search.d.mts.map +1 -0
- package/dist/linkedin-search.mjs +158 -0
- package/dist/linkedin-search.mjs.map +1 -0
- package/dist/mcp/api.d.mts +44 -0
- package/dist/mcp/api.d.mts.map +1 -0
- package/dist/mcp/api.mjs +557 -0
- package/dist/mcp/api.mjs.map +1 -0
- package/dist/mcp/recordings.d.mts +40 -0
- package/dist/mcp/recordings.d.mts.map +1 -0
- package/dist/mcp/recordings.mjs +2390 -0
- package/dist/mcp/recordings.mjs.map +1 -0
- package/dist/mcp/state.d.mts +71 -0
- package/dist/mcp/state.d.mts.map +1 -0
- package/dist/mcp/state.mjs +83 -0
- package/dist/mcp/state.mjs.map +1 -0
- package/dist/mcp.d.mts +84 -0
- package/dist/mcp.d.mts.map +1 -0
- package/dist/mcp.mjs +241 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/messages.d.mts +183 -0
- package/dist/messages.d.mts.map +1 -0
- package/dist/messages.mjs +532 -0
- package/dist/messages.mjs.map +1 -0
- package/dist/microsoft/api.d.mts +47 -0
- package/dist/microsoft/api.d.mts.map +1 -0
- package/dist/microsoft/api.mjs +178 -0
- package/dist/microsoft/api.mjs.map +1 -0
- package/dist/microsoft/calendar.d.mts +30 -0
- package/dist/microsoft/calendar.d.mts.map +1 -0
- package/dist/microsoft/calendar.mjs +271 -0
- package/dist/microsoft/calendar.mjs.map +1 -0
- package/dist/microsoft/drive.d.mts +36 -0
- package/dist/microsoft/drive.d.mts.map +1 -0
- package/dist/microsoft/drive.mjs +298 -0
- package/dist/microsoft/drive.mjs.map +1 -0
- package/dist/microsoft/graph.d.mts +198 -0
- package/dist/microsoft/graph.d.mts.map +1 -0
- package/dist/microsoft/graph.mjs +264 -0
- package/dist/microsoft/graph.mjs.map +1 -0
- package/dist/microsoft/mail.d.mts +48 -0
- package/dist/microsoft/mail.d.mts.map +1 -0
- package/dist/microsoft/mail.mjs +488 -0
- package/dist/microsoft/mail.mjs.map +1 -0
- package/dist/microsoft/state.d.mts +480 -0
- package/dist/microsoft/state.d.mts.map +1 -0
- package/dist/microsoft/state.mjs +625 -0
- package/dist/microsoft/state.mjs.map +1 -0
- package/dist/microsoft.d.mts +168 -0
- package/dist/microsoft.d.mts.map +1 -0
- package/dist/microsoft.mjs +483 -0
- package/dist/microsoft.mjs.map +1 -0
- package/dist/node.d.mts +47 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +178 -0
- package/dist/node.mjs.map +1 -0
- package/dist/notion/api.d.mts +51 -0
- package/dist/notion/api.d.mts.map +1 -0
- package/dist/notion/api.mjs +507 -0
- package/dist/notion/api.mjs.map +1 -0
- package/dist/notion/state.d.mts +329 -0
- package/dist/notion/state.d.mts.map +1 -0
- package/dist/notion/state.mjs +432 -0
- package/dist/notion/state.mjs.map +1 -0
- package/dist/notion.d.mts +72 -0
- package/dist/notion.d.mts.map +1 -0
- package/dist/notion.mjs +127 -0
- package/dist/notion.mjs.map +1 -0
- package/dist/openai.d.mts +165 -0
- package/dist/openai.d.mts.map +1 -0
- package/dist/openai.mjs +141 -0
- package/dist/openai.mjs.map +1 -0
- package/dist/opencode-recordings.d.mts +7 -0
- package/dist/opencode-recordings.d.mts.map +1 -0
- package/dist/opencode-recordings.mjs +222 -0
- package/dist/opencode-recordings.mjs.map +1 -0
- package/dist/opencode.d.mts +70 -0
- package/dist/opencode.d.mts.map +1 -0
- package/dist/opencode.mjs +217 -0
- package/dist/opencode.mjs.map +1 -0
- package/dist/r2-fixtures.d.mts +36 -0
- package/dist/r2-fixtures.d.mts.map +1 -0
- package/dist/r2-fixtures.mjs +245 -0
- package/dist/r2-fixtures.mjs.map +1 -0
- package/dist/r2-guard.d.mts +43 -0
- package/dist/r2-guard.d.mts.map +1 -0
- package/dist/r2-guard.mjs +215 -0
- package/dist/r2-guard.mjs.map +1 -0
- package/dist/r2.d.mts +166 -0
- package/dist/r2.d.mts.map +1 -0
- package/dist/r2.mjs +517 -0
- package/dist/r2.mjs.map +1 -0
- package/dist/responses.d.mts +231 -0
- package/dist/responses.d.mts.map +1 -0
- package/dist/responses.mjs +556 -0
- package/dist/responses.mjs.map +1 -0
- package/dist/route-evidence.d.mts +45 -0
- package/dist/route-evidence.d.mts.map +1 -0
- package/dist/route-evidence.mjs +56 -0
- package/dist/route-evidence.mjs.map +1 -0
- package/dist/router.d.mts +93 -0
- package/dist/router.d.mts.map +1 -0
- package/dist/router.mjs +259 -0
- package/dist/router.mjs.map +1 -0
- package/dist/stateful-core.d.mts +17 -0
- package/dist/stateful-core.d.mts.map +1 -0
- package/dist/stateful-core.mjs +41 -0
- package/dist/stateful-core.mjs.map +1 -0
- package/dist/stateful-emulator.d.mts +565 -0
- package/dist/stateful-emulator.d.mts.map +1 -0
- package/dist/stateful-emulator.mjs +1228 -0
- package/dist/stateful-emulator.mjs.map +1 -0
- package/dist/stateful-secrets.d.mts +84 -0
- package/dist/stateful-secrets.d.mts.map +1 -0
- package/dist/stateful-secrets.mjs +216 -0
- package/dist/stateful-secrets.mjs.map +1 -0
- package/dist/subscription-usage-recordings.d.mts +9 -0
- package/dist/subscription-usage-recordings.d.mts.map +1 -0
- package/dist/subscription-usage-recordings.mjs +53 -0
- package/dist/subscription-usage-recordings.mjs.map +1 -0
- package/dist/subscription-usage.d.mts +53 -0
- package/dist/subscription-usage.d.mts.map +1 -0
- package/dist/subscription-usage.mjs +21 -0
- package/dist/subscription-usage.mjs.map +1 -0
- package/dist/telegram/api.d.mts +34 -0
- package/dist/telegram/api.d.mts.map +1 -0
- package/dist/telegram/api.mjs +257 -0
- package/dist/telegram/api.mjs.map +1 -0
- package/dist/telegram/state.d.mts +128 -0
- package/dist/telegram/state.d.mts.map +1 -0
- package/dist/telegram/state.mjs +154 -0
- package/dist/telegram/state.mjs.map +1 -0
- package/dist/telegram.d.mts +62 -0
- package/dist/telegram.d.mts.map +1 -0
- package/dist/telegram.mjs +127 -0
- package/dist/telegram.mjs.map +1 -0
- package/dist/todoist/api.d.mts +55 -0
- package/dist/todoist/api.d.mts.map +1 -0
- package/dist/todoist/api.mjs +415 -0
- package/dist/todoist/api.mjs.map +1 -0
- package/dist/todoist/state.d.mts +275 -0
- package/dist/todoist/state.d.mts.map +1 -0
- package/dist/todoist/state.mjs +345 -0
- package/dist/todoist/state.mjs.map +1 -0
- package/dist/todoist.d.mts +68 -0
- package/dist/todoist.d.mts.map +1 -0
- package/dist/todoist.mjs +181 -0
- package/dist/todoist.mjs.map +1 -0
- package/dist/xai.d.mts +267 -0
- package/dist/xai.d.mts.map +1 -0
- package/dist/xai.mjs +252 -0
- package/dist/xai.mjs.map +1 -0
- package/package.json +157 -0
- package/src/anthropic.ts +289 -0
- package/src/chat-completions.ts +924 -0
- package/src/codex.ts +296 -0
- package/src/dropbox/api.ts +1084 -0
- package/src/dropbox/state.ts +364 -0
- package/src/dropbox.ts +203 -0
- package/src/email-fixtures.ts +1297 -0
- package/src/email.ts +1108 -0
- package/src/emulator-compose.ts +147 -0
- package/src/emulator-http.ts +184 -0
- package/src/emulator-kernel.ts +844 -0
- package/src/fixture-route.ts +561 -0
- package/src/fortnox/api.ts +1352 -0
- package/src/fortnox/state.ts +798 -0
- package/src/fortnox.ts +801 -0
- package/src/gateway-evaluate-recordings.ts +153 -0
- package/src/gateway.ts +530 -0
- package/src/github/api.ts +986 -0
- package/src/github/state.ts +439 -0
- package/src/github.ts +271 -0
- package/src/google/calendar.ts +486 -0
- package/src/google/drive.ts +471 -0
- package/src/google/gmail.ts +1011 -0
- package/src/google/shared.ts +321 -0
- package/src/google/state.ts +686 -0
- package/src/google.ts +298 -0
- package/src/linkedin-search/api.ts +363 -0
- package/src/linkedin-search/state.ts +373 -0
- package/src/linkedin-search.ts +241 -0
- package/src/mcp/api.ts +995 -0
- package/src/mcp/recordings.ts +2665 -0
- package/src/mcp/state.ts +125 -0
- package/src/mcp.ts +319 -0
- package/src/messages.ts +844 -0
- package/src/microsoft/api.ts +426 -0
- package/src/microsoft/calendar.ts +491 -0
- package/src/microsoft/drive.ts +541 -0
- package/src/microsoft/graph.ts +550 -0
- package/src/microsoft/mail.ts +833 -0
- package/src/microsoft/state.ts +822 -0
- package/src/microsoft.ts +982 -0
- package/src/node.ts +293 -0
- package/src/notion/api.ts +976 -0
- package/src/notion/state.ts +512 -0
- package/src/notion.ts +210 -0
- package/src/openai.ts +198 -0
- package/src/opencode-recordings.ts +257 -0
- package/src/opencode.ts +271 -0
- package/src/r2-fixtures.ts +295 -0
- package/src/r2-guard.ts +323 -0
- package/src/r2.ts +890 -0
- package/src/responses.ts +901 -0
- package/src/route-evidence.ts +90 -0
- package/src/router.ts +490 -0
- package/src/stateful-core.ts +70 -0
- package/src/stateful-emulator.ts +2571 -0
- package/src/stateful-secrets.ts +299 -0
- package/src/subscription-usage-recordings.ts +77 -0
- package/src/subscription-usage.ts +68 -0
- package/src/telegram/api.ts +437 -0
- package/src/telegram/state.ts +229 -0
- package/src/telegram.ts +227 -0
- package/src/todoist/api.ts +736 -0
- package/src/todoist/state.ts +456 -0
- package/src/todoist.ts +316 -0
- 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
|
+
})
|