@hop-top/xrr 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CODE_OF_CONDUCT.md +12 -0
  2. package/CONTRIBUTING.md +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +22 -0
  5. package/SECURITY.md +13 -0
  6. package/dist/src/adapters/exec.d.ts +21 -0
  7. package/dist/src/adapters/exec.d.ts.map +1 -0
  8. package/dist/src/adapters/exec.js +29 -0
  9. package/dist/src/adapters/exec.js.map +1 -0
  10. package/dist/src/adapters/fs.d.ts +28 -0
  11. package/dist/src/adapters/fs.d.ts.map +1 -0
  12. package/dist/src/adapters/fs.js +76 -0
  13. package/dist/src/adapters/fs.js.map +1 -0
  14. package/dist/src/adapters/http.d.ts +21 -0
  15. package/dist/src/adapters/http.d.ts.map +1 -0
  16. package/dist/src/adapters/http.js +35 -0
  17. package/dist/src/adapters/http.js.map +1 -0
  18. package/dist/src/adapters/redis.d.ts +17 -0
  19. package/dist/src/adapters/redis.d.ts.map +1 -0
  20. package/dist/src/adapters/redis.js +25 -0
  21. package/dist/src/adapters/redis.js.map +1 -0
  22. package/dist/src/adapters/sql.d.ts +18 -0
  23. package/dist/src/adapters/sql.d.ts.map +1 -0
  24. package/dist/src/adapters/sql.js +32 -0
  25. package/dist/src/adapters/sql.js.map +1 -0
  26. package/dist/src/cassette.d.ts +13 -0
  27. package/dist/src/cassette.d.ts.map +1 -0
  28. package/dist/src/cassette.js +54 -0
  29. package/dist/src/cassette.js.map +1 -0
  30. package/dist/src/index.d.ts +18 -0
  31. package/dist/src/index.d.ts.map +1 -0
  32. package/dist/src/index.js +12 -0
  33. package/dist/src/index.js.map +1 -0
  34. package/dist/src/session.d.ts +13 -0
  35. package/dist/src/session.d.ts.map +1 -0
  36. package/dist/src/session.js +46 -0
  37. package/dist/src/session.js.map +1 -0
  38. package/dist/src/xrr.d.ts +24 -0
  39. package/dist/src/xrr.d.ts.map +1 -0
  40. package/dist/src/xrr.js +5 -0
  41. package/dist/src/xrr.js.map +1 -0
  42. package/package.json +34 -0
  43. package/src/adapters/exec.ts +53 -0
  44. package/src/adapters/fs.ts +115 -0
  45. package/src/adapters/http.ts +59 -0
  46. package/src/adapters/redis.ts +40 -0
  47. package/src/adapters/sql.ts +54 -0
  48. package/src/cassette.ts +80 -0
  49. package/src/index.ts +17 -0
  50. package/src/session.ts +61 -0
  51. package/src/xrr.ts +29 -0
@@ -0,0 +1,12 @@
1
+ # Code of Conduct
2
+
3
+ xrr follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
4
+
5
+ ## Summary
6
+
7
+ - Be respectful and inclusive.
8
+ - No harassment, discrimination, or personal attacks.
9
+ - Critique ideas, not people.
10
+ - Report issues: me@jadb.io
11
+
12
+ Full text: https://www.contributor-covenant.org/version/2/1/code_of_conduct/
@@ -0,0 +1,30 @@
1
+ # Contributing to xrr
2
+
3
+ ## Quick start
4
+
5
+ ```
6
+ git clone ...
7
+ task test # all 5 languages
8
+ task lint # all 5 linters
9
+ ```
10
+
11
+ ## Adding an adapter
12
+
13
+ 1. Implement `Adapter` interface in target language (4 methods: `id`, `fingerprint`,
14
+ `serialize`, `deserialize`) — use `go/adapters/exec/` as reference.
15
+ 2. Add conformance fixture: `spec/fixtures/<adapter>-happy/` with `manifest.yaml`,
16
+ `<adapter>-<fp>.req.yaml`, `<adapter>-<fp>.resp.yaml`.
17
+ 3. Run conformance in all ports — every port must pass new fixture without code change.
18
+ 4. Open PR; link to `spec/cassette-format-v1.md` and the relevant interface docs.
19
+
20
+ ## Porting to a new language
21
+
22
+ See the [Porting Guide](README.md#porting-guide) in the root README.
23
+
24
+ ## Commit style
25
+
26
+ Conventional Commits: `feat|fix|refactor|build|ci|chore|docs|style|perf|test`.
27
+
28
+ ## Code of Conduct
29
+
30
+ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IdeaCrafters Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # xrr — TypeScript SDK
2
+
3
+ > Auto-published from [poly-xrr](https://github.com/hop-top/poly-xrr).
4
+ > Do not open issues or PRs here — contribute to poly-xrr instead.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @hop-top/xrr
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```ts
15
+ const sess = new Session({ cassette: "fixtures/my-test" });
16
+ const resp = await sess.record("http-get-users", adapter);
17
+ sess.close();
18
+ ```
19
+
20
+ ## License
21
+
22
+ MIT — see [LICENSE](LICENSE)
package/SECURITY.md ADDED
@@ -0,0 +1,13 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ | Version | Supported |
6
+ |---------|-----------|
7
+ | latest | ✓ |
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ Email me@jadb.io. Do not open a public issue for security bugs.
12
+
13
+ Response within 48 hours. Fix target: 14 days for critical, 90 days for others.
@@ -0,0 +1,21 @@
1
+ import type { Adapter } from "../xrr.js";
2
+ export interface ExecRequest {
3
+ argv: string[];
4
+ stdin?: string;
5
+ env?: Record<string, string>;
6
+ }
7
+ export interface ExecResponse {
8
+ stdout: string;
9
+ stderr?: string;
10
+ exit_code: number;
11
+ duration_ms?: number;
12
+ }
13
+ export declare class ExecAdapter implements Adapter<ExecRequest, ExecResponse> {
14
+ readonly id = "exec";
15
+ fingerprint(req: ExecRequest): Promise<string>;
16
+ serializeReq(req: ExecRequest): unknown;
17
+ serializeResp(resp: ExecResponse): unknown;
18
+ deserializeReq(data: unknown): ExecRequest;
19
+ deserializeResp(data: unknown): ExecResponse;
20
+ }
21
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../../src/adapters/exec.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAUD,qBAAa,WAAY,YAAW,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC;IACpE,QAAQ,CAAC,EAAE,UAAU;IAEf,WAAW,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAOpD,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO;IAIvC,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO;IAI1C,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW;IAI1C,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY;CAG7C"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * exec adapter — fingerprints on argv + stdin.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ function sortedKeys(obj) {
6
+ return Object.fromEntries(Object.keys(obj)
7
+ .sort()
8
+ .map((k) => [k, obj[k]]));
9
+ }
10
+ export class ExecAdapter {
11
+ id = "exec";
12
+ async fingerprint(req) {
13
+ const canonical = JSON.stringify(sortedKeys({ argv: req.argv, stdin: req.stdin ?? "" }));
14
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
15
+ }
16
+ serializeReq(req) {
17
+ return req;
18
+ }
19
+ serializeResp(resp) {
20
+ return resp;
21
+ }
22
+ deserializeReq(data) {
23
+ return data;
24
+ }
25
+ deserializeResp(data) {
26
+ return data;
27
+ }
28
+ }
29
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../../src/adapters/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAgBzC,SAAS,UAAU,CAAC,GAA4B;IAC9C,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;SACb,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,WAAW;IACb,EAAE,GAAG,MAAM,CAAC;IAErB,KAAK,CAAC,WAAW,CAAC,GAAgB;QAChC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAC9B,UAAU,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC,CACvD,CAAC;QACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,YAAY,CAAC,GAAgB;QAC3B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,aAAa,CAAC,IAAkB;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,cAAc,CAAC,IAAa;QAC1B,OAAO,IAAmB,CAAC;IAC7B,CAAC;IAED,eAAe,CAAC,IAAa;QAC3B,OAAO,IAAoB,CAAC;IAC9B,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ import type { Adapter } from "../xrr.js";
2
+ export type FsOp = "write" | "mkdir" | "remove" | "rename" | "chmod" | "chown" | "symlink" | "hardlink" | "truncate";
3
+ export interface FsRequest {
4
+ op: FsOp;
5
+ path: string;
6
+ /** UTF-8 string per spec. Binary callers MUST base64-encode beforehand. */
7
+ data?: string;
8
+ mode?: number;
9
+ uid?: number;
10
+ gid?: number;
11
+ dest?: string;
12
+ size?: number;
13
+ flags?: number;
14
+ recursive?: boolean;
15
+ }
16
+ export interface FsResponse {
17
+ duration_ms?: number;
18
+ bytes_written?: number;
19
+ }
20
+ export declare class FsAdapter implements Adapter<FsRequest, FsResponse> {
21
+ readonly id = "fs";
22
+ fingerprint(req: FsRequest): Promise<string>;
23
+ serializeReq(req: FsRequest): unknown;
24
+ serializeResp(resp: FsResponse): unknown;
25
+ deserializeReq(data: unknown): FsRequest;
26
+ deserializeResp(data: unknown): FsResponse;
27
+ }
28
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../../src/adapters/fs.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,MAAM,IAAI,GACZ,OAAO,GACP,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,OAAO,GACP,SAAS,GACT,UAAU,GACV,UAAU,CAAC;AAEf,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAUD,qBAAa,SAAU,YAAW,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9D,QAAQ,CAAC,EAAE,QAAQ;IAEb,WAAW,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAmClD,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO;IAIrC,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO;IAIxC,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS;IAIxC,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU;CAG3C"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * fs adapter — fingerprints on op + path + presence-gated optional fields.
3
+ *
4
+ * Mirrors go/adapters/fs/fs.go. The fingerprint algorithm hashes a
5
+ * canonical JSON object (sorted keys) containing `op`, `path`, and
6
+ * each optional field only when "set" per the omit-on-zero rules
7
+ * documented in spec/cassette-format-v1.md.
8
+ *
9
+ * Field naming uses snake_case in the TS interface to match the
10
+ * YAML wire form used by every other port — js-yaml round-trips
11
+ * the field names verbatim, so what you see in code is what you
12
+ * see in the cassette.
13
+ *
14
+ * Path normalization: the TS adapter accepts paths verbatim. The
15
+ * Go adapter ships a PathNormalizer hook because Go is the typical
16
+ * record-side runtime; on replay-side (where TS is most useful),
17
+ * paths arrive already-normalized from the cassette. Adopters that
18
+ * need TS-side recording with normalization can pre-normalize at
19
+ * the caller layer before constructing the request.
20
+ */
21
+ import { createHash } from "node:crypto";
22
+ function sortedKeys(obj) {
23
+ return Object.fromEntries(Object.keys(obj)
24
+ .sort()
25
+ .map((k) => [k, obj[k]]));
26
+ }
27
+ export class FsAdapter {
28
+ id = "fs";
29
+ async fingerprint(req) {
30
+ const fields = {
31
+ op: req.op,
32
+ path: req.path,
33
+ };
34
+ if (req.data !== undefined && req.data !== "") {
35
+ fields.data_sha256 = createHash("sha256")
36
+ .update(req.data, "utf8")
37
+ .digest("hex");
38
+ }
39
+ if (req.mode !== undefined) {
40
+ fields.mode = req.mode;
41
+ }
42
+ if (req.uid !== undefined) {
43
+ fields.uid = req.uid;
44
+ }
45
+ if (req.gid !== undefined) {
46
+ fields.gid = req.gid;
47
+ }
48
+ if (req.dest !== undefined && req.dest !== "") {
49
+ fields.dest = req.dest;
50
+ }
51
+ if (req.size !== undefined) {
52
+ fields.size = req.size;
53
+ }
54
+ if (req.flags !== undefined && req.flags !== 0) {
55
+ fields.flags = req.flags;
56
+ }
57
+ if (req.recursive === true) {
58
+ fields.recursive = true;
59
+ }
60
+ const canonical = JSON.stringify(sortedKeys(fields));
61
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
62
+ }
63
+ serializeReq(req) {
64
+ return req;
65
+ }
66
+ serializeResp(resp) {
67
+ return resp;
68
+ }
69
+ deserializeReq(data) {
70
+ return data;
71
+ }
72
+ deserializeResp(data) {
73
+ return data;
74
+ }
75
+ }
76
+ //# sourceMappingURL=fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../../src/adapters/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAiCzC,SAAS,UAAU,CAAC,GAA4B;IAC9C,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;SACb,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,SAAS;IACX,EAAE,GAAG,IAAI,CAAC;IAEnB,KAAK,CAAC,WAAW,CAAC,GAAc;QAC9B,MAAM,MAAM,GAA4B;YACtC,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,IAAI,EAAE,GAAG,CAAC,IAAI;SACf,CAAC;QACF,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;YAC9C,MAAM,CAAC,WAAW,GAAG,UAAU,CAAC,QAAQ,CAAC;iBACtC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC;iBACxB,MAAM,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACzB,CAAC;QACD,IAAI,GAAG,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;QACvB,CAAC;QACD,IAAI,GAAG,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;QACvB,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;YAC9C,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACzB,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACzB,CAAC;QACD,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;QAC3B,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YAC3B,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;QAC1B,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QACrD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,YAAY,CAAC,GAAc;QACzB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,aAAa,CAAC,IAAgB;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,cAAc,CAAC,IAAa;QAC1B,OAAO,IAAiB,CAAC;IAC3B,CAAC;IAED,eAAe,CAAC,IAAa;QAC3B,OAAO,IAAkB,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,21 @@
1
+ import type { Adapter } from "../xrr.js";
2
+ export interface HttpRequest {
3
+ method: string;
4
+ url: string;
5
+ headers?: Record<string, string>;
6
+ body?: string;
7
+ }
8
+ export interface HttpResponse {
9
+ status: number;
10
+ headers?: Record<string, string>;
11
+ body?: string;
12
+ }
13
+ export declare class HttpAdapter implements Adapter<HttpRequest, HttpResponse> {
14
+ readonly id = "http";
15
+ fingerprint(req: HttpRequest): Promise<string>;
16
+ serializeReq(req: HttpRequest): unknown;
17
+ serializeResp(resp: HttpResponse): unknown;
18
+ deserializeReq(data: unknown): HttpRequest;
19
+ deserializeResp(data: unknown): HttpResponse;
20
+ }
21
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/adapters/http.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAUD,qBAAa,WAAY,YAAW,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC;IACpE,QAAQ,CAAC,EAAE,UAAU;IAEf,WAAW,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAapD,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO;IAIvC,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO;IAI1C,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW;IAI1C,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY;CAG7C"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * http adapter — fingerprints on method + path+query + sha256(body)[:8].
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ function sortedKeys(obj) {
6
+ return Object.fromEntries(Object.keys(obj)
7
+ .sort()
8
+ .map((k) => [k, obj[k]]));
9
+ }
10
+ export class HttpAdapter {
11
+ id = "http";
12
+ async fingerprint(req) {
13
+ const u = new URL(req.url);
14
+ const pathQuery = u.search ? `${u.pathname}${u.search}` : u.pathname;
15
+ const bodyHash = createHash("sha256")
16
+ .update(req.body ?? "")
17
+ .digest("hex")
18
+ .slice(0, 8);
19
+ const canonical = JSON.stringify(sortedKeys({ body_hash: bodyHash, method: req.method, path: pathQuery }));
20
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
21
+ }
22
+ serializeReq(req) {
23
+ return req;
24
+ }
25
+ serializeResp(resp) {
26
+ return resp;
27
+ }
28
+ deserializeReq(data) {
29
+ return data;
30
+ }
31
+ deserializeResp(data) {
32
+ return data;
33
+ }
34
+ }
35
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/adapters/http.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAgBzC,SAAS,UAAU,CAAC,GAA4B;IAC9C,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;SACb,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,WAAW;IACb,EAAE,GAAG,MAAM,CAAC;IAErB,KAAK,CAAC,WAAW,CAAC,GAAgB;QAChC,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACrE,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;aAClC,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;aACtB,MAAM,CAAC,KAAK,CAAC;aACb,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACf,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAC9B,UAAU,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CACzE,CAAC;QACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,YAAY,CAAC,GAAgB;QAC3B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,aAAa,CAAC,IAAkB;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,cAAc,CAAC,IAAa;QAC1B,OAAO,IAAmB,CAAC;IAC7B,CAAC;IAED,eAAe,CAAC,IAAa;QAC3B,OAAO,IAAoB,CAAC;IAC9B,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ import type { Adapter } from "../xrr.js";
2
+ export interface RedisRequest {
3
+ command: string;
4
+ args?: string[];
5
+ }
6
+ export interface RedisResponse {
7
+ result: unknown;
8
+ }
9
+ export declare class RedisAdapter implements Adapter<RedisRequest, RedisResponse> {
10
+ readonly id = "redis";
11
+ fingerprint(req: RedisRequest): Promise<string>;
12
+ serializeReq(req: RedisRequest): unknown;
13
+ serializeResp(resp: RedisResponse): unknown;
14
+ deserializeReq(data: unknown): RedisRequest;
15
+ deserializeResp(data: unknown): RedisResponse;
16
+ }
17
+ //# sourceMappingURL=redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../../src/adapters/redis.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,YAAa,YAAW,OAAO,CAAC,YAAY,EAAE,aAAa,CAAC;IACvE,QAAQ,CAAC,EAAE,WAAW;IAEhB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAMrD,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO;IAIxC,aAAa,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO;IAI3C,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY;IAI3C,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa;CAG9C"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * redis adapter — fingerprints on command + args joined.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ export class RedisAdapter {
6
+ id = "redis";
7
+ async fingerprint(req) {
8
+ const parts = [req.command.toUpperCase(), ...(req.args ?? [])];
9
+ const canonical = JSON.stringify(parts.join(" "));
10
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
11
+ }
12
+ serializeReq(req) {
13
+ return req;
14
+ }
15
+ serializeResp(resp) {
16
+ return resp;
17
+ }
18
+ deserializeReq(data) {
19
+ return data;
20
+ }
21
+ deserializeResp(data) {
22
+ return data;
23
+ }
24
+ }
25
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.js","sourceRoot":"","sources":["../../../src/adapters/redis.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAYzC,MAAM,OAAO,YAAY;IACd,EAAE,GAAG,OAAO,CAAC;IAEtB,KAAK,CAAC,WAAW,CAAC,GAAiB;QACjC,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;QAC/D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAClD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,YAAY,CAAC,GAAiB;QAC5B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,aAAa,CAAC,IAAmB;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,cAAc,CAAC,IAAa;QAC1B,OAAO,IAAoB,CAAC;IAC9B,CAAC;IAED,eAAe,CAAC,IAAa;QAC3B,OAAO,IAAqB,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ import type { Adapter } from "../xrr.js";
2
+ export interface SqlRequest {
3
+ query: string;
4
+ args?: unknown[];
5
+ }
6
+ export interface SqlResponse {
7
+ rows?: Record<string, unknown>[];
8
+ affected?: number;
9
+ }
10
+ export declare class SqlAdapter implements Adapter<SqlRequest, SqlResponse> {
11
+ readonly id = "sql";
12
+ fingerprint(req: SqlRequest): Promise<string>;
13
+ serializeReq(req: SqlRequest): unknown;
14
+ serializeResp(resp: SqlResponse): unknown;
15
+ deserializeReq(data: unknown): SqlRequest;
16
+ deserializeResp(data: unknown): SqlResponse;
17
+ }
18
+ //# sourceMappingURL=sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../src/adapters/sql.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAcD,qBAAa,UAAW,YAAW,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC;IACjE,QAAQ,CAAC,EAAE,SAAS;IAEd,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAOnD,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO;IAItC,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO;IAIzC,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU;IAIzC,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW;CAG5C"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * sql adapter — fingerprints on normalized query + args.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ function normalizeQuery(q) {
6
+ return q.toLowerCase().replace(/\s+/g, " ").trim();
7
+ }
8
+ function sortedKeys(obj) {
9
+ return Object.fromEntries(Object.keys(obj)
10
+ .sort()
11
+ .map((k) => [k, obj[k]]));
12
+ }
13
+ export class SqlAdapter {
14
+ id = "sql";
15
+ async fingerprint(req) {
16
+ const canonical = JSON.stringify(sortedKeys({ args: req.args ?? null, query: normalizeQuery(req.query) }));
17
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
18
+ }
19
+ serializeReq(req) {
20
+ return req;
21
+ }
22
+ serializeResp(resp) {
23
+ return resp;
24
+ }
25
+ deserializeReq(data) {
26
+ return data;
27
+ }
28
+ deserializeResp(data) {
29
+ return data;
30
+ }
31
+ }
32
+ //# sourceMappingURL=sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.js","sourceRoot":"","sources":["../../../src/adapters/sql.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAazC,SAAS,cAAc,CAAC,CAAS;IAC/B,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACrD,CAAC;AAED,SAAS,UAAU,CAAC,GAA4B;IAC9C,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;SACb,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,UAAU;IACZ,EAAE,GAAG,KAAK,CAAC;IAEpB,KAAK,CAAC,WAAW,CAAC,GAAe;QAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAC9B,UAAU,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CACzE,CAAC;QACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,YAAY,CAAC,GAAe;QAC1B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,aAAa,CAAC,IAAiB;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,cAAc,CAAC,IAAa;QAC1B,OAAO,IAAkB,CAAC;IAC5B,CAAC;IAED,eAAe,CAAC,IAAa;QAC3B,OAAO,IAAmB,CAAC;IAC7B,CAAC;CACF"}
@@ -0,0 +1,13 @@
1
+ import { type Cassette } from "./xrr.js";
2
+ export declare class FileCassette implements Cassette {
3
+ private readonly dir;
4
+ constructor(dir: string);
5
+ save(adapterID: string, fingerprint: string, req: unknown, resp: unknown): Promise<void>;
6
+ private write;
7
+ load(adapterID: string, fingerprint: string): Promise<{
8
+ req: unknown;
9
+ resp: unknown;
10
+ }>;
11
+ private read;
12
+ }
13
+ //# sourceMappingURL=cassette.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cassette.d.ts","sourceRoot":"","sources":["../../src/cassette.ts"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAU1D,qBAAa,YAAa,YAAW,QAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM;IAElC,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC;YAMF,KAAK;IAmBb,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,GAAG,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;YAM7B,IAAI;CAqBnB"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * FileCassette — reads/writes YAML envelope files.
3
+ */
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import yaml from "js-yaml";
7
+ import { ErrCassetteMiss } from "./xrr.js";
8
+ export class FileCassette {
9
+ dir;
10
+ constructor(dir) {
11
+ this.dir = dir;
12
+ }
13
+ async save(adapterID, fingerprint, req, resp) {
14
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
15
+ await this.write(adapterID, fingerprint, "req", now, req);
16
+ await this.write(adapterID, fingerprint, "resp", now, resp);
17
+ }
18
+ async write(adapterID, fingerprint, kind, recordedAt, payload) {
19
+ const env = {
20
+ xrr: "1",
21
+ adapter: adapterID,
22
+ fingerprint,
23
+ recorded_at: recordedAt,
24
+ payload,
25
+ };
26
+ const data = yaml.dump(env, { lineWidth: -1 });
27
+ const filePath = path.join(this.dir, `${adapterID}-${fingerprint}.${kind}.yaml`);
28
+ await fs.writeFile(filePath, data, "utf8");
29
+ }
30
+ async load(adapterID, fingerprint) {
31
+ const req = await this.read(adapterID, fingerprint, "req");
32
+ const resp = await this.read(adapterID, fingerprint, "resp");
33
+ return { req, resp };
34
+ }
35
+ async read(adapterID, fingerprint, kind) {
36
+ const filePath = path.join(this.dir, `${adapterID}-${fingerprint}.${kind}.yaml`);
37
+ let data;
38
+ try {
39
+ data = await fs.readFile(filePath, "utf8");
40
+ }
41
+ catch (err) {
42
+ if (err.code === "ENOENT") {
43
+ throw ErrCassetteMiss;
44
+ }
45
+ throw err;
46
+ }
47
+ const env = yaml.load(data);
48
+ if (!env || typeof env !== "object" || !("payload" in env)) {
49
+ throw new Error(`xrr: missing payload in ${kind}`);
50
+ }
51
+ return env.payload;
52
+ }
53
+ }
54
+ //# sourceMappingURL=cassette.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cassette.js","sourceRoot":"","sources":["../../src/cassette.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAiB,MAAM,UAAU,CAAC;AAU1D,MAAM,OAAO,YAAY;IACM;IAA7B,YAA6B,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAE5C,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,WAAmB,EACnB,GAAY,EACZ,IAAa;QAEb,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QAC/D,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAC1D,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC9D,CAAC;IAEO,KAAK,CAAC,KAAK,CACjB,SAAiB,EACjB,WAAmB,EACnB,IAAoB,EACpB,UAAkB,EAClB,OAAgB;QAEhB,MAAM,GAAG,GAAa;YACpB,GAAG,EAAE,GAAG;YACR,OAAO,EAAE,SAAS;YAClB,WAAW;YACX,WAAW,EAAE,UAAU;YACvB,OAAO;SACR,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,IAAI,WAAW,IAAI,IAAI,OAAO,CAAC,CAAC;QACjF,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,WAAmB;QAEnB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QAC7D,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACvB,CAAC;IAEO,KAAK,CAAC,IAAI,CAChB,SAAiB,EACjB,WAAmB,EACnB,IAAoB;QAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,IAAI,WAAW,IAAI,IAAI,OAAO,CAAC,CAAC;QACjF,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrD,MAAM,eAAe,CAAC;YACxB,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAa,CAAC;QACxC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,EAAE,CAAC;YAC3D,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,GAAG,CAAC,OAAO,CAAC;IACrB,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Barrel export for @hop-top/xrr.
3
+ */
4
+ export { ErrCassetteMiss } from "./xrr.js";
5
+ export type { Adapter, Cassette, Mode, Session } from "./xrr.js";
6
+ export { FileCassette } from "./cassette.js";
7
+ export { FileSession } from "./session.js";
8
+ export { ExecAdapter } from "./adapters/exec.js";
9
+ export type { ExecRequest, ExecResponse } from "./adapters/exec.js";
10
+ export { HttpAdapter } from "./adapters/http.js";
11
+ export type { HttpRequest, HttpResponse } from "./adapters/http.js";
12
+ export { RedisAdapter } from "./adapters/redis.js";
13
+ export type { RedisRequest, RedisResponse } from "./adapters/redis.js";
14
+ export { SqlAdapter } from "./adapters/sql.js";
15
+ export type { SqlRequest, SqlResponse } from "./adapters/sql.js";
16
+ export { FsAdapter } from "./adapters/fs.js";
17
+ export type { FsOp, FsRequest, FsResponse } from "./adapters/fs.js";
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Barrel export for @hop-top/xrr.
3
+ */
4
+ export { ErrCassetteMiss } from "./xrr.js";
5
+ export { FileCassette } from "./cassette.js";
6
+ export { FileSession } from "./session.js";
7
+ export { ExecAdapter } from "./adapters/exec.js";
8
+ export { HttpAdapter } from "./adapters/http.js";
9
+ export { RedisAdapter } from "./adapters/redis.js";
10
+ export { SqlAdapter } from "./adapters/sql.js";
11
+ export { FsAdapter } from "./adapters/fs.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * FileSession — record/replay/passthrough session.
3
+ */
4
+ import { type Adapter, type Cassette, type Mode, type Session } from "./xrr.js";
5
+ export declare class FileSession implements Session {
6
+ private readonly mode;
7
+ private readonly cassette;
8
+ constructor(mode: Mode, cassette: Cassette);
9
+ record<Req, Resp>(adapter: Adapter<Req, Resp>, req: Req, do_: () => Promise<Resp>): Promise<Resp>;
10
+ private doRecord;
11
+ private doReplay;
12
+ }
13
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAmB,KAAK,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAEjG,qBAAa,WAAY,YAAW,OAAO;IAEvC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ;IAG/B,MAAM,CAAC,GAAG,EAAE,IAAI,EACpB,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,EAC3B,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACvB,OAAO,CAAC,IAAI,CAAC;YAeF,QAAQ;YAgBR,QAAQ;CAcvB"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * FileSession — record/replay/passthrough session.
3
+ */
4
+ import { ErrCassetteMiss } from "./xrr.js";
5
+ export class FileSession {
6
+ mode;
7
+ cassette;
8
+ constructor(mode, cassette) {
9
+ this.mode = mode;
10
+ this.cassette = cassette;
11
+ }
12
+ async record(adapter, req, do_) {
13
+ switch (this.mode) {
14
+ case "record":
15
+ return this.doRecord(adapter, req, do_);
16
+ case "replay":
17
+ return this.doReplay(adapter, req);
18
+ case "passthrough":
19
+ return do_();
20
+ default: {
21
+ const exhaustive = this.mode;
22
+ throw new Error(`xrr: unknown mode "${exhaustive}"`);
23
+ }
24
+ }
25
+ }
26
+ async doRecord(adapter, req, do_) {
27
+ const resp = await do_();
28
+ const fp = await adapter.fingerprint(req);
29
+ await this.cassette.save(adapter.id, fp, adapter.serializeReq(req), adapter.serializeResp(resp));
30
+ return resp;
31
+ }
32
+ async doReplay(adapter, req) {
33
+ const fp = await adapter.fingerprint(req);
34
+ let loaded;
35
+ try {
36
+ loaded = await this.cassette.load(adapter.id, fp);
37
+ }
38
+ catch (err) {
39
+ if (err === ErrCassetteMiss)
40
+ throw ErrCassetteMiss;
41
+ throw err;
42
+ }
43
+ return adapter.deserializeResp(loaded.resp);
44
+ }
45
+ }
46
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAA+B,eAAe,EAA2B,MAAM,UAAU,CAAC;AAEjG,MAAM,OAAO,WAAW;IAEH;IACA;IAFnB,YACmB,IAAU,EACV,QAAkB;QADlB,SAAI,GAAJ,IAAI,CAAM;QACV,aAAQ,GAAR,QAAQ,CAAU;IAClC,CAAC;IAEJ,KAAK,CAAC,MAAM,CACV,OAA2B,EAC3B,GAAQ,EACR,GAAwB;QAExB,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAClB,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YAC1C,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YACrC,KAAK,aAAa;gBAChB,OAAO,GAAG,EAAE,CAAC;YACf,OAAO,CAAC,CAAC,CAAC;gBACR,MAAM,UAAU,GAAU,IAAI,CAAC,IAAI,CAAC;gBACpC,MAAM,IAAI,KAAK,CAAC,sBAAsB,UAAU,GAAG,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,QAAQ,CACpB,OAA2B,EAC3B,GAAQ,EACR,GAAwB;QAExB,MAAM,IAAI,GAAG,MAAM,GAAG,EAAE,CAAC;QACzB,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CACtB,OAAO,CAAC,EAAE,EACV,EAAE,EACF,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,EACzB,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAC5B,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,KAAK,CAAC,QAAQ,CACpB,OAA2B,EAC3B,GAAQ;QAER,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,MAAuC,CAAC;QAC5C,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,KAAK,eAAe;gBAAE,MAAM,eAAe,CAAC;YACnD,MAAM,GAAG,CAAC;QACZ,CAAC;QACD,OAAO,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;CACF"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Core xrr interfaces — mirrors Go reference implementation.
3
+ */
4
+ export type Mode = "record" | "replay" | "passthrough";
5
+ export declare const ErrCassetteMiss: Error;
6
+ export interface Adapter<Req, Resp> {
7
+ id: string;
8
+ fingerprint(req: Req): Promise<string>;
9
+ serializeReq(req: Req): unknown;
10
+ serializeResp(resp: Resp): unknown;
11
+ deserializeReq(data: unknown): Req;
12
+ deserializeResp(data: unknown): Resp;
13
+ }
14
+ export interface Cassette {
15
+ save(adapterID: string, fingerprint: string, req: unknown, resp: unknown): Promise<void>;
16
+ load(adapterID: string, fingerprint: string): Promise<{
17
+ req: unknown;
18
+ resp: unknown;
19
+ }>;
20
+ }
21
+ export interface Session {
22
+ record<Req, Resp>(adapter: Adapter<Req, Resp>, req: Req, do_: () => Promise<Resp>): Promise<Resp>;
23
+ }
24
+ //# sourceMappingURL=xrr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xrr.d.ts","sourceRoot":"","sources":["../../src/xrr.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,IAAI,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEvD,eAAO,MAAM,eAAe,OAAkC,CAAC;AAE/D,MAAM,WAAW,OAAO,CAAC,GAAG,EAAE,IAAI;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC;IAChC,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC;IACnC,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,GAAG,CAAC;IACnC,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACxF;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,GAAG,EAAE,IAAI,EACd,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,EAC3B,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACvB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Core xrr interfaces — mirrors Go reference implementation.
3
+ */
4
+ export const ErrCassetteMiss = new Error("xrr: cassette miss");
5
+ //# sourceMappingURL=xrr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xrr.js","sourceRoot":"","sources":["../../src/xrr.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@hop-top/xrr",
3
+ "version": "0.1.0-alpha.4",
4
+ "type": "module",
5
+ "description": "Multi-channel interaction recorder/replayer",
6
+ "main": "./src/index.ts",
7
+ "devDependencies": {
8
+ "@eslint/js": "^10.0.1",
9
+ "@types/js-yaml": "^4.0.9",
10
+ "@types/node": "^22.0.0",
11
+ "eslint": "^10.1.0",
12
+ "typescript": "^5.7.0",
13
+ "typescript-eslint": "^8.58.0",
14
+ "vitest": "^3.0.0"
15
+ },
16
+ "dependencies": {
17
+ "js-yaml": "^4.1.0"
18
+ },
19
+ "files": [
20
+ "src/",
21
+ "dist/",
22
+ "LICENSE",
23
+ "CODE_OF_CONDUCT.md",
24
+ "CONTRIBUTING.md",
25
+ "SECURITY.md",
26
+ "README.md"
27
+ ],
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "test": "vitest run",
31
+ "lint": "eslint src tests",
32
+ "ci:build": "tsc -p tsconfig.build.json"
33
+ }
34
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * exec adapter — fingerprints on argv + stdin.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ import type { Adapter } from "../xrr.js";
6
+
7
+ export interface ExecRequest {
8
+ argv: string[];
9
+ stdin?: string;
10
+ env?: Record<string, string>;
11
+ }
12
+
13
+ export interface ExecResponse {
14
+ stdout: string;
15
+ stderr?: string;
16
+ exit_code: number;
17
+ duration_ms?: number;
18
+ }
19
+
20
+ function sortedKeys(obj: Record<string, unknown>): Record<string, unknown> {
21
+ return Object.fromEntries(
22
+ Object.keys(obj)
23
+ .sort()
24
+ .map((k) => [k, obj[k]])
25
+ );
26
+ }
27
+
28
+ export class ExecAdapter implements Adapter<ExecRequest, ExecResponse> {
29
+ readonly id = "exec";
30
+
31
+ async fingerprint(req: ExecRequest): Promise<string> {
32
+ const canonical = JSON.stringify(
33
+ sortedKeys({ argv: req.argv, stdin: req.stdin ?? "" })
34
+ );
35
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
36
+ }
37
+
38
+ serializeReq(req: ExecRequest): unknown {
39
+ return req;
40
+ }
41
+
42
+ serializeResp(resp: ExecResponse): unknown {
43
+ return resp;
44
+ }
45
+
46
+ deserializeReq(data: unknown): ExecRequest {
47
+ return data as ExecRequest;
48
+ }
49
+
50
+ deserializeResp(data: unknown): ExecResponse {
51
+ return data as ExecResponse;
52
+ }
53
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * fs adapter — fingerprints on op + path + presence-gated optional fields.
3
+ *
4
+ * Mirrors go/adapters/fs/fs.go. The fingerprint algorithm hashes a
5
+ * canonical JSON object (sorted keys) containing `op`, `path`, and
6
+ * each optional field only when "set" per the omit-on-zero rules
7
+ * documented in spec/cassette-format-v1.md.
8
+ *
9
+ * Field naming uses snake_case in the TS interface to match the
10
+ * YAML wire form used by every other port — js-yaml round-trips
11
+ * the field names verbatim, so what you see in code is what you
12
+ * see in the cassette.
13
+ *
14
+ * Path normalization: the TS adapter accepts paths verbatim. The
15
+ * Go adapter ships a PathNormalizer hook because Go is the typical
16
+ * record-side runtime; on replay-side (where TS is most useful),
17
+ * paths arrive already-normalized from the cassette. Adopters that
18
+ * need TS-side recording with normalization can pre-normalize at
19
+ * the caller layer before constructing the request.
20
+ */
21
+ import { createHash } from "node:crypto";
22
+ import type { Adapter } from "../xrr.js";
23
+
24
+ export type FsOp =
25
+ | "write"
26
+ | "mkdir"
27
+ | "remove"
28
+ | "rename"
29
+ | "chmod"
30
+ | "chown"
31
+ | "symlink"
32
+ | "hardlink"
33
+ | "truncate";
34
+
35
+ export interface FsRequest {
36
+ op: FsOp;
37
+ path: string;
38
+ /** UTF-8 string per spec. Binary callers MUST base64-encode beforehand. */
39
+ data?: string;
40
+ mode?: number;
41
+ uid?: number;
42
+ gid?: number;
43
+ dest?: string;
44
+ size?: number;
45
+ flags?: number;
46
+ recursive?: boolean;
47
+ }
48
+
49
+ export interface FsResponse {
50
+ duration_ms?: number;
51
+ bytes_written?: number;
52
+ }
53
+
54
+ function sortedKeys(obj: Record<string, unknown>): Record<string, unknown> {
55
+ return Object.fromEntries(
56
+ Object.keys(obj)
57
+ .sort()
58
+ .map((k) => [k, obj[k]])
59
+ );
60
+ }
61
+
62
+ export class FsAdapter implements Adapter<FsRequest, FsResponse> {
63
+ readonly id = "fs";
64
+
65
+ async fingerprint(req: FsRequest): Promise<string> {
66
+ const fields: Record<string, unknown> = {
67
+ op: req.op,
68
+ path: req.path,
69
+ };
70
+ if (req.data !== undefined && req.data !== "") {
71
+ fields.data_sha256 = createHash("sha256")
72
+ .update(req.data, "utf8")
73
+ .digest("hex");
74
+ }
75
+ if (req.mode !== undefined) {
76
+ fields.mode = req.mode;
77
+ }
78
+ if (req.uid !== undefined) {
79
+ fields.uid = req.uid;
80
+ }
81
+ if (req.gid !== undefined) {
82
+ fields.gid = req.gid;
83
+ }
84
+ if (req.dest !== undefined && req.dest !== "") {
85
+ fields.dest = req.dest;
86
+ }
87
+ if (req.size !== undefined) {
88
+ fields.size = req.size;
89
+ }
90
+ if (req.flags !== undefined && req.flags !== 0) {
91
+ fields.flags = req.flags;
92
+ }
93
+ if (req.recursive === true) {
94
+ fields.recursive = true;
95
+ }
96
+ const canonical = JSON.stringify(sortedKeys(fields));
97
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
98
+ }
99
+
100
+ serializeReq(req: FsRequest): unknown {
101
+ return req;
102
+ }
103
+
104
+ serializeResp(resp: FsResponse): unknown {
105
+ return resp;
106
+ }
107
+
108
+ deserializeReq(data: unknown): FsRequest {
109
+ return data as FsRequest;
110
+ }
111
+
112
+ deserializeResp(data: unknown): FsResponse {
113
+ return data as FsResponse;
114
+ }
115
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * http adapter — fingerprints on method + path+query + sha256(body)[:8].
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ import type { Adapter } from "../xrr.js";
6
+
7
+ export interface HttpRequest {
8
+ method: string;
9
+ url: string;
10
+ headers?: Record<string, string>;
11
+ body?: string;
12
+ }
13
+
14
+ export interface HttpResponse {
15
+ status: number;
16
+ headers?: Record<string, string>;
17
+ body?: string;
18
+ }
19
+
20
+ function sortedKeys(obj: Record<string, unknown>): Record<string, unknown> {
21
+ return Object.fromEntries(
22
+ Object.keys(obj)
23
+ .sort()
24
+ .map((k) => [k, obj[k]])
25
+ );
26
+ }
27
+
28
+ export class HttpAdapter implements Adapter<HttpRequest, HttpResponse> {
29
+ readonly id = "http";
30
+
31
+ async fingerprint(req: HttpRequest): Promise<string> {
32
+ const u = new URL(req.url);
33
+ const pathQuery = u.search ? `${u.pathname}${u.search}` : u.pathname;
34
+ const bodyHash = createHash("sha256")
35
+ .update(req.body ?? "")
36
+ .digest("hex")
37
+ .slice(0, 8);
38
+ const canonical = JSON.stringify(
39
+ sortedKeys({ body_hash: bodyHash, method: req.method, path: pathQuery })
40
+ );
41
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
42
+ }
43
+
44
+ serializeReq(req: HttpRequest): unknown {
45
+ return req;
46
+ }
47
+
48
+ serializeResp(resp: HttpResponse): unknown {
49
+ return resp;
50
+ }
51
+
52
+ deserializeReq(data: unknown): HttpRequest {
53
+ return data as HttpRequest;
54
+ }
55
+
56
+ deserializeResp(data: unknown): HttpResponse {
57
+ return data as HttpResponse;
58
+ }
59
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * redis adapter — fingerprints on command + args joined.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ import type { Adapter } from "../xrr.js";
6
+
7
+ export interface RedisRequest {
8
+ command: string;
9
+ args?: string[];
10
+ }
11
+
12
+ export interface RedisResponse {
13
+ result: unknown;
14
+ }
15
+
16
+ export class RedisAdapter implements Adapter<RedisRequest, RedisResponse> {
17
+ readonly id = "redis";
18
+
19
+ async fingerprint(req: RedisRequest): Promise<string> {
20
+ const parts = [req.command.toUpperCase(), ...(req.args ?? [])];
21
+ const canonical = JSON.stringify(parts.join(" "));
22
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
23
+ }
24
+
25
+ serializeReq(req: RedisRequest): unknown {
26
+ return req;
27
+ }
28
+
29
+ serializeResp(resp: RedisResponse): unknown {
30
+ return resp;
31
+ }
32
+
33
+ deserializeReq(data: unknown): RedisRequest {
34
+ return data as RedisRequest;
35
+ }
36
+
37
+ deserializeResp(data: unknown): RedisResponse {
38
+ return data as RedisResponse;
39
+ }
40
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * sql adapter — fingerprints on normalized query + args.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ import type { Adapter } from "../xrr.js";
6
+
7
+ export interface SqlRequest {
8
+ query: string;
9
+ args?: unknown[];
10
+ }
11
+
12
+ export interface SqlResponse {
13
+ rows?: Record<string, unknown>[];
14
+ affected?: number;
15
+ }
16
+
17
+ function normalizeQuery(q: string): string {
18
+ return q.toLowerCase().replace(/\s+/g, " ").trim();
19
+ }
20
+
21
+ function sortedKeys(obj: Record<string, unknown>): Record<string, unknown> {
22
+ return Object.fromEntries(
23
+ Object.keys(obj)
24
+ .sort()
25
+ .map((k) => [k, obj[k]])
26
+ );
27
+ }
28
+
29
+ export class SqlAdapter implements Adapter<SqlRequest, SqlResponse> {
30
+ readonly id = "sql";
31
+
32
+ async fingerprint(req: SqlRequest): Promise<string> {
33
+ const canonical = JSON.stringify(
34
+ sortedKeys({ args: req.args ?? null, query: normalizeQuery(req.query) })
35
+ );
36
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
37
+ }
38
+
39
+ serializeReq(req: SqlRequest): unknown {
40
+ return req;
41
+ }
42
+
43
+ serializeResp(resp: SqlResponse): unknown {
44
+ return resp;
45
+ }
46
+
47
+ deserializeReq(data: unknown): SqlRequest {
48
+ return data as SqlRequest;
49
+ }
50
+
51
+ deserializeResp(data: unknown): SqlResponse {
52
+ return data as SqlResponse;
53
+ }
54
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * FileCassette — reads/writes YAML envelope files.
3
+ */
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import yaml from "js-yaml";
7
+ import { ErrCassetteMiss, type Cassette } from "./xrr.js";
8
+
9
+ interface Envelope {
10
+ xrr: string;
11
+ adapter: string;
12
+ fingerprint: string;
13
+ recorded_at: string;
14
+ payload: unknown;
15
+ }
16
+
17
+ export class FileCassette implements Cassette {
18
+ constructor(private readonly dir: string) {}
19
+
20
+ async save(
21
+ adapterID: string,
22
+ fingerprint: string,
23
+ req: unknown,
24
+ resp: unknown
25
+ ): Promise<void> {
26
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
27
+ await this.write(adapterID, fingerprint, "req", now, req);
28
+ await this.write(adapterID, fingerprint, "resp", now, resp);
29
+ }
30
+
31
+ private async write(
32
+ adapterID: string,
33
+ fingerprint: string,
34
+ kind: "req" | "resp",
35
+ recordedAt: string,
36
+ payload: unknown
37
+ ): Promise<void> {
38
+ const env: Envelope = {
39
+ xrr: "1",
40
+ adapter: adapterID,
41
+ fingerprint,
42
+ recorded_at: recordedAt,
43
+ payload,
44
+ };
45
+ const data = yaml.dump(env, { lineWidth: -1 });
46
+ const filePath = path.join(this.dir, `${adapterID}-${fingerprint}.${kind}.yaml`);
47
+ await fs.writeFile(filePath, data, "utf8");
48
+ }
49
+
50
+ async load(
51
+ adapterID: string,
52
+ fingerprint: string
53
+ ): Promise<{ req: unknown; resp: unknown }> {
54
+ const req = await this.read(adapterID, fingerprint, "req");
55
+ const resp = await this.read(adapterID, fingerprint, "resp");
56
+ return { req, resp };
57
+ }
58
+
59
+ private async read(
60
+ adapterID: string,
61
+ fingerprint: string,
62
+ kind: "req" | "resp"
63
+ ): Promise<unknown> {
64
+ const filePath = path.join(this.dir, `${adapterID}-${fingerprint}.${kind}.yaml`);
65
+ let data: string;
66
+ try {
67
+ data = await fs.readFile(filePath, "utf8");
68
+ } catch (err: unknown) {
69
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
70
+ throw ErrCassetteMiss;
71
+ }
72
+ throw err;
73
+ }
74
+ const env = yaml.load(data) as Envelope;
75
+ if (!env || typeof env !== "object" || !("payload" in env)) {
76
+ throw new Error(`xrr: missing payload in ${kind}`);
77
+ }
78
+ return env.payload;
79
+ }
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Barrel export for @hop-top/xrr.
3
+ */
4
+ export { ErrCassetteMiss } from "./xrr.js";
5
+ export type { Adapter, Cassette, Mode, Session } from "./xrr.js";
6
+ export { FileCassette } from "./cassette.js";
7
+ export { FileSession } from "./session.js";
8
+ export { ExecAdapter } from "./adapters/exec.js";
9
+ export type { ExecRequest, ExecResponse } from "./adapters/exec.js";
10
+ export { HttpAdapter } from "./adapters/http.js";
11
+ export type { HttpRequest, HttpResponse } from "./adapters/http.js";
12
+ export { RedisAdapter } from "./adapters/redis.js";
13
+ export type { RedisRequest, RedisResponse } from "./adapters/redis.js";
14
+ export { SqlAdapter } from "./adapters/sql.js";
15
+ export type { SqlRequest, SqlResponse } from "./adapters/sql.js";
16
+ export { FsAdapter } from "./adapters/fs.js";
17
+ export type { FsOp, FsRequest, FsResponse } from "./adapters/fs.js";
package/src/session.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * FileSession — record/replay/passthrough session.
3
+ */
4
+ import { type Adapter, type Cassette, ErrCassetteMiss, type Mode, type Session } from "./xrr.js";
5
+
6
+ export class FileSession implements Session {
7
+ constructor(
8
+ private readonly mode: Mode,
9
+ private readonly cassette: Cassette
10
+ ) {}
11
+
12
+ async record<Req, Resp>(
13
+ adapter: Adapter<Req, Resp>,
14
+ req: Req,
15
+ do_: () => Promise<Resp>
16
+ ): Promise<Resp> {
17
+ switch (this.mode) {
18
+ case "record":
19
+ return this.doRecord(adapter, req, do_);
20
+ case "replay":
21
+ return this.doReplay(adapter, req);
22
+ case "passthrough":
23
+ return do_();
24
+ default: {
25
+ const exhaustive: never = this.mode;
26
+ throw new Error(`xrr: unknown mode "${exhaustive}"`);
27
+ }
28
+ }
29
+ }
30
+
31
+ private async doRecord<Req, Resp>(
32
+ adapter: Adapter<Req, Resp>,
33
+ req: Req,
34
+ do_: () => Promise<Resp>
35
+ ): Promise<Resp> {
36
+ const resp = await do_();
37
+ const fp = await adapter.fingerprint(req);
38
+ await this.cassette.save(
39
+ adapter.id,
40
+ fp,
41
+ adapter.serializeReq(req),
42
+ adapter.serializeResp(resp)
43
+ );
44
+ return resp;
45
+ }
46
+
47
+ private async doReplay<Req, Resp>(
48
+ adapter: Adapter<Req, Resp>,
49
+ req: Req
50
+ ): Promise<Resp> {
51
+ const fp = await adapter.fingerprint(req);
52
+ let loaded: { req: unknown; resp: unknown };
53
+ try {
54
+ loaded = await this.cassette.load(adapter.id, fp);
55
+ } catch (err) {
56
+ if (err === ErrCassetteMiss) throw ErrCassetteMiss;
57
+ throw err;
58
+ }
59
+ return adapter.deserializeResp(loaded.resp);
60
+ }
61
+ }
package/src/xrr.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Core xrr interfaces — mirrors Go reference implementation.
3
+ */
4
+
5
+ export type Mode = "record" | "replay" | "passthrough";
6
+
7
+ export const ErrCassetteMiss = new Error("xrr: cassette miss");
8
+
9
+ export interface Adapter<Req, Resp> {
10
+ id: string;
11
+ fingerprint(req: Req): Promise<string>;
12
+ serializeReq(req: Req): unknown;
13
+ serializeResp(resp: Resp): unknown;
14
+ deserializeReq(data: unknown): Req;
15
+ deserializeResp(data: unknown): Resp;
16
+ }
17
+
18
+ export interface Cassette {
19
+ save(adapterID: string, fingerprint: string, req: unknown, resp: unknown): Promise<void>;
20
+ load(adapterID: string, fingerprint: string): Promise<{ req: unknown; resp: unknown }>;
21
+ }
22
+
23
+ export interface Session {
24
+ record<Req, Resp>(
25
+ adapter: Adapter<Req, Resp>,
26
+ req: Req,
27
+ do_: () => Promise<Resp>
28
+ ): Promise<Resp>;
29
+ }