@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.
- package/README.md +52 -21
- package/dist/index.js +80 -4
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
# API Tape
|
|
1
|
+
# π API Tape
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **Runtime Metrics** β
|
|
16
|
-
- **
|
|
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
|
|
96
|
-
| `--redact-json-path <paths>` | Comma-separated JSON paths to redact in
|
|
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
|
|
125
|
+
### Match Strategies
|
|
126
|
+
|
|
127
|
+
Use these strategies to handle non-deterministic API behaviors and improve replay hit rates:
|
|
126
128
|
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
- `
|
|
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
|
-
|
|
135
|
-
tape
|
|
136
|
-
|
|
137
|
-
|
|
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 = (
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|