@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
package/dist/r2.mjs
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { bindRouteHandlers, emulatorRouteKey } from "./route-evidence.mjs";
|
|
2
|
+
import { r2EmulatorFixtures } from "./r2-fixtures.mjs";
|
|
3
|
+
import { guardedValues, hasOwnProtoKey, textCarriesCredential, textsOf, withoutCredentials } from "./r2-guard.mjs";
|
|
4
|
+
import { Data, Equal, Predicate, Result } from "effect";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
//#region src/r2.ts
|
|
7
|
+
/**
|
|
8
|
+
* R2 emulator: a fixture-driven, in-memory fake backend for the host R2 ports `R2Presigner` and
|
|
9
|
+
* `R2ObjectClient`.
|
|
10
|
+
*
|
|
11
|
+
* The R2 connector never talks to R2 itself: hosts implement `R2Presigner` (local SigV4 signing of
|
|
12
|
+
* a PUT URL) and `R2ObjectClient` (conditional get and put). This emulator stands in for such a
|
|
13
|
+
* host at the port level: `call(port, method, request)` takes one port call as plain JSON (the
|
|
14
|
+
* credential-free request; bytes as base64) and answers `{ response }`, `{ failure }`, or a
|
|
15
|
+
* fail-closed `{ notEmulated }`. There is no S3, SigV4 signer, socket, `node:` builtin, or SDK
|
|
16
|
+
* import here: it is a plain structural object, and `r2PortsLayerFromBackend` in
|
|
17
|
+
* `@yolk-sdk/connectors/r2-storage/conformance` turns it into both port layers.
|
|
18
|
+
*
|
|
19
|
+
* Response behaviour comes ONLY from the R2 conformance fixtures (copied as data in
|
|
20
|
+
* `r2-fixtures.ts`). The emulator keeps an in-memory bucket (objects with their etag and bytes)
|
|
21
|
+
* seeded with the object the fixtures describe; the state only selects which fixture answers (the
|
|
22
|
+
* first matching fixture consistent with the bucket) and is updated with what that fixture says
|
|
23
|
+
* happened (an object created or replaced under the etag the fixture names). It never invents an
|
|
24
|
+
* etag, a byte, a failure, or a response. Presigning writes nothing.
|
|
25
|
+
*
|
|
26
|
+
* Credentials: every credential field (the shared port scan's `isPortCredentialKey` names, copied
|
|
27
|
+
* in `r2-guard.ts`) is dropped at any depth before anything is compared or recorded. Every refusal
|
|
28
|
+
* (`unknown-method`, `invalid-request`, `uncheckable-body`, `credential-in-request`,
|
|
29
|
+
* `uncheckable-request`, `no-matching-fixture`, `state-conflict`) is ledgered with constant text
|
|
30
|
+
* only (request `<redacted>`; port and method `<unrecognised>` for `unknown-method` and
|
|
31
|
+
* `uncheckable-request`), uses no fault, and changes no state. Only a request equal to a fixture
|
|
32
|
+
* request (minus credential fields) is recorded, so the ledger holds fixture text, with every
|
|
33
|
+
* `bodyBase64` as `<redacted>` plus `bodyBytes` (the decoded length). An own `__proto__` key at any
|
|
34
|
+
* depth is `invalid-request`. Every `bodyBase64` must be canonical standard base64 of UTF-8 text
|
|
35
|
+
* (else `uncheckable-body`); its decoded text is checked with the request's own texts. A request is
|
|
36
|
+
* `credential-in-request` when any key, string value, number (as printed and, for an integer, as
|
|
37
|
+
* its digit string), or decoded body repeats a guarded value, or holds, once the exact canonical
|
|
38
|
+
* placeholders are blanked out, a SigV4 credential name, a credential query parameter, or a token
|
|
39
|
+
* the shared scan flags (a bearer token, an API-key prefix, a JSON Web Token, a PEM private key).
|
|
40
|
+
* Guarded values are every non-empty key, string, and number under a credential field (a numeric
|
|
41
|
+
* string also in its number forms), with no minimum length, unlike the shared helpers. Both checks
|
|
42
|
+
* run raw, in R2's own decodings (`decodedVariants`: up to three percent and three escape rounds,
|
|
43
|
+
* with `\uXXXX`, `\xXX`, and numeric HTML references), and through the shared fail-closed closure
|
|
44
|
+
* (`textClosureOutcome` of `stateful-secrets.ts`, imported, never copied; a cap refuses). A request
|
|
45
|
+
* the checks cannot walk (cyclic, or nested too deeply) is `uncheckable-request`: `call` never
|
|
46
|
+
* throws. The copied credential key, parameter, and token lists live in `r2-guard.ts`;
|
|
47
|
+
* `test/r2.test.ts` holds one sample per entry of each, asserts each sample count equals its list's
|
|
48
|
+
* length, and checks every sample against the shared scan (`isPortCredentialKey`,
|
|
49
|
+
* `scanPortFixtureForSecrets`) and the emulator, so an entry added here without a sample fails. A
|
|
50
|
+
* name or pattern the shared scan adds later is not enforced until it is copied here with a sample.
|
|
51
|
+
*
|
|
52
|
+
* Request-shape latitude (the only one): credential fields are never compared or recorded, and JSON
|
|
53
|
+
* key order is not compared. Everything else (the endpoint, bucket, key, content type, `maxBytes`,
|
|
54
|
+
* `expectedEtag`, the put `condition`, `bodyBase64`, and `maxUploadBytes`) must equal a fixture
|
|
55
|
+
* request exactly, so only the fixtures' `run-synthetic` run id is emulated. A `bodyBase64` must be
|
|
56
|
+
* canonical standard base64 of UTF-8 text (else `uncheckable-body`); it is compared as sent but
|
|
57
|
+
* recorded only as its decoded length. No object may have an own `__proto__` key (else
|
|
58
|
+
* `invalid-request`), and no key or value, the decoded body included, may carry a credential or
|
|
59
|
+
* repeat a dropped credential value of any length (else `credential-in-request`). Every refusal is
|
|
60
|
+
* ledgered with constant text only (request `<redacted>`). Anything that does not match (an unknown
|
|
61
|
+
* port or method, no matching fixture, or no fixture consistent with the bucket) fails closed with
|
|
62
|
+
* a ledgered `notEmulated` answer, the port analogue of HTTP 400.
|
|
63
|
+
*
|
|
64
|
+
* @experimental
|
|
65
|
+
*/
|
|
66
|
+
/** The presigner port the presign fixture and manifest route name. */
|
|
67
|
+
const r2EmulatorPresignerPort = "R2Presigner";
|
|
68
|
+
/** The object-client port the get and put fixtures and manifest routes name. */
|
|
69
|
+
const r2EmulatorObjectClientPort = "R2ObjectClient";
|
|
70
|
+
/** Manifest routes of port emulators use this pseudo method; `path` is `<Port>.<method>`. */
|
|
71
|
+
const r2EmulatorRouteMethod = "PORT";
|
|
72
|
+
const portRoute = (port, method, write, caseIds) => ({
|
|
73
|
+
method: r2EmulatorRouteMethod,
|
|
74
|
+
path: `${port}.${method}`,
|
|
75
|
+
kind: "connector",
|
|
76
|
+
write,
|
|
77
|
+
caseIds,
|
|
78
|
+
evidence: "unverified",
|
|
79
|
+
observedAt: void 0
|
|
80
|
+
});
|
|
81
|
+
const presignUploadUrl = "r2.presign.put-upload-url";
|
|
82
|
+
const getMaxBytes = "r2.objects.get-max-bytes";
|
|
83
|
+
const getExpectedEtag = "r2.objects.get-expected-etag";
|
|
84
|
+
const getMissing = "r2.objects.get-missing-not-found";
|
|
85
|
+
const createIfAbsent = "r2.objects.create-if-absent";
|
|
86
|
+
const updateIfMatch = "r2.objects.update-if-match";
|
|
87
|
+
/**
|
|
88
|
+
* Route evidence manifest: every emulated port method (`PORT <Port>.<method>`) and the R2
|
|
89
|
+
* conformance cases whose (synthetic, unverified) fixtures it follows. `R2ObjectClient.put` is an
|
|
90
|
+
* unverified connector write: it needs a pending entry in the repo's evidence check until an
|
|
91
|
+
* owner-approved live run against a practice bucket verifies it. Presigning is local signing and
|
|
92
|
+
* `get` reads, so neither is a write.
|
|
93
|
+
*/
|
|
94
|
+
const r2EmulatorRoutes = [
|
|
95
|
+
portRoute(r2EmulatorPresignerPort, "presignPutObject", false, [presignUploadUrl]),
|
|
96
|
+
portRoute(r2EmulatorObjectClientPort, "get", false, [
|
|
97
|
+
getMaxBytes,
|
|
98
|
+
getExpectedEtag,
|
|
99
|
+
getMissing,
|
|
100
|
+
createIfAbsent,
|
|
101
|
+
updateIfMatch
|
|
102
|
+
]),
|
|
103
|
+
portRoute(r2EmulatorObjectClientPort, "put", true, [createIfAbsent, updateIfMatch])
|
|
104
|
+
];
|
|
105
|
+
const NonEmpty = Schema.String.check(Schema.isNonEmpty());
|
|
106
|
+
/** Bytes of canonical standard base64, or `undefined` when it does not decode canonically. */
|
|
107
|
+
const base64Bytes = (text) => {
|
|
108
|
+
let binary;
|
|
109
|
+
try {
|
|
110
|
+
binary = atob(text);
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
return btoa(binary) === text ? Uint8Array.from(binary, (character) => character.charCodeAt(0)) : void 0;
|
|
115
|
+
};
|
|
116
|
+
const Base64 = Schema.String.check(Schema.makeFilter((text) => base64Bytes(text) !== void 0, {
|
|
117
|
+
identifier: "R2EmulatorBase64",
|
|
118
|
+
description: "canonical standard base64"
|
|
119
|
+
}));
|
|
120
|
+
/** One object in the emulated bucket: its key, its etag (quotes kept), and its bytes as base64. */
|
|
121
|
+
const R2EmulatorObject = Schema.Struct({
|
|
122
|
+
key: NonEmpty,
|
|
123
|
+
etag: NonEmpty,
|
|
124
|
+
bodyBase64: Base64
|
|
125
|
+
});
|
|
126
|
+
const R2EmulatorBucket = Schema.Struct({
|
|
127
|
+
name: NonEmpty,
|
|
128
|
+
objects: Schema.Array(R2EmulatorObject)
|
|
129
|
+
});
|
|
130
|
+
/** The emulated buckets: plain JSON, used both as the seed and as the `state()` snapshot. */
|
|
131
|
+
const R2EmulatorSeed = Schema.Struct({ buckets: Schema.Array(R2EmulatorBucket) });
|
|
132
|
+
/**
|
|
133
|
+
* The synthetic practice bucket the R2 fixtures describe (the default seed): the seeded object the
|
|
134
|
+
* get fixtures read, with the etag and bytes they answer.
|
|
135
|
+
*/
|
|
136
|
+
const r2EmulatorDefaultSeed = { buckets: [{
|
|
137
|
+
name: "yolk-synthetic-bucket",
|
|
138
|
+
objects: [{
|
|
139
|
+
key: "fixtures/synthetic-object.txt",
|
|
140
|
+
etag: "\"a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5\"",
|
|
141
|
+
bodyBase64: "U3ludGhldGljIGNvbmZvcm1hbmNlIG9iamVjdDogc2FmZSB0byByZWFkLgo="
|
|
142
|
+
}]
|
|
143
|
+
}] };
|
|
144
|
+
const Failure = Schema.Struct({
|
|
145
|
+
kind: Schema.Literals(["expected", "error"]),
|
|
146
|
+
code: NonEmpty,
|
|
147
|
+
message: Schema.String,
|
|
148
|
+
status: Schema.optionalKey(Schema.Int)
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* A fault: answer the next `count` calls (every one when omitted) of `port` and `method` whose
|
|
152
|
+
* credential-free request contains `match` (a deep subset: objects by key, arrays element by
|
|
153
|
+
* element, other values exactly) with `failure` instead of a fixture. Faults apply only to a call a
|
|
154
|
+
* fixture would answer, and change no bucket state.
|
|
155
|
+
*/
|
|
156
|
+
const R2EmulatorFault = Schema.Struct({
|
|
157
|
+
kind: Schema.Literal("failure"),
|
|
158
|
+
port: NonEmpty,
|
|
159
|
+
method: NonEmpty,
|
|
160
|
+
match: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)),
|
|
161
|
+
count: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
|
|
162
|
+
failure: Failure
|
|
163
|
+
});
|
|
164
|
+
/** Thrown by `makeR2Emulator` (invalid seed) and `faults.add` (invalid fault). */
|
|
165
|
+
var R2EmulatorInputInvalid = class extends Data.TaggedError("R2EmulatorInputInvalid") {
|
|
166
|
+
get message() {
|
|
167
|
+
return `Invalid R2 emulator ${this.input}: ${this.reason}`;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const notEmulatedText = {
|
|
171
|
+
"unknown-method": "unknown-method: the port and method have no emulated route",
|
|
172
|
+
"invalid-request": "invalid-request: the request is not a JSON object",
|
|
173
|
+
"uncheckable-body": "uncheckable-body: a bodyBase64 is not canonical base64 of UTF-8 text",
|
|
174
|
+
"credential-in-request": "credential-in-request: the request carries a credential outside its credential fields",
|
|
175
|
+
"uncheckable-request": "uncheckable-request: the request could not be checked",
|
|
176
|
+
"no-matching-fixture": "no-matching-fixture: no fixture matches this request",
|
|
177
|
+
"state-conflict": "state-conflict: no matching fixture is consistent with the emulated bucket"
|
|
178
|
+
};
|
|
179
|
+
/** The constant port, method, and request of a ledger entry that records nothing it received. */
|
|
180
|
+
const r2EmulatorRedacted = "<redacted>";
|
|
181
|
+
const r2EmulatorUnrecognised = "<unrecognised>";
|
|
182
|
+
const isJsonObject = (value) => value !== void 0 && value !== null && Predicate.isObject(value) && !Array.isArray(value);
|
|
183
|
+
const objectField = (value, key) => {
|
|
184
|
+
const field = isJsonObject(value) ? value[key] : void 0;
|
|
185
|
+
return isJsonObject(field) ? field : void 0;
|
|
186
|
+
};
|
|
187
|
+
const stringField = (value, key) => {
|
|
188
|
+
const field = isJsonObject(value) ? value[key] : void 0;
|
|
189
|
+
return Predicate.isString(field) ? field : void 0;
|
|
190
|
+
};
|
|
191
|
+
const numberField = (value, key) => {
|
|
192
|
+
const field = isJsonObject(value) ? value[key] : void 0;
|
|
193
|
+
return Predicate.isNumber(field) ? field : void 0;
|
|
194
|
+
};
|
|
195
|
+
/** The field that carries put bytes as base64; checked and recorded only as decoded text. */
|
|
196
|
+
const bodyField = "bodyBase64";
|
|
197
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
198
|
+
/** The UTF-8 text of canonical standard base64, or `undefined` when it is anything else. */
|
|
199
|
+
const bodyText = (value) => {
|
|
200
|
+
const bytes = Predicate.isString(value) ? base64Bytes(value) : void 0;
|
|
201
|
+
if (bytes === void 0) return void 0;
|
|
202
|
+
try {
|
|
203
|
+
return utf8.decode(bytes);
|
|
204
|
+
} catch {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
/** Every `bodyBase64` value of `value`, at any depth. */
|
|
209
|
+
const bodyValues = (value) => {
|
|
210
|
+
if (Array.isArray(value)) return value.flatMap(bodyValues);
|
|
211
|
+
if (!isJsonObject(value)) return [];
|
|
212
|
+
return Object.entries(value).flatMap(([key, item]) => key === bodyField ? [item] : bodyValues(item));
|
|
213
|
+
};
|
|
214
|
+
/** A copy with every `bodyBase64` value replaced by `<redacted>`, at any depth. */
|
|
215
|
+
const withoutBodies = (value) => {
|
|
216
|
+
if (Array.isArray(value)) return value.map(withoutBodies);
|
|
217
|
+
if (!isJsonObject(value)) return value;
|
|
218
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, key === bodyField ? r2EmulatorRedacted : withoutBodies(item)]));
|
|
219
|
+
};
|
|
220
|
+
/** Deep subset: objects by key, arrays element by element (same length), other values exactly. */
|
|
221
|
+
const containsSubset = (value, pattern) => {
|
|
222
|
+
if (value === void 0) return false;
|
|
223
|
+
if (Array.isArray(pattern)) return Array.isArray(value) && value.length === pattern.length && pattern.every((item, index) => containsSubset(value[index], item));
|
|
224
|
+
if (isJsonObject(pattern)) return isJsonObject(value) && Object.entries(pattern).every(([key, item]) => Object.hasOwn(value, key) && containsSubset(value[key], item));
|
|
225
|
+
return Equal.equals(value, pattern);
|
|
226
|
+
};
|
|
227
|
+
const bucketsFrom = (seed) => ({ buckets: seed.buckets.map((bucket) => ({
|
|
228
|
+
name: bucket.name,
|
|
229
|
+
objects: bucket.objects.map((object) => ({ ...object }))
|
|
230
|
+
})) });
|
|
231
|
+
const snapshotOf = (state) => ({ buckets: state.buckets.map((bucket) => ({
|
|
232
|
+
name: bucket.name,
|
|
233
|
+
objects: bucket.objects.map((object) => ({ ...object }))
|
|
234
|
+
})) });
|
|
235
|
+
const bucketOf = (state, request) => {
|
|
236
|
+
const name = stringField(request, "bucket");
|
|
237
|
+
return state.buckets.find((bucket) => bucket.name === name);
|
|
238
|
+
};
|
|
239
|
+
const objectIn = (bucket, request) => {
|
|
240
|
+
const key = stringField(request, "key");
|
|
241
|
+
return bucket.objects.find((object) => object.key === key);
|
|
242
|
+
};
|
|
243
|
+
const byteLength = (bodyBase64) => bodyBase64 === void 0 ? void 0 : base64Bytes(bodyBase64)?.byteLength;
|
|
244
|
+
/** Presigning is local signing: every matching fixture is consistent, and nothing changes. */
|
|
245
|
+
const presignModel = {
|
|
246
|
+
consistent: () => true,
|
|
247
|
+
apply: () => void 0
|
|
248
|
+
};
|
|
249
|
+
const getModel = {
|
|
250
|
+
consistent: (state, request, fixture) => {
|
|
251
|
+
const bucket = bucketOf(state, request);
|
|
252
|
+
if (bucket === void 0) return false;
|
|
253
|
+
const object = objectIn(bucket, request);
|
|
254
|
+
const expected = stringField(request, "expectedEtag");
|
|
255
|
+
const maxBytes = numberField(request, "maxBytes");
|
|
256
|
+
const size = byteLength(object?.bodyBase64);
|
|
257
|
+
const etagHolds = object !== void 0 && (expected === void 0 || expected === object.etag);
|
|
258
|
+
if (fixture.failure !== void 0) switch (fixture.failure.code) {
|
|
259
|
+
case "not_found": return object === void 0;
|
|
260
|
+
case "conflict": return object !== void 0 && expected !== void 0 && expected !== object.etag;
|
|
261
|
+
case "response_too_large": return etagHolds && size !== void 0 && maxBytes !== void 0 && size > maxBytes;
|
|
262
|
+
default: return false;
|
|
263
|
+
}
|
|
264
|
+
return object !== void 0 && etagHolds && size !== void 0 && maxBytes !== void 0 && size <= maxBytes && Equal.equals(fixture.response, {
|
|
265
|
+
etag: object.etag,
|
|
266
|
+
size,
|
|
267
|
+
bodyBase64: object.bodyBase64
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
apply: () => void 0
|
|
271
|
+
};
|
|
272
|
+
const putModel = {
|
|
273
|
+
consistent: (state, request, fixture) => {
|
|
274
|
+
const bucket = bucketOf(state, request);
|
|
275
|
+
if (bucket === void 0) return false;
|
|
276
|
+
const object = objectIn(bucket, request);
|
|
277
|
+
const condition = objectField(request, "condition");
|
|
278
|
+
const kind = stringField(condition, "kind");
|
|
279
|
+
const conditionEtag = stringField(condition, "etag");
|
|
280
|
+
if (fixture.failure !== void 0) {
|
|
281
|
+
if (fixture.failure.code !== "conflict") return false;
|
|
282
|
+
return kind === "absent" ? object !== void 0 : kind === "etag" && object !== void 0 && object.etag !== conditionEtag;
|
|
283
|
+
}
|
|
284
|
+
const size = byteLength(stringField(request, "bodyBase64"));
|
|
285
|
+
const maxUploadBytes = numberField(request, "maxUploadBytes");
|
|
286
|
+
const etag = stringField(fixture.response, "etag");
|
|
287
|
+
return (kind === "absent" ? object === void 0 : kind === "etag" && object !== void 0 && object.etag === conditionEtag && etag !== object.etag) && etag !== void 0 && size !== void 0 && maxUploadBytes !== void 0 && size <= maxUploadBytes && numberField(fixture.response, "size") === size;
|
|
288
|
+
},
|
|
289
|
+
apply: (state, request, fixture) => {
|
|
290
|
+
const bucket = bucketOf(state, request);
|
|
291
|
+
const key = stringField(request, "key");
|
|
292
|
+
const bodyBase64 = stringField(request, "bodyBase64");
|
|
293
|
+
const etag = stringField(fixture.response, "etag");
|
|
294
|
+
if (fixture.failure !== void 0 || bucket === void 0 || key === void 0 || bodyBase64 === void 0 || etag === void 0) return;
|
|
295
|
+
const object = objectIn(bucket, request);
|
|
296
|
+
if (object === void 0) bucket.objects.push({
|
|
297
|
+
key,
|
|
298
|
+
etag,
|
|
299
|
+
bodyBase64
|
|
300
|
+
});
|
|
301
|
+
else {
|
|
302
|
+
object.etag = etag;
|
|
303
|
+
object.bodyBase64 = bodyBase64;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const routePath = (port, method) => `${port}.${method}`;
|
|
308
|
+
const routeModels = new Map([
|
|
309
|
+
[emulatorRouteKey(r2EmulatorRouteMethod, routePath(r2EmulatorPresignerPort, "presignPutObject")), presignModel],
|
|
310
|
+
[emulatorRouteKey(r2EmulatorRouteMethod, routePath(r2EmulatorObjectClientPort, "get")), getModel],
|
|
311
|
+
[emulatorRouteKey(r2EmulatorRouteMethod, routePath(r2EmulatorObjectClientPort, "put")), putModel]
|
|
312
|
+
]);
|
|
313
|
+
const strict = { onExcessProperty: "error" };
|
|
314
|
+
const decodeSeed = Schema.decodeUnknownResult(R2EmulatorSeed, strict);
|
|
315
|
+
const decodeFault = Schema.decodeUnknownResult(R2EmulatorFault, strict);
|
|
316
|
+
const issueText = (error) => error.message;
|
|
317
|
+
/** Bucket names, and object keys within a bucket, must be unique so fixtures address one object. */
|
|
318
|
+
const seedProblem = (seed) => {
|
|
319
|
+
const names = seed.buckets.map((bucket) => bucket.name);
|
|
320
|
+
if (new Set(names).size !== names.length) return "bucket names must be unique";
|
|
321
|
+
for (const bucket of seed.buckets) {
|
|
322
|
+
const keys = bucket.objects.map((object) => object.key);
|
|
323
|
+
if (new Set(keys).size !== keys.length) return `object keys in ${bucket.name} must be unique`;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Create an R2 emulator. Each call has its own buckets, ledger, faults, and fixture use.
|
|
328
|
+
*
|
|
329
|
+
* `call(port, method, request)` answers in this order: a port and method outside the manifest
|
|
330
|
+
* (`unknown-method`), a non-object request or one with an own `__proto__` key (`invalid-request`),
|
|
331
|
+
* a `bodyBase64` that is not canonical base64 of UTF-8 text (`uncheckable-body`), or a credential
|
|
332
|
+
* outside the credential fields (`credential-in-request`) fails closed; otherwise the fixtures
|
|
333
|
+
* whose port, method, and credential-free request match are checked against the buckets, and the
|
|
334
|
+
* first consistent one (preferring one not used since the last reset) is chosen; no match
|
|
335
|
+
* (`no-matching-fixture`) or no consistent match (`state-conflict`) fails closed. Only then does
|
|
336
|
+
* the first active matching fault answer its failure (changing nothing); otherwise the chosen
|
|
337
|
+
* fixture answers and updates the buckets. Every refusal is ledgered with constant text only; a
|
|
338
|
+
* request the checks cannot walk is refused as `uncheckable-request`, so `call` never throws.
|
|
339
|
+
*
|
|
340
|
+
* Throws `R2EmulatorInputInvalid` for an invalid seed.
|
|
341
|
+
*/
|
|
342
|
+
const makeR2Emulator = (options = {}) => {
|
|
343
|
+
const seedResult = decodeSeed(options.seed ?? r2EmulatorDefaultSeed);
|
|
344
|
+
if (Result.isFailure(seedResult)) throw new R2EmulatorInputInvalid({
|
|
345
|
+
input: "seed",
|
|
346
|
+
reason: issueText(seedResult.failure)
|
|
347
|
+
});
|
|
348
|
+
const seed = seedResult.success;
|
|
349
|
+
const problem = seedProblem(seed);
|
|
350
|
+
if (problem !== void 0) throw new R2EmulatorInputInvalid({
|
|
351
|
+
input: "seed",
|
|
352
|
+
reason: problem
|
|
353
|
+
});
|
|
354
|
+
const bound = new Map(bindRouteHandlers(r2EmulatorRoutes, routeModels).map(({ route, handler }) => [route.path, {
|
|
355
|
+
route,
|
|
356
|
+
model: handler
|
|
357
|
+
}]));
|
|
358
|
+
let state = bucketsFrom(seed);
|
|
359
|
+
let entries = [];
|
|
360
|
+
let faults = [];
|
|
361
|
+
let nextFaultId = 1;
|
|
362
|
+
let nextSeq = 1;
|
|
363
|
+
const used = /* @__PURE__ */ new Set();
|
|
364
|
+
const record = (entry) => {
|
|
365
|
+
entries.push({
|
|
366
|
+
seq: nextSeq,
|
|
367
|
+
...entry
|
|
368
|
+
});
|
|
369
|
+
nextSeq += 1;
|
|
370
|
+
};
|
|
371
|
+
const refuse = (entry, reason) => {
|
|
372
|
+
record({
|
|
373
|
+
...entry,
|
|
374
|
+
outcome: "not-emulated",
|
|
375
|
+
reason
|
|
376
|
+
});
|
|
377
|
+
return { notEmulated: { reason: notEmulatedText[reason] } };
|
|
378
|
+
};
|
|
379
|
+
const answer = (port, method, rawRequest) => {
|
|
380
|
+
const target = bound.get(routePath(port, method));
|
|
381
|
+
if (target === void 0) return refuse({
|
|
382
|
+
port: r2EmulatorUnrecognised,
|
|
383
|
+
method: r2EmulatorUnrecognised,
|
|
384
|
+
request: r2EmulatorRedacted,
|
|
385
|
+
evidence: "unknown-method"
|
|
386
|
+
}, "unknown-method");
|
|
387
|
+
const evidence = target.route.evidence;
|
|
388
|
+
const constant = {
|
|
389
|
+
port,
|
|
390
|
+
method,
|
|
391
|
+
request: r2EmulatorRedacted,
|
|
392
|
+
evidence
|
|
393
|
+
};
|
|
394
|
+
if (!isJsonObject(rawRequest) || hasOwnProtoKey(rawRequest)) return refuse(constant, "invalid-request");
|
|
395
|
+
const request = withoutCredentials(rawRequest);
|
|
396
|
+
const bodies = bodyValues(request).map(bodyText);
|
|
397
|
+
const decodedBodies = bodies.flatMap((body) => body === void 0 ? [] : [body]);
|
|
398
|
+
if (decodedBodies.length !== bodies.length) return refuse(constant, "uncheckable-body");
|
|
399
|
+
const guarded = guardedValues(rawRequest);
|
|
400
|
+
if ([...textsOf(request), ...decodedBodies].some((text) => textCarriesCredential(text, guarded))) return refuse(constant, "credential-in-request");
|
|
401
|
+
const matching = r2EmulatorFixtures.filter((fixture) => fixture.port === port && fixture.method === method && isJsonObject(fixture.request) && Equal.equals(withoutCredentials(fixture.request), request));
|
|
402
|
+
if (matching.length === 0) return refuse(constant, "no-matching-fixture");
|
|
403
|
+
const consistent = matching.filter((fixture) => target.model.consistent(state, request, fixture));
|
|
404
|
+
const chosen = consistent.find((fixture) => !used.has(fixture.id)) ?? consistent[0];
|
|
405
|
+
if (chosen === void 0) return refuse(constant, "state-conflict");
|
|
406
|
+
const bodyBytes = byteLength(stringField(request, bodyField));
|
|
407
|
+
const ledgerRequest = withoutBodies(request);
|
|
408
|
+
const recorded = (entry) => {
|
|
409
|
+
const base = {
|
|
410
|
+
port,
|
|
411
|
+
method,
|
|
412
|
+
request: ledgerRequest,
|
|
413
|
+
evidence,
|
|
414
|
+
...entry
|
|
415
|
+
};
|
|
416
|
+
record(bodyBytes === void 0 ? base : {
|
|
417
|
+
...base,
|
|
418
|
+
bodyBytes
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
const fault = faults.find((candidate) => candidate.fault.port === port && candidate.fault.method === method && (candidate.remaining === void 0 || candidate.remaining > 0) && (candidate.fault.match === void 0 || containsSubset(request, candidate.fault.match)));
|
|
422
|
+
if (fault !== void 0) {
|
|
423
|
+
fault.applied += 1;
|
|
424
|
+
if (fault.remaining !== void 0) fault.remaining -= 1;
|
|
425
|
+
recorded({
|
|
426
|
+
outcome: "fault",
|
|
427
|
+
faultId: fault.id
|
|
428
|
+
});
|
|
429
|
+
return { failure: fault.fault.failure };
|
|
430
|
+
}
|
|
431
|
+
target.model.apply(state, request, chosen);
|
|
432
|
+
used.add(chosen.id);
|
|
433
|
+
recorded({
|
|
434
|
+
outcome: "answered",
|
|
435
|
+
fixtureId: chosen.id
|
|
436
|
+
});
|
|
437
|
+
return chosen.failure === void 0 ? { response: chosen.response } : { failure: chosen.failure };
|
|
438
|
+
};
|
|
439
|
+
/**
|
|
440
|
+
* `answer`, never throwing: a request the checks cannot walk (a cyclic or too deeply nested
|
|
441
|
+
* value) is ledgered with constant text and answered not-emulated. Every check runs before any
|
|
442
|
+
* fault is used or any state changes, so a throw leaves both untouched.
|
|
443
|
+
*/
|
|
444
|
+
const call = (port, method, rawRequest) => {
|
|
445
|
+
try {
|
|
446
|
+
return answer(port, method, rawRequest);
|
|
447
|
+
} catch {
|
|
448
|
+
return refuse({
|
|
449
|
+
port: r2EmulatorUnrecognised,
|
|
450
|
+
method: r2EmulatorUnrecognised,
|
|
451
|
+
request: r2EmulatorRedacted,
|
|
452
|
+
evidence: "unknown-method"
|
|
453
|
+
}, "uncheckable-request");
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const faultState = (fault) => ({
|
|
457
|
+
id: fault.id,
|
|
458
|
+
fault: fault.fault,
|
|
459
|
+
remaining: fault.remaining,
|
|
460
|
+
applied: fault.applied
|
|
461
|
+
});
|
|
462
|
+
return {
|
|
463
|
+
call,
|
|
464
|
+
state: () => snapshotOf(state),
|
|
465
|
+
seed: () => snapshotOf(bucketsFrom(seed)),
|
|
466
|
+
ledger: {
|
|
467
|
+
entries: () => entries.map((entry) => ({ ...entry })),
|
|
468
|
+
clear: () => {
|
|
469
|
+
entries = [];
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
reset: () => {
|
|
473
|
+
state = bucketsFrom(seed);
|
|
474
|
+
entries = [];
|
|
475
|
+
faults = [];
|
|
476
|
+
used.clear();
|
|
477
|
+
},
|
|
478
|
+
faults: {
|
|
479
|
+
add: (input) => {
|
|
480
|
+
const decoded = decodeFault(input);
|
|
481
|
+
if (Result.isFailure(decoded)) throw new R2EmulatorInputInvalid({
|
|
482
|
+
input: "fault",
|
|
483
|
+
reason: issueText(decoded.failure)
|
|
484
|
+
});
|
|
485
|
+
if (!bound.has(routePath(decoded.success.port, decoded.success.method))) throw new R2EmulatorInputInvalid({
|
|
486
|
+
input: "fault",
|
|
487
|
+
reason: "the port and method have no emulated route"
|
|
488
|
+
});
|
|
489
|
+
const added = {
|
|
490
|
+
id: nextFaultId,
|
|
491
|
+
fault: decoded.success,
|
|
492
|
+
remaining: decoded.success.count,
|
|
493
|
+
applied: 0
|
|
494
|
+
};
|
|
495
|
+
nextFaultId += 1;
|
|
496
|
+
faults.push(added);
|
|
497
|
+
return faultState(added);
|
|
498
|
+
},
|
|
499
|
+
list: () => faults.map(faultState),
|
|
500
|
+
clear: () => {
|
|
501
|
+
faults = [];
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
coverage: () => ({
|
|
505
|
+
routes: r2EmulatorRoutes.map((route) => ({
|
|
506
|
+
...route,
|
|
507
|
+
calls: entries.filter((entry) => routePath(entry.port, entry.method) === route.path).length
|
|
508
|
+
})),
|
|
509
|
+
notEmulatedCalls: entries.filter((entry) => entry.outcome === "not-emulated").length,
|
|
510
|
+
unusedFixtureIds: r2EmulatorFixtures.flatMap((fixture) => used.has(fixture.id) ? [] : [fixture.id])
|
|
511
|
+
})
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
//#endregion
|
|
515
|
+
export { R2EmulatorBucket, R2EmulatorFault, R2EmulatorInputInvalid, R2EmulatorObject, R2EmulatorSeed, makeR2Emulator, r2EmulatorDefaultSeed, r2EmulatorFixtures, r2EmulatorObjectClientPort, r2EmulatorPresignerPort, r2EmulatorRedacted, r2EmulatorRouteMethod, r2EmulatorRoutes, r2EmulatorUnrecognised };
|
|
516
|
+
|
|
517
|
+
//# sourceMappingURL=r2.mjs.map
|
package/dist/r2.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.mjs","names":[],"sources":["../src/r2.ts"],"sourcesContent":["/**\n * R2 emulator: a fixture-driven, in-memory fake backend for the host R2 ports `R2Presigner` and\n * `R2ObjectClient`.\n *\n * The R2 connector never talks to R2 itself: hosts implement `R2Presigner` (local SigV4 signing of\n * a PUT URL) and `R2ObjectClient` (conditional get and put). This emulator stands in for such a\n * host at the port level: `call(port, method, request)` takes one port call as plain JSON (the\n * credential-free request; bytes as base64) and answers `{ response }`, `{ failure }`, or a\n * fail-closed `{ notEmulated }`. There is no S3, SigV4 signer, socket, `node:` builtin, or SDK\n * import here: it is a plain structural object, and `r2PortsLayerFromBackend` in\n * `@yolk-sdk/connectors/r2-storage/conformance` turns it into both port layers.\n *\n * Response behaviour comes ONLY from the R2 conformance fixtures (copied as data in\n * `r2-fixtures.ts`). The emulator keeps an in-memory bucket (objects with their etag and bytes)\n * seeded with the object the fixtures describe; the state only selects which fixture answers (the\n * first matching fixture consistent with the bucket) and is updated with what that fixture says\n * happened (an object created or replaced under the etag the fixture names). It never invents an\n * etag, a byte, a failure, or a response. Presigning writes nothing.\n *\n * Credentials: every credential field (the shared port scan's `isPortCredentialKey` names, copied\n * in `r2-guard.ts`) is dropped at any depth before anything is compared or recorded. Every refusal\n * (`unknown-method`, `invalid-request`, `uncheckable-body`, `credential-in-request`,\n * `uncheckable-request`, `no-matching-fixture`, `state-conflict`) is ledgered with constant text\n * only (request `<redacted>`; port and method `<unrecognised>` for `unknown-method` and\n * `uncheckable-request`), uses no fault, and changes no state. Only a request equal to a fixture\n * request (minus credential fields) is recorded, so the ledger holds fixture text, with every\n * `bodyBase64` as `<redacted>` plus `bodyBytes` (the decoded length). An own `__proto__` key at any\n * depth is `invalid-request`. Every `bodyBase64` must be canonical standard base64 of UTF-8 text\n * (else `uncheckable-body`); its decoded text is checked with the request's own texts. A request is\n * `credential-in-request` when any key, string value, number (as printed and, for an integer, as\n * its digit string), or decoded body repeats a guarded value, or holds, once the exact canonical\n * placeholders are blanked out, a SigV4 credential name, a credential query parameter, or a token\n * the shared scan flags (a bearer token, an API-key prefix, a JSON Web Token, a PEM private key).\n * Guarded values are every non-empty key, string, and number under a credential field (a numeric\n * string also in its number forms), with no minimum length, unlike the shared helpers. Both checks\n * run raw, in R2's own decodings (`decodedVariants`: up to three percent and three escape rounds,\n * with `\\uXXXX`, `\\xXX`, and numeric HTML references), and through the shared fail-closed closure\n * (`textClosureOutcome` of `stateful-secrets.ts`, imported, never copied; a cap refuses). A request\n * the checks cannot walk (cyclic, or nested too deeply) is `uncheckable-request`: `call` never\n * throws. The copied credential key, parameter, and token lists live in `r2-guard.ts`;\n * `test/r2.test.ts` holds one sample per entry of each, asserts each sample count equals its list's\n * length, and checks every sample against the shared scan (`isPortCredentialKey`,\n * `scanPortFixtureForSecrets`) and the emulator, so an entry added here without a sample fails. A\n * name or pattern the shared scan adds later is not enforced until it is copied here with a sample.\n *\n * Request-shape latitude (the only one): credential fields are never compared or recorded, and JSON\n * key order is not compared. Everything else (the endpoint, bucket, key, content type, `maxBytes`,\n * `expectedEtag`, the put `condition`, `bodyBase64`, and `maxUploadBytes`) must equal a fixture\n * request exactly, so only the fixtures' `run-synthetic` run id is emulated. A `bodyBase64` must be\n * canonical standard base64 of UTF-8 text (else `uncheckable-body`); it is compared as sent but\n * recorded only as its decoded length. No object may have an own `__proto__` key (else\n * `invalid-request`), and no key or value, the decoded body included, may carry a credential or\n * repeat a dropped credential value of any length (else `credential-in-request`). Every refusal is\n * ledgered with constant text only (request `<redacted>`). Anything that does not match (an unknown\n * port or method, no matching fixture, or no fixture consistent with the bucket) fails closed with\n * a ledgered `notEmulated` answer, the port analogue of HTTP 400.\n *\n * @experimental\n */\nimport { Data, Equal, Predicate, Result } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport {\n r2EmulatorFixtures,\n type R2EmulatorFailure,\n type R2EmulatorFixture\n} from './r2-fixtures.ts'\nimport {\n bindRouteHandlers,\n emulatorRouteKey,\n type EmulatorEvidence,\n type EmulatorRouteEvidence\n} from './route-evidence.ts'\nimport {\n guardedValues,\n hasOwnProtoKey,\n textCarriesCredential,\n textsOf,\n withoutCredentials\n} from './r2-guard.ts'\n\nexport type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'\n\nexport { r2EmulatorFixtures, type R2EmulatorFailure, type R2EmulatorFixture }\n\n/** The presigner port the presign fixture and manifest route name. */\nexport const r2EmulatorPresignerPort = 'R2Presigner'\n\n/** The object-client port the get and put fixtures and manifest routes name. */\nexport const r2EmulatorObjectClientPort = 'R2ObjectClient'\n\n/** Manifest routes of port emulators use this pseudo method; `path` is `<Port>.<method>`. */\nexport const r2EmulatorRouteMethod = 'PORT'\n\nconst portRoute = (\n port: string,\n method: string,\n write: boolean,\n caseIds: ReadonlyArray<string>\n): EmulatorRouteEvidence => ({\n method: r2EmulatorRouteMethod,\n path: `${port}.${method}`,\n kind: 'connector',\n write,\n caseIds,\n evidence: 'unverified',\n observedAt: undefined\n})\n\nconst presignUploadUrl = 'r2.presign.put-upload-url'\n\nconst getMaxBytes = 'r2.objects.get-max-bytes'\n\nconst getExpectedEtag = 'r2.objects.get-expected-etag'\n\nconst getMissing = 'r2.objects.get-missing-not-found'\n\nconst createIfAbsent = 'r2.objects.create-if-absent'\n\nconst updateIfMatch = 'r2.objects.update-if-match'\n\n/**\n * Route evidence manifest: every emulated port method (`PORT <Port>.<method>`) and the R2\n * conformance cases whose (synthetic, unverified) fixtures it follows. `R2ObjectClient.put` is an\n * unverified connector write: it needs a pending entry in the repo's evidence check until an\n * owner-approved live run against a practice bucket verifies it. Presigning is local signing and\n * `get` reads, so neither is a write.\n */\nexport const r2EmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> = [\n portRoute(r2EmulatorPresignerPort, 'presignPutObject', false, [presignUploadUrl]),\n portRoute(r2EmulatorObjectClientPort, 'get', false, [\n getMaxBytes,\n getExpectedEtag,\n getMissing,\n createIfAbsent,\n updateIfMatch\n ]),\n portRoute(r2EmulatorObjectClientPort, 'put', true, [createIfAbsent, updateIfMatch])\n]\n\nconst NonEmpty = Schema.String.check(Schema.isNonEmpty())\n\n/** Bytes of canonical standard base64, or `undefined` when it does not decode canonically. */\nconst base64Bytes = (text: string): Uint8Array | undefined => {\n let binary: string\n\n try {\n binary = atob(text)\n } catch {\n return undefined\n }\n\n return btoa(binary) === text\n ? Uint8Array.from(binary, character => character.charCodeAt(0))\n : undefined\n}\n\nconst Base64 = Schema.String.check(\n Schema.makeFilter((text: string) => base64Bytes(text) !== undefined, {\n identifier: 'R2EmulatorBase64',\n description: 'canonical standard base64'\n })\n)\n\n/** One object in the emulated bucket: its key, its etag (quotes kept), and its bytes as base64. */\nexport const R2EmulatorObject = Schema.Struct({\n key: NonEmpty,\n etag: NonEmpty,\n bodyBase64: Base64\n})\n\nexport type R2EmulatorObject = typeof R2EmulatorObject.Type\n\nexport const R2EmulatorBucket = Schema.Struct({\n name: NonEmpty,\n objects: Schema.Array(R2EmulatorObject)\n})\n\nexport type R2EmulatorBucket = typeof R2EmulatorBucket.Type\n\n/** The emulated buckets: plain JSON, used both as the seed and as the `state()` snapshot. */\nexport const R2EmulatorSeed = Schema.Struct({\n buckets: Schema.Array(R2EmulatorBucket)\n})\n\nexport type R2EmulatorSeed = typeof R2EmulatorSeed.Type\n\n/**\n * The synthetic practice bucket the R2 fixtures describe (the default seed): the seeded object the\n * get fixtures read, with the etag and bytes they answer.\n */\nexport const r2EmulatorDefaultSeed: R2EmulatorSeed = {\n buckets: [\n {\n name: 'yolk-synthetic-bucket',\n objects: [\n {\n key: 'fixtures/synthetic-object.txt',\n etag: '\"a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5\"',\n bodyBase64: 'U3ludGhldGljIGNvbmZvcm1hbmNlIG9iamVjdDogc2FmZSB0byByZWFkLgo='\n }\n ]\n }\n ]\n}\n\nconst Failure = Schema.Struct({\n kind: Schema.Literals(['expected', 'error']),\n code: NonEmpty,\n message: Schema.String,\n status: Schema.optionalKey(Schema.Int)\n})\n\n/**\n * A fault: answer the next `count` calls (every one when omitted) of `port` and `method` whose\n * credential-free request contains `match` (a deep subset: objects by key, arrays element by\n * element, other values exactly) with `failure` instead of a fixture. Faults apply only to a call a\n * fixture would answer, and change no bucket state.\n */\nexport const R2EmulatorFault = Schema.Struct({\n kind: Schema.Literal('failure'),\n port: NonEmpty,\n method: NonEmpty,\n match: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)),\n count: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),\n failure: Failure\n})\n\nexport type R2EmulatorFault = typeof R2EmulatorFault.Type\n\nexport type R2EmulatorFaultState = {\n readonly id: number\n readonly fault: R2EmulatorFault\n /** Remaining matching calls; `undefined` for an unlimited fault. */\n readonly remaining: number | undefined\n readonly applied: number\n}\n\n/** Thrown by `makeR2Emulator` (invalid seed) and `faults.add` (invalid fault). */\nexport class R2EmulatorInputInvalid extends Data.TaggedError('R2EmulatorInputInvalid')<{\n readonly input: 'seed' | 'fault'\n readonly reason: string\n}> {\n override get message(): string {\n return `Invalid R2 emulator ${this.input}: ${this.reason}`\n }\n}\n\n/** Why a call failed closed. */\nexport type R2EmulatorNotEmulatedReason =\n | 'unknown-method'\n | 'invalid-request'\n | 'uncheckable-body'\n | 'credential-in-request'\n | 'uncheckable-request'\n | 'no-matching-fixture'\n | 'state-conflict'\n\nconst notEmulatedText: Record<R2EmulatorNotEmulatedReason, string> = {\n 'unknown-method': 'unknown-method: the port and method have no emulated route',\n 'invalid-request': 'invalid-request: the request is not a JSON object',\n 'uncheckable-body': 'uncheckable-body: a bodyBase64 is not canonical base64 of UTF-8 text',\n 'credential-in-request':\n 'credential-in-request: the request carries a credential outside its credential fields',\n 'uncheckable-request': 'uncheckable-request: the request could not be checked',\n 'no-matching-fixture': 'no-matching-fixture: no fixture matches this request',\n 'state-conflict': 'state-conflict: no matching fixture is consistent with the emulated bucket'\n}\n\n/** The constant port, method, and request of a ledger entry that records nothing it received. */\nexport const r2EmulatorRedacted = '<redacted>'\n\nexport const r2EmulatorUnrecognised = '<unrecognised>'\n\n/** One emulator answer; structurally the reply of a plain-JSON `R2Backend`. */\nexport type R2EmulatorReply =\n | { readonly response: Schema.Json }\n | { readonly failure: R2EmulatorFailure }\n | { readonly notEmulated: { readonly reason: string } }\n\nexport type R2EmulatorLedgerEntry = {\n readonly seq: number\n /** The port, or `<unrecognised>` for an `unknown-method` or `uncheckable-request` refusal. */\n readonly port: string\n /** The method, or `<unrecognised>` for an `unknown-method` or `uncheckable-request` refusal. */\n readonly method: string\n /**\n * For an answered or faulted call, the request without credential fields (a fixture request) with\n * every `bodyBase64` value replaced by `<redacted>`; `<redacted>` whole for every refusal.\n */\n readonly request: Schema.Json\n /** The decoded byte length of a recorded request's top-level `bodyBase64`. */\n readonly bodyBytes?: number\n readonly outcome: 'answered' | 'fault' | 'not-emulated'\n /** `unknown-method` for a call outside the manifest. */\n readonly evidence: EmulatorEvidence | 'unknown-method'\n readonly fixtureId?: string\n readonly faultId?: number\n readonly reason?: R2EmulatorNotEmulatedReason\n}\n\nexport type R2EmulatorRouteCoverage = EmulatorRouteEvidence & {\n /** Ledger calls to this route since the last ledger clear or reset. */\n readonly calls: number\n}\n\nexport type R2EmulatorCoverage = {\n readonly routes: ReadonlyArray<R2EmulatorRouteCoverage>\n /** Ledger calls that failed closed. */\n readonly notEmulatedCalls: number\n /** Fixtures that have not answered a call since the emulator was made or last reset. */\n readonly unusedFixtureIds: ReadonlyArray<string>\n}\n\nexport type R2EmulatorOptions = {\n /** The initial buckets. Defaults to `r2EmulatorDefaultSeed`. */\n readonly seed?: R2EmulatorSeed\n}\n\nexport type R2Emulator = {\n /** Answer one R2 port call; never throws. This makes the emulator an `R2Backend`. */\n readonly call: (port: string, method: string, request: Schema.Json) => R2EmulatorReply\n /** The current buckets (a copy). */\n readonly state: () => R2EmulatorSeed\n /** The seed the emulator started from (a copy). */\n readonly seed: () => R2EmulatorSeed\n readonly ledger: {\n readonly entries: () => ReadonlyArray<R2EmulatorLedgerEntry>\n readonly clear: () => void\n }\n /** Clear the ledger and faults, forget fixture use, and restore the seeded buckets. */\n readonly reset: () => void\n readonly faults: {\n readonly add: (fault: R2EmulatorFault) => R2EmulatorFaultState\n readonly list: () => ReadonlyArray<R2EmulatorFaultState>\n readonly clear: () => void\n }\n readonly coverage: () => R2EmulatorCoverage\n}\n\n// JSON helpers.\n\ntype JsonObject = Schema.JsonObject\n\nconst isJsonObject = (value: Schema.Json | undefined): value is JsonObject =>\n value !== undefined && value !== null && Predicate.isObject(value) && !Array.isArray(value)\n\nconst objectField = (value: Schema.Json | undefined, key: string): JsonObject | undefined => {\n const field = isJsonObject(value) ? value[key] : undefined\n\n return isJsonObject(field) ? field : undefined\n}\n\nconst stringField = (value: Schema.Json | undefined, key: string): string | undefined => {\n const field = isJsonObject(value) ? value[key] : undefined\n\n return Predicate.isString(field) ? field : undefined\n}\n\nconst numberField = (value: Schema.Json | undefined, key: string): number | undefined => {\n const field = isJsonObject(value) ? value[key] : undefined\n\n return Predicate.isNumber(field) ? field : undefined\n}\n\n/** The field that carries put bytes as base64; checked and recorded only as decoded text. */\nconst bodyField = 'bodyBase64'\n\nconst utf8 = new TextDecoder('utf-8', { fatal: true })\n\n/** The UTF-8 text of canonical standard base64, or `undefined` when it is anything else. */\nconst bodyText = (value: Schema.Json): string | undefined => {\n const bytes = Predicate.isString(value) ? base64Bytes(value) : undefined\n\n if (bytes === undefined) return undefined\n\n try {\n return utf8.decode(bytes)\n } catch {\n return undefined\n }\n}\n\n/** Every `bodyBase64` value of `value`, at any depth. */\nconst bodyValues = (value: Schema.Json): ReadonlyArray<Schema.Json> => {\n if (Array.isArray(value)) return value.flatMap(bodyValues)\n\n if (!isJsonObject(value)) return []\n\n return Object.entries(value).flatMap(([key, item]) =>\n key === bodyField ? [item] : bodyValues(item)\n )\n}\n\n/** A copy with every `bodyBase64` value replaced by `<redacted>`, at any depth. */\nconst withoutBodies = (value: Schema.Json): Schema.Json => {\n if (Array.isArray(value)) return value.map(withoutBodies)\n\n if (!isJsonObject(value)) return value\n\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [\n key,\n key === bodyField ? r2EmulatorRedacted : withoutBodies(item)\n ])\n )\n}\n\n/** Deep subset: objects by key, arrays element by element (same length), other values exactly. */\nconst containsSubset = (value: Schema.Json | undefined, pattern: Schema.Json): boolean => {\n if (value === undefined) return false\n\n if (Array.isArray(pattern)) {\n return (\n Array.isArray(value) &&\n value.length === pattern.length &&\n pattern.every((item, index) => containsSubset(value[index], item))\n )\n }\n\n if (isJsonObject(pattern)) {\n return (\n isJsonObject(value) &&\n Object.entries(pattern).every(\n ([key, item]) => Object.hasOwn(value, key) && containsSubset(value[key], item)\n )\n )\n }\n\n return Equal.equals(value, pattern)\n}\n\n// Bucket state.\n\ntype ObjectState = { readonly key: string; etag: string; bodyBase64: string }\n\ntype BucketState = { readonly name: string; objects: Array<ObjectState> }\n\ntype BucketsState = { readonly buckets: Array<BucketState> }\n\nconst bucketsFrom = (seed: R2EmulatorSeed): BucketsState => ({\n buckets: seed.buckets.map(bucket => ({\n name: bucket.name,\n objects: bucket.objects.map(object => ({ ...object }))\n }))\n})\n\nconst snapshotOf = (state: BucketsState): R2EmulatorSeed => ({\n buckets: state.buckets.map(bucket => ({\n name: bucket.name,\n objects: bucket.objects.map(object => ({ ...object }))\n }))\n})\n\nconst bucketOf = (state: BucketsState, request: JsonObject): BucketState | undefined => {\n const name = stringField(request, 'bucket')\n\n return state.buckets.find(bucket => bucket.name === name)\n}\n\nconst objectIn = (bucket: BucketState, request: JsonObject): ObjectState | undefined => {\n const key = stringField(request, 'key')\n\n return bucket.objects.find(object => object.key === key)\n}\n\nconst byteLength = (bodyBase64: string | undefined): number | undefined =>\n bodyBase64 === undefined ? undefined : base64Bytes(bodyBase64)?.byteLength\n\n/** A state model for one route: whether a fixture is consistent, and what it changes. */\ntype RouteModel = {\n readonly consistent: (\n state: BucketsState,\n request: JsonObject,\n fixture: R2EmulatorFixture\n ) => boolean\n readonly apply: (state: BucketsState, request: JsonObject, fixture: R2EmulatorFixture) => void\n}\n\n/** Presigning is local signing: every matching fixture is consistent, and nothing changes. */\nconst presignModel: RouteModel = {\n consistent: () => true,\n apply: () => undefined\n}\n\nconst getModel: RouteModel = {\n consistent: (state, request, fixture) => {\n const bucket = bucketOf(state, request)\n\n if (bucket === undefined) return false\n\n const object = objectIn(bucket, request)\n const expected = stringField(request, 'expectedEtag')\n const maxBytes = numberField(request, 'maxBytes')\n const size = byteLength(object?.bodyBase64)\n const etagHolds = object !== undefined && (expected === undefined || expected === object.etag)\n\n if (fixture.failure !== undefined) {\n switch (fixture.failure.code) {\n case 'not_found':\n return object === undefined\n case 'conflict':\n return object !== undefined && expected !== undefined && expected !== object.etag\n case 'response_too_large':\n return etagHolds && size !== undefined && maxBytes !== undefined && size > maxBytes\n default:\n // No other get failure is recorded with a state it follows from.\n return false\n }\n }\n\n return (\n object !== undefined &&\n etagHolds &&\n size !== undefined &&\n maxBytes !== undefined &&\n size <= maxBytes &&\n Equal.equals(fixture.response, {\n etag: object.etag,\n size,\n bodyBase64: object.bodyBase64\n })\n )\n },\n apply: () => undefined\n}\n\nconst putModel: RouteModel = {\n consistent: (state, request, fixture) => {\n const bucket = bucketOf(state, request)\n\n if (bucket === undefined) return false\n\n const object = objectIn(bucket, request)\n const condition = objectField(request, 'condition')\n const kind = stringField(condition, 'kind')\n const conditionEtag = stringField(condition, 'etag')\n\n if (fixture.failure !== undefined) {\n if (fixture.failure.code !== 'conflict') return false\n\n return kind === 'absent'\n ? object !== undefined\n : kind === 'etag' && object !== undefined && object.etag !== conditionEtag\n }\n\n const size = byteLength(stringField(request, 'bodyBase64'))\n const maxUploadBytes = numberField(request, 'maxUploadBytes')\n const etag = stringField(fixture.response, 'etag')\n\n const conditionHolds =\n kind === 'absent'\n ? object === undefined\n : kind === 'etag' &&\n object !== undefined &&\n object.etag === conditionEtag &&\n etag !== object.etag\n\n return (\n conditionHolds &&\n etag !== undefined &&\n size !== undefined &&\n maxUploadBytes !== undefined &&\n size <= maxUploadBytes &&\n numberField(fixture.response, 'size') === size\n )\n },\n apply: (state, request, fixture) => {\n const bucket = bucketOf(state, request)\n const key = stringField(request, 'key')\n const bodyBase64 = stringField(request, 'bodyBase64')\n const etag = stringField(fixture.response, 'etag')\n\n if (\n fixture.failure !== undefined ||\n bucket === undefined ||\n key === undefined ||\n bodyBase64 === undefined ||\n etag === undefined\n ) {\n return\n }\n\n const object = objectIn(bucket, request)\n\n if (object === undefined) {\n bucket.objects.push({ key, etag, bodyBase64 })\n } else {\n object.etag = etag\n object.bodyBase64 = bodyBase64\n }\n }\n}\n\nconst routePath = (port: string, method: string) => `${port}.${method}`\n\nconst routeModels: ReadonlyMap<string, RouteModel> = new Map([\n [\n emulatorRouteKey(r2EmulatorRouteMethod, routePath(r2EmulatorPresignerPort, 'presignPutObject')),\n presignModel\n ],\n [emulatorRouteKey(r2EmulatorRouteMethod, routePath(r2EmulatorObjectClientPort, 'get')), getModel],\n [emulatorRouteKey(r2EmulatorRouteMethod, routePath(r2EmulatorObjectClientPort, 'put')), putModel]\n])\n\nconst strict = { onExcessProperty: 'error' } as const\n\nconst decodeSeed = Schema.decodeUnknownResult(R2EmulatorSeed, strict)\n\nconst decodeFault = Schema.decodeUnknownResult(R2EmulatorFault, strict)\n\nconst issueText = (error: Schema.SchemaError): string => error.message\n\n/** Bucket names, and object keys within a bucket, must be unique so fixtures address one object. */\nconst seedProblem = (seed: R2EmulatorSeed): string | undefined => {\n const names = seed.buckets.map(bucket => bucket.name)\n\n if (new Set(names).size !== names.length) {\n return 'bucket names must be unique'\n }\n\n for (const bucket of seed.buckets) {\n const keys = bucket.objects.map(object => object.key)\n\n if (new Set(keys).size !== keys.length) {\n return `object keys in ${bucket.name} must be unique`\n }\n }\n\n return undefined\n}\n\n/**\n * Create an R2 emulator. Each call has its own buckets, ledger, faults, and fixture use.\n *\n * `call(port, method, request)` answers in this order: a port and method outside the manifest\n * (`unknown-method`), a non-object request or one with an own `__proto__` key (`invalid-request`),\n * a `bodyBase64` that is not canonical base64 of UTF-8 text (`uncheckable-body`), or a credential\n * outside the credential fields (`credential-in-request`) fails closed; otherwise the fixtures\n * whose port, method, and credential-free request match are checked against the buckets, and the\n * first consistent one (preferring one not used since the last reset) is chosen; no match\n * (`no-matching-fixture`) or no consistent match (`state-conflict`) fails closed. Only then does\n * the first active matching fault answer its failure (changing nothing); otherwise the chosen\n * fixture answers and updates the buckets. Every refusal is ledgered with constant text only; a\n * request the checks cannot walk is refused as `uncheckable-request`, so `call` never throws.\n *\n * Throws `R2EmulatorInputInvalid` for an invalid seed.\n */\nexport const makeR2Emulator = (options: R2EmulatorOptions = {}): R2Emulator => {\n const seedResult = decodeSeed(options.seed ?? r2EmulatorDefaultSeed)\n\n if (Result.isFailure(seedResult)) {\n throw new R2EmulatorInputInvalid({ input: 'seed', reason: issueText(seedResult.failure) })\n }\n\n const seed = seedResult.success\n const problem = seedProblem(seed)\n\n if (problem !== undefined) {\n throw new R2EmulatorInputInvalid({ input: 'seed', reason: problem })\n }\n\n const bound = new Map(\n bindRouteHandlers(r2EmulatorRoutes, routeModels).map(({ route, handler }) => [\n route.path,\n { route, model: handler }\n ])\n )\n\n let state = bucketsFrom(seed)\n let entries: Array<R2EmulatorLedgerEntry> = []\n\n let faults: Array<{\n id: number\n fault: R2EmulatorFault\n remaining: number | undefined\n applied: number\n }> = []\n\n let nextFaultId = 1\n let nextSeq = 1\n const used = new Set<string>()\n\n const record = (entry: Omit<R2EmulatorLedgerEntry, 'seq'>) => {\n entries.push({ seq: nextSeq, ...entry })\n nextSeq += 1\n }\n\n const refuse = (\n entry: Pick<R2EmulatorLedgerEntry, 'port' | 'method' | 'request' | 'evidence'>,\n reason: R2EmulatorNotEmulatedReason\n ): R2EmulatorReply => {\n record({ ...entry, outcome: 'not-emulated', reason })\n\n return { notEmulated: { reason: notEmulatedText[reason] } }\n }\n\n const answer = (port: string, method: string, rawRequest: Schema.Json): R2EmulatorReply => {\n const target = bound.get(routePath(port, method))\n\n if (target === undefined) {\n // Nothing from an unrecognised call is recorded: its port, method, or request may hold\n // anything.\n return refuse(\n {\n port: r2EmulatorUnrecognised,\n method: r2EmulatorUnrecognised,\n request: r2EmulatorRedacted,\n evidence: 'unknown-method'\n },\n 'unknown-method'\n )\n }\n\n const evidence = target.route.evidence\n\n // Every refusal below records constant text only: no request text reaches the ledger unless\n // the request equals a fixture request (minus credential fields).\n const constant = { port, method, request: r2EmulatorRedacted, evidence }\n\n if (!isJsonObject(rawRequest) || hasOwnProtoKey(rawRequest)) {\n return refuse(constant, 'invalid-request')\n }\n\n const request = withoutCredentials(rawRequest)\n\n // Bytes are checked as the text they decode to; anything else cannot be checked.\n const bodies = bodyValues(request).map(bodyText)\n const decodedBodies = bodies.flatMap(body => (body === undefined ? [] : [body]))\n\n if (decodedBodies.length !== bodies.length) {\n return refuse(constant, 'uncheckable-body')\n }\n\n const guarded = guardedValues(rawRequest)\n\n if (\n [...textsOf(request), ...decodedBodies].some(text => textCarriesCredential(text, guarded))\n ) {\n return refuse(constant, 'credential-in-request')\n }\n\n const matching = r2EmulatorFixtures.filter(\n fixture =>\n fixture.port === port &&\n fixture.method === method &&\n isJsonObject(fixture.request) &&\n Equal.equals(withoutCredentials(fixture.request), request)\n )\n\n if (matching.length === 0) {\n return refuse(constant, 'no-matching-fixture')\n }\n\n const consistent = matching.filter(fixture => target.model.consistent(state, request, fixture))\n const chosen = consistent.find(fixture => !used.has(fixture.id)) ?? consistent[0]\n\n if (chosen === undefined) {\n return refuse(constant, 'state-conflict')\n }\n\n // From here on the request equals a fixture request: what is recorded is fixture text, with\n // the body only as its decoded length.\n const bodyBytes = byteLength(stringField(request, bodyField))\n const ledgerRequest = withoutBodies(request)\n\n const recorded = (entry: Pick<R2EmulatorLedgerEntry, 'outcome' | 'fixtureId' | 'faultId'>) => {\n const base = { port, method, request: ledgerRequest, evidence, ...entry }\n\n record(bodyBytes === undefined ? base : { ...base, bodyBytes })\n }\n\n // Faults apply only to a call a fixture would answer: a refused call above leaves every fault\n // untouched. A fault changes no bucket state.\n const fault = faults.find(\n candidate =>\n candidate.fault.port === port &&\n candidate.fault.method === method &&\n (candidate.remaining === undefined || candidate.remaining > 0) &&\n (candidate.fault.match === undefined || containsSubset(request, candidate.fault.match))\n )\n\n if (fault !== undefined) {\n fault.applied += 1\n\n if (fault.remaining !== undefined) {\n fault.remaining -= 1\n }\n\n recorded({ outcome: 'fault', faultId: fault.id })\n\n return { failure: fault.fault.failure }\n }\n\n target.model.apply(state, request, chosen)\n used.add(chosen.id)\n recorded({ outcome: 'answered', fixtureId: chosen.id })\n\n return chosen.failure === undefined\n ? { response: chosen.response }\n : { failure: chosen.failure }\n }\n\n /**\n * `answer`, never throwing: a request the checks cannot walk (a cyclic or too deeply nested\n * value) is ledgered with constant text and answered not-emulated. Every check runs before any\n * fault is used or any state changes, so a throw leaves both untouched.\n */\n const call = (port: string, method: string, rawRequest: Schema.Json): R2EmulatorReply => {\n try {\n return answer(port, method, rawRequest)\n } catch {\n return refuse(\n {\n port: r2EmulatorUnrecognised,\n method: r2EmulatorUnrecognised,\n request: r2EmulatorRedacted,\n evidence: 'unknown-method'\n },\n 'uncheckable-request'\n )\n }\n }\n\n const faultState = (fault: (typeof faults)[number]): R2EmulatorFaultState => ({\n id: fault.id,\n fault: fault.fault,\n remaining: fault.remaining,\n applied: fault.applied\n })\n\n return {\n call,\n state: () => snapshotOf(state),\n seed: () => snapshotOf(bucketsFrom(seed)),\n ledger: {\n entries: () => entries.map(entry => ({ ...entry })),\n clear: () => {\n entries = []\n }\n },\n reset: () => {\n state = bucketsFrom(seed)\n entries = []\n faults = []\n used.clear()\n },\n faults: {\n add: input => {\n const decoded = decodeFault(input)\n\n if (Result.isFailure(decoded)) {\n throw new R2EmulatorInputInvalid({ input: 'fault', reason: issueText(decoded.failure) })\n }\n\n if (!bound.has(routePath(decoded.success.port, decoded.success.method))) {\n throw new R2EmulatorInputInvalid({\n input: 'fault',\n reason: 'the port and method have no emulated route'\n })\n }\n\n const added = {\n id: nextFaultId,\n fault: decoded.success,\n remaining: decoded.success.count,\n applied: 0\n }\n\n nextFaultId += 1\n faults.push(added)\n\n return faultState(added)\n },\n list: () => faults.map(faultState),\n clear: () => {\n faults = []\n }\n },\n coverage: () => ({\n routes: r2EmulatorRoutes.map(route => ({\n ...route,\n calls: entries.filter(entry => routePath(entry.port, entry.method) === route.path).length\n })),\n notEmulatedCalls: entries.filter(entry => entry.outcome === 'not-emulated').length,\n unusedFixtureIds: r2EmulatorFixtures.flatMap(fixture =>\n used.has(fixture.id) ? [] : [fixture.id]\n )\n })\n }\n}\n"],"mappings":"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"}
|