@laphilosophia/api-tape 1.6.0 β†’ 1.6.2

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 (3) hide show
  1. package/README.md +52 -21
  2. package/dist/index.js +80 -4
  3. package/package.json +8 -4
package/README.md CHANGED
@@ -1,23 +1,23 @@
1
- # API Tape
1
+ # πŸ–­ API Tape
2
2
 
3
- **Record and Replay HTTP API responses for offline development.**
3
+ **High-integrity HTTP proxy for deterministic API record & replay.**
4
4
 
5
5
  API Tape is a zero-config CLI tool that acts as a transparent HTTP proxy. It records API responses to local JSON files ("tapes") and replays them instantlyβ€”perfect for offline development, flaky API testing, and reproducible demos.
6
6
 
7
+ > [!NOTE]
8
+ > **v1.6.2 Highlights**: Introduced a structural **Graceful Shutdown** mechanism for all platforms (including Windows), a new **Comprehensive Examples** suite, and improved test reliability in CI environments.
9
+
7
10
  ## Features
8
11
 
9
12
  - **Record Mode** β€” Proxies requests to your target API and saves responses
10
13
  - **Replay Mode** β€” Serves cached responses instantly from disk
11
14
  - **Hybrid Mode** β€” Replays cached tapes, falls back to upstream on cache miss
12
- - **Tape Management Commands** β€” List, inspect, clear, and prune tapes from CLI
13
- - **Header Redaction** β€” Mask sensitive response headers before writing tapes
14
- - **JSON Body Redaction** β€” Redact selected JSON paths before persisting response bodies
15
- - **Runtime Metrics** β€” Periodic and shutdown stats for replay hit/miss, upstream calls, and latency
16
- - **Zero Config** β€” Works out of the box with sensible defaults
17
- - **Binary Safe** β€” Handles images, compressed responses, and any content type
15
+ - **Forensic CLI** β€” List, inspect, clear, and prune tapes with `tape`
16
+ - **Security Redaction** β€” Mask sensitive response headers and JSON body paths
17
+ - **Non-Deterministic Matching** β€” Handle shifting query params and unstable JSON bodies
18
+ - **Runtime Metrics** β€” Real-time and shutdown stats for hit rates and latency
19
+ - **Binary Safe** β€” Handles images, compressed payloads, and any content type
18
20
  - **Replay Header** β€” Responses include `X-Api-Tape: Replayed` for easy debugging
19
- - **Versioned Tape Schema** β€” Each tape includes `schemaVersion` for compatibility checks
20
- - **Match Strategies** β€” `exact`, `normalized`, and `body-aware` matching for better replay hit rates
21
21
 
22
22
  ---
23
23
 
@@ -92,8 +92,8 @@ Both legacy mode (`tape --target ...`) and explicit serve command (`tape serve -
92
92
  | `-p, --port <number>` | Local server port | `8080` |
93
93
  | `-d, --dir <path>` | Directory to save tapes | `./tapes` |
94
94
  | `--record-on-miss <boolean>` | In hybrid mode, save upstream response when tape is missing | `true` |
95
- | `--redact-header <headers>` | Comma-separated response header names to redact in saved tapes | β€” |
96
- | `--redact-json-path <paths>` | Comma-separated JSON paths to redact in JSON response bodies | β€” |
95
+ | `--redact-header <headers>` | Comma-separated **response** header names to redact | β€” |
96
+ | `--redact-json-path <paths>` | Comma-separated JSON paths to redact in response bodies | β€” |
97
97
  | `--stats-interval <seconds>` | Emit runtime metrics every N seconds (`0` disables) | `0` |
98
98
  | `--stats-json` | Emit metrics as JSON lines | `false` |
99
99
  | `--match-strategy <strategy>` | Tape matching strategy: `exact`, `normalized`, or `body-aware` | `exact` |
@@ -110,7 +110,7 @@ For machine-readable output:
110
110
  tape serve --target "https://jsonplaceholder.typicode.com" --stats-interval 10 --stats-json
111
111
  ```
112
112
 
113
- On shutdown, API Tape always prints a final summary (`FINAL_STATS`).
113
+ On shutdown, API Tape flushes one immediate `STATS` snapshot (if interval is enabled) and always prints a final summary (`FINAL_STATS`).
114
114
 
115
115
  ### Redaction options
116
116
 
@@ -122,19 +122,34 @@ tape serve --target "https://api.example.com" --mode record \
122
122
 
123
123
  `--redact-json-path` applies only when response `content-type` is JSON.
124
124
 
125
- ### Match strategy
125
+ ### Match Strategies
126
+
127
+ Use these strategies to handle non-deterministic API behaviors and improve replay hit rates:
126
128
 
127
- - `exact` (default): hashes `METHOD|URL` as-is.
128
- - `normalized`: sorts query params before hashing, so `/search?a=1&b=2` and `/search?b=2&a=1` map to the same tape.
129
- - `body-aware`: uses normalized URL plus request body signature (JSON canonicalized when possible), useful for POST/PUT APIs sharing paths.
129
+ - **`exact`** (default): Hashes the literal `METHOD|URL`. Use for simple, static APIs.
130
+ - **`normalized`**: Canonicalizes query parameters by sorting them alphabetically.
131
+ - Transforms: `/search?page=1&q=test` β†’ `/search?q=test&page=1`
132
+ - _Benefit_: Drastically improves hit rates for clients that send query params in varying orders.
133
+ - **`body-aware`**: Combines the normalized URL with a canonicalized request body signature.
134
+ - Handles: Differences in JSON key order or spacing in POST/PUT requests.
135
+ - _Benefit_: Essential for GraphQL or complex REST APIs where the same URL is used for different operations based on the body payload.
130
136
 
131
137
  ### Tape management commands
132
138
 
139
+ Manage your forensic substrate with built-in tape utilities:
140
+
133
141
  ```bash
134
- tape tape list --dir ./tapes
135
- tape tape inspect <hash> --dir ./tapes
136
- tape tape clear --yes --dir ./tapes
137
- tape tape prune --older-than 30 --dir ./tapes
142
+ # List all recorded tapes with their method, route, and timestamp
143
+ tape list --dir ./tapes
144
+
145
+ # Inspect a specific tape's metadata, status, and headers
146
+ tape inspect <hash> --dir ./tapes
147
+
148
+ # Clear all tapes from a directory (requires --yes confirmation)
149
+ tape clear --yes --dir ./tapes
150
+
151
+ # Prune tapes older than N days to keep your local environment clean
152
+ tape prune --older-than 30 --dir ./tapes
138
153
  ```
139
154
 
140
155
  ---
@@ -177,6 +192,22 @@ A GitHub Actions workflow runs `npm test` on both Linux and Windows for pushes a
177
192
 
178
193
  ---
179
194
 
195
+ ---
196
+
197
+ ## Contributing
198
+
199
+ We welcome contributions! Please see our [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to get started. All participants are expected to follow our [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
200
+
201
+ ## Security
202
+
203
+ To report a security vulnerability, please use the process described in [SECURITY.md](./SECURITY.md).
204
+
205
+ ## Support
206
+
207
+ If you need help using API Tape, check our [SUPPORT.md](./SUPPORT.md) or join the conversation in [GitHub Discussions](https://github.com/laphilosophia/api-tape/discussions).
208
+
209
+ ---
210
+
180
211
  ## Use Cases
181
212
 
182
213
  - **Offline Development** β€” Work without internet or VPN
package/dist/index.js CHANGED
@@ -33,6 +33,68 @@ var import_http_proxy = __toESM(require("http-proxy"));
33
33
  var import_path = __toESM(require("path"));
34
34
  var import_stream = require("stream");
35
35
 
36
+ // package.json
37
+ var package_default = {
38
+ name: "@laphilosophia/api-tape",
39
+ version: "1.6.2",
40
+ description: "High-integrity HTTP proxy for deterministic API record & replay. Features non-deterministic matching (canonical JSON/Query), sensitive data redaction, and deep forensics via CLI management.",
41
+ main: "dist/index.js",
42
+ bin: {
43
+ tape: "./dist/index.js",
44
+ "api-tape": "./dist/index.js"
45
+ },
46
+ files: [
47
+ "dist"
48
+ ],
49
+ publishConfig: {
50
+ access: "public"
51
+ },
52
+ scripts: {
53
+ build: "tsup src/index.ts --format cjs --clean",
54
+ prepublishOnly: "npm run build",
55
+ test: "npm run build && node --test"
56
+ },
57
+ keywords: [
58
+ "api",
59
+ "mock",
60
+ "record",
61
+ "replay",
62
+ "proxy",
63
+ "offline",
64
+ "testing",
65
+ "http",
66
+ "vcr",
67
+ "deterministic",
68
+ "redaction",
69
+ "forensics",
70
+ "canonical-json"
71
+ ],
72
+ author: "Erdem Arslan <me@erdem.work>",
73
+ license: "MIT",
74
+ repository: {
75
+ type: "git",
76
+ url: "git+https://github.com/laphilosophia/api-tape.git"
77
+ },
78
+ homepage: "https://github.com/laphilosophia/api-tape#readme",
79
+ bugs: {
80
+ url: "https://github.com/laphilosophia/api-tape/issues"
81
+ },
82
+ type: "commonjs",
83
+ dependencies: {
84
+ chalk: "^5.6.2",
85
+ commander: "^14.0.2",
86
+ "fs-extra": "^11.3.3",
87
+ "http-proxy": "^1.18.1"
88
+ },
89
+ devDependencies: {
90
+ "@types/fs-extra": "^11.0.4",
91
+ "@types/http-proxy": "^1.17.17",
92
+ "@types/node": "^25.0.8",
93
+ tsup: "^8.5.1",
94
+ typescript: "^5.9.3"
95
+ }
96
+ };
97
+
36
98
  // src/constants.ts
37
99
  var CURRENT_SCHEMA_VERSION = 1;
38
100
 
@@ -423,16 +485,24 @@ var runServe = (opts) => {
423
485
  });
424
486
  });
425
487
  let metricsTimer;
488
+ let isShuttingDown = false;
426
489
  if (statsIntervalSec > 0) {
427
490
  metricsTimer = setInterval(() => {
428
491
  printMetrics(metrics, statsJson);
429
492
  }, statsIntervalSec * 1e3);
430
493
  }
431
- const shutdown = (signal) => {
494
+ const shutdown = (_signal) => {
495
+ if (isShuttingDown) {
496
+ return;
497
+ }
498
+ isShuttingDown = true;
432
499
  if (metricsTimer) {
433
500
  clearInterval(metricsTimer);
434
501
  metricsTimer = void 0;
435
502
  }
503
+ if (statsIntervalSec > 0) {
504
+ printMetrics(metrics, statsJson, "STATS");
505
+ }
436
506
  printMetrics(metrics, statsJson, "FINAL_STATS");
437
507
  server.close(() => {
438
508
  process.exit(0);
@@ -444,6 +514,10 @@ var runServe = (opts) => {
444
514
  process.once("SIGINT", () => shutdown("SIGINT"));
445
515
  process.once("SIGTERM", () => shutdown("SIGTERM"));
446
516
  process.once("SIGBREAK", () => shutdown("SIGBREAK"));
517
+ if (!process.stdin.isTTY) {
518
+ process.stdin.resume();
519
+ process.stdin.once("close", () => shutdown("STDIN_EOF"));
520
+ }
447
521
  console.log(import_chalk2.default.bold(`
448
522
  \u{1F4FC} API Tape Running`));
449
523
  console.log(
@@ -467,7 +541,9 @@ var runServe = (opts) => {
467
541
  if (statsJson) {
468
542
  console.log(` ${import_chalk2.default.dim("Stats Format:")} json`);
469
543
  }
470
- console.log(` ${import_chalk2.default.dim("Match Strategy:")} ${matchStrategy}`);
544
+ if (matchStrategy) {
545
+ console.log(` ${import_chalk2.default.dim("Match Strategy:")} ${matchStrategy}`);
546
+ }
471
547
  console.log("");
472
548
  server.listen(port);
473
549
  };
@@ -558,14 +634,14 @@ var run = () => {
558
634
  const argv = process.argv;
559
635
  const hasSubCommand = argv.length > 2 && ["serve", "tape"].includes(argv[2]);
560
636
  if (!hasSubCommand) {
561
- const legacy = addServeOptions(new import_commander.Command()).name("api-tape").description("Record and Replay HTTP API responses for offline development.").version("1.6.0").action((options) => {
637
+ const legacy = addServeOptions(new import_commander.Command()).name("api-tape").description("Record and Replay HTTP API responses for offline development.").version(package_default.version).action((options) => {
562
638
  runServe(options);
563
639
  });
564
640
  legacy.parse(argv);
565
641
  return;
566
642
  }
567
643
  const program = new import_commander.Command();
568
- program.name("api-tape").description("Record and Replay HTTP API responses for offline development.").version("1.6.0");
644
+ program.name("api-tape").description("Record and Replay HTTP API responses for offline development.").version(package_default.version);
569
645
  addServeOptions(program.command("serve").description("Run API Tape proxy server")).action(
570
646
  (options) => {
571
647
  runServe(options);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@laphilosophia/api-tape",
3
- "version": "1.6.0",
4
- "description": "Record and Replay HTTP API responses for offline development",
3
+ "version": "1.6.2",
4
+ "description": "High-integrity HTTP proxy for deterministic API record & replay. Features non-deterministic matching (canonical JSON/Query), sensitive data redaction, and deep forensics via CLI management.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "tape": "./dist/index.js",
@@ -27,9 +27,13 @@
27
27
  "offline",
28
28
  "testing",
29
29
  "http",
30
- "vcr"
30
+ "vcr",
31
+ "deterministic",
32
+ "redaction",
33
+ "forensics",
34
+ "canonical-json"
31
35
  ],
32
- "author": "Erdem Arslan",
36
+ "author": "Erdem Arslan <me@erdem.work>",
33
37
  "license": "MIT",
34
38
  "repository": {
35
39
  "type": "git",