@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,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 })
|