api-tracer-kit 1.0.0 → 1.1.0

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.
@@ -0,0 +1,120 @@
1
+ # Getting started
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm install api-tracer-kit
7
+ ```
8
+
9
+ Nothing runs on install, and importing the package evaluates no browser global.
10
+ Tracing begins only when you call `init()`.
11
+
12
+ ## One command
13
+
14
+ ```bash
15
+ npx api-tracer setup
16
+ ```
17
+
18
+ It detects your framework, writes a module that configures the tracer, adds the
19
+ import to your entry file, and gitignores the console's data directory. Re-running
20
+ it is safe, and it warns if anything else already calls `init()`.
21
+
22
+ | Option | |
23
+ | --- | --- |
24
+ | `--framework cra\|vite\|next\|node` | override the detection |
25
+ | `--entry <file>` | the entry file to import from |
26
+ | `--out <file>` | where to write the module (default `src/apiTracer.js`) |
27
+ | `--axios false` | do not attach axios |
28
+ | `--envelope false` | your API reports failure in the status line, not the body |
29
+ | `--force` | overwrite an existing file |
30
+
31
+ What it works out for you:
32
+
33
+ - **the framework**, from your dependencies
34
+ - **the environment gate** that your bundler will actually fold — `REACT_APP_ENV`
35
+ for CRA, `import.meta.env.DEV` for Vite, `NODE_ENV` for Next and Node
36
+ - **whether to attach axios**, from whether you depend on it
37
+ - **TypeScript or JavaScript**, from whether you have a `tsconfig.json`
38
+
39
+ ## Or do it by hand
40
+
41
+ Put this at your application's entry point, before anything makes a request:
42
+
43
+ ```ts
44
+ import { apiTracer } from 'api-tracer-kit';
45
+
46
+ apiTracer.init();
47
+ ```
48
+
49
+ Every `fetch` and `XMLHttpRequest` afterwards is traced. Read them back with:
50
+
51
+ ```ts
52
+ apiTracer.getTraces();
53
+ ```
54
+
55
+ ### If you use axios
56
+
57
+ axios instances are plain objects with no global to patch, so hand yours over:
58
+
59
+ ```ts
60
+ import axios from 'axios';
61
+
62
+ apiTracer.init();
63
+ apiTracer.useAxios(axios);
64
+ ```
65
+
66
+ One call covers the default export **and every instance `axios.create()` makes
67
+ afterwards**. See [axios](./tracer.md#axios) for why this is the one integration
68
+ the package cannot do for you.
69
+
70
+ > **Configure it in exactly one place.** `init()` is idempotent — a second call
71
+ > keeps the first one's configuration and warns about the options it ignored.
72
+ > Two modules each calling `init()` is the most common way to end up with a
73
+ > tracer that runs but does nothing you asked for.
74
+
75
+ ## See them
76
+
77
+ Three options, in increasing order of usefulness.
78
+
79
+ **1. The console log.** Fine for a quick check:
80
+
81
+ ```ts
82
+ apiTracer.subscribe((trace) => {
83
+ console.log(trace.request.method, trace.request.url, trace.response?.status, `${trace.timing.duration}ms`);
84
+ });
85
+ ```
86
+
87
+ **2. The in-app panel.** A floating list with search, filters and per-call
88
+ detail. React only:
89
+
90
+ ```tsx
91
+ import { ApiTracerPanel } from 'api-tracer-kit/ui';
92
+
93
+ <ApiTracerPanel enabled={process.env.NODE_ENV !== 'production'} />;
94
+ ```
95
+
96
+ **3. The console.** A separate process that maps every endpoint in your source,
97
+ receives your app's traffic, and lets you re-send any call. This is the one
98
+ worth setting up. See [the console](./console.md).
99
+
100
+ ```bash
101
+ npx api-tracer start
102
+ ```
103
+
104
+ then point the tracer at it:
105
+
106
+ ```ts
107
+ apiTracer.init({ reportTo: true }); // true means http://localhost:4400
108
+ ```
109
+
110
+ ## Where to go next
111
+
112
+ | | |
113
+ | --- | --- |
114
+ | [Tracer API](./tracer.md) | every option, the `ApiTrace` model, storage, lifecycle |
115
+ | [The console](./console.md) | CLI, dashboard, replay, drift, coverage, import/export |
116
+ | [Configuration](./configuration.md) | the config file, scanner presets, sign-in flows |
117
+ | [Frameworks](./frameworks.md) | React, Next.js, Vite, CRA, Node, plain JavaScript |
118
+ | [Troubleshooting](./troubleshooting.md) | when nothing is being recorded |
119
+ | [Security](./security.md) | redaction, what is stored, what is safe to share |
120
+ | [Architecture](./architecture.md) | how it works inside |
@@ -0,0 +1,143 @@
1
+ # Security
2
+
3
+ The tracer sees everything your API sees. On some products that means
4
+ credentials, personal data, and — in health, finance or HR software — data whose
5
+ mishandling is a regulatory matter, not just an embarrassment. Read this before
6
+ deploying a console anywhere but your own laptop.
7
+
8
+ ## What is redacted, by default
9
+
10
+ **Headers**, matched case-insensitively:
11
+
12
+ ```
13
+ authorization, cookie, set-cookie, proxy-authorization, x-api-key, api-key,
14
+ auth_token, access_token, secret_token, x-auth-token, x-csrf-token
15
+ ```
16
+
17
+ **Body and query fields**, matched by name:
18
+
19
+ ```
20
+ /(token|password|passwd|secret|api[-_]?key|authorization|credential|otp|ssn)/i
21
+ ```
22
+
23
+ The **value** is replaced with `<redacted>` and the surrounding shape is kept, so
24
+ the sample still shows what the endpoint expects.
25
+
26
+ Redaction happens **before** a trace is stored, so a real credential is never
27
+ held in the tracer's memory, never reaches a subscriber, and never leaves the
28
+ browser. The console redacts again before writing anything to disk.
29
+
30
+ Extend rather than replace:
31
+
32
+ ```ts
33
+ import { DEFAULT_REDACT_HEADERS } from 'api-tracer-kit';
34
+
35
+ apiTracer.init({
36
+ redactHeaders: [...DEFAULT_REDACT_HEADERS, 'x-internal-key'],
37
+ redactFields: /(token|password|secret|nhs_number|date_of_birth)/i,
38
+ });
39
+ ```
40
+
41
+ ## What is *not* redacted
42
+
43
+ This is the part that matters.
44
+
45
+ Redaction is **name-based**. A field called `notes` containing a patient's
46
+ history, a `body` containing an address, a response listing every user — none of
47
+ those look like credentials, and none are touched. The tracer records response
48
+ bodies in full, up to `maxBodyBytes`.
49
+
50
+ So: **assume every trace and every capture contains real data from the
51
+ environment you are watching.**
52
+
53
+ If that is unacceptable for your product, either narrow what is traced:
54
+
55
+ ```ts
56
+ apiTracer.init({ include: ['/api/reference/'] });
57
+ ```
58
+
59
+ or drop response bodies entirely:
60
+
61
+ ```ts
62
+ apiTracer.init({ maxBodyBytes: 0 });
63
+ ```
64
+
65
+ or post-process in a subscriber before anything is stored or reported.
66
+
67
+ ## Where data ends up
68
+
69
+ ### In the browser
70
+
71
+ Traces live in memory, in a capped ring (500 by default), and disappear on
72
+ reload. Nothing is persisted unless you supply storage that does.
73
+
74
+ ### In the console's data directory
75
+
76
+ `<project>/.api-tracer/` holds captured request and response bodies, response
77
+ shapes, and a real session token.
78
+
79
+ **Add it to `.gitignore` before you run anything:**
80
+
81
+ ```
82
+ .api-tracer/
83
+ ```
84
+
85
+ `tokens.json` is written owner-only (`chmod 600`), but it is still a real
86
+ credential on disk. **Sign out** deletes it, and on a shared machine that is
87
+ worth doing.
88
+
89
+ If it has already been committed, unstage it — the files stay on disk:
90
+
91
+ ```bash
92
+ git rm -r --cached .api-tracer
93
+ ```
94
+
95
+ If it reached a pushed commit, treat the token as compromised and rotate it. The
96
+ bodies are in the history and removing them means rewriting it.
97
+
98
+ ## What is safe to share
99
+
100
+ HAR, cURL and Postman exports carry `<paste your token>` in place of a real
101
+ token, so an export is safe to attach to a ticket or hand to a backend
102
+ developer — **as far as credentials go**.
103
+
104
+ The bodies in them are still real. An export of a busy session is a data
105
+ extract. Treat it accordingly.
106
+
107
+ ## Deploying a console
108
+
109
+ It is a Node process that writes JSON files and holds a token. It is not a
110
+ static asset.
111
+
112
+ - **Keep it off the public internet.** VPN, SSO, or an allowlisted origin behind
113
+ your CDN.
114
+ - **`CONSOLE_USER` / `CONSOLE_PASS`** turn on basic auth — but **`/api/record`
115
+ stays open**, because the app posts recordings from a browser that cannot send
116
+ those credentials. The gate protects reading captures, replaying calls and the
117
+ stored token; it does not stop someone who can reach the host from posting
118
+ junk recordings. If that matters, put the whole thing behind VPN or SSO.
119
+ - **`ALLOWED_ORIGINS`** is an allowlist for posting recordings. Localhost always
120
+ works; every other origin must be named, so a stray site cannot feed or read
121
+ the console.
122
+ - **`HOST`** stays on `127.0.0.1` unless you change it, so a laptop instance is
123
+ not exposed by accident.
124
+ - **Keep the data volume off shared storage.**
125
+ - **Mixed content**: an HTTPS app cannot post to an `http://` console, and the
126
+ browser blocks it silently. Terminate TLS in front of it, or serve it under a
127
+ path on the app's own origin.
128
+
129
+ ## Replaying against production
130
+
131
+ The console refuses to replay writes against an environment named `prod`.
132
+
133
+ That is a guard, not a guarantee — it matches on the environment name you
134
+ configured. Consider commenting out the production base URL in your config
135
+ entirely, so it cannot be selected. `LOCK_ENV` pins a shared console to one
136
+ environment so nobody can switch it.
137
+
138
+ Replay is not a dry run. Every POST creates a real record each time.
139
+
140
+ ## Reporting a vulnerability
141
+
142
+ Open an issue for anything non-sensitive. For a genuine vulnerability, contact
143
+ the maintainer directly rather than filing publicly.
package/docs/tracer.md ADDED
@@ -0,0 +1,330 @@
1
+ # The tracer
2
+
3
+ The framework-free half of the package. No React, no axios, no DOM required.
4
+
5
+ ## `apiTracer`
6
+
7
+ A single shared instance. It is held in a global registry keyed by
8
+ `Symbol.for('api-tracer-kit.registry')`, so `api-tracer-kit` and
9
+ `api-tracer-kit/axios` — separate bundles with separate module graphs — resolve
10
+ to the same object. Without that, `useAxios()` would attach to an instance
11
+ `getTraces()` never reads from.
12
+
13
+ | Method | Description |
14
+ | --- | --- |
15
+ | `init(options?)` | Installs the interceptors. Idempotent: a second call keeps the first one's configuration and warns about the options it ignored. Returns `this`. |
16
+ | `useAxios(axios)` | Traces an axios object and everything it creates. Calls `init()` first if needed. Returns `this`. |
17
+ | `getTraces()` | Every trace held, oldest first. |
18
+ | `getTrace(id)` | One trace, or `undefined`. |
19
+ | `subscribe(fn)` | Calls `fn(trace)` for each **finished** trace. Returns the unsubscribe function. |
20
+ | `clear()` | Drops every trace. |
21
+ | `remove(id)` | Drops one. |
22
+ | `destroy()` | Restores every patched global and every axios adapter. After this the runtime behaves exactly as it did before `init()`. |
23
+ | `isActive` | Whether tracing is currently installed. |
24
+
25
+ Standalone function forms, for callers who would rather not reach through an
26
+ object: `initApiTracer`, `getApiTraces`, `clearApiTraces`,
27
+ `subscribeToApiTraces`, `destroyApiTracer`. They act on the same shared
28
+ instance.
29
+
30
+ `ApiTracer` is exported as a class too, if you want an isolated instance — most
31
+ useful in tests, where a shared singleton between test cases is a nuisance.
32
+
33
+ ```ts
34
+ import { ApiTracer } from 'api-tracer-kit';
35
+
36
+ const tracer = new ApiTracer().init({ transports: ['fetch'] });
37
+ // ...
38
+ tracer.destroy();
39
+ ```
40
+
41
+ ## Options
42
+
43
+ Every one is optional.
44
+
45
+ ### `transports`
46
+
47
+ ```ts
48
+ transports?: ('fetch' | 'xhr' | 'axios')[]
49
+ ```
50
+
51
+ Which globals to patch. Default: all of them that exist in the runtime. Pass
52
+ `[]` to patch nothing globally — useful when you only want axios, which is
53
+ attached separately.
54
+
55
+ `'axios'` in this list does nothing on its own; axios has no global to patch and
56
+ is reached only through `useAxios()`.
57
+
58
+ ### `include` / `exclude`
59
+
60
+ ```ts
61
+ include?: (string | RegExp)[]
62
+ exclude?: (string | RegExp)[]
63
+ ```
64
+
65
+ Strings match by substring, so `'/api/'` is usually enough. `exclude` is applied
66
+ first and wins. With no `include`, everything not excluded is traced.
67
+
68
+ ```ts
69
+ apiTracer.init({ include: ['/api/'], exclude: [/\/health$/, 'analytics'] });
70
+ ```
71
+
72
+ `reportTo`'s own endpoint is excluded automatically — the tracer never traces
73
+ its own reporting.
74
+
75
+ ### `redactHeaders` / `redactFields`
76
+
77
+ ```ts
78
+ redactHeaders?: string[] // default: DEFAULT_REDACT_HEADERS
79
+ redactFields?: RegExp // default: DEFAULT_REDACT_FIELDS
80
+ ```
81
+
82
+ Header names are matched case-insensitively; field names are matched with the
83
+ regex. Both replace the **value** with `<redacted>` and keep the surrounding
84
+ shape. Redaction happens before a trace is stored, so a real credential is never
85
+ held in memory.
86
+
87
+ Defaults are exported so you can extend rather than replace them:
88
+
89
+ ```ts
90
+ import { DEFAULT_REDACT_HEADERS } from 'api-tracer-kit';
91
+
92
+ apiTracer.init({ redactHeaders: [...DEFAULT_REDACT_HEADERS, 'x-internal-key'] });
93
+ ```
94
+
95
+ `DEFAULT_REDACT_HEADERS` covers `authorization`, `cookie`, `set-cookie`,
96
+ `proxy-authorization`, `x-api-key`, `api-key`, `auth_token`, `access_token`,
97
+ `secret_token`, `x-auth-token`, `x-csrf-token`.
98
+
99
+ `DEFAULT_REDACT_FIELDS` is
100
+ `/(token|password|passwd|secret|api[-_]?key|authorization|credential|otp|ssn)/i`.
101
+
102
+ ### `maxBodyBytes`
103
+
104
+ ```ts
105
+ maxBodyBytes?: number // default 200_000
106
+ ```
107
+
108
+ Bodies larger than this are recorded as omitted rather than truncated — a
109
+ partial JSON body is more misleading than none. A response declaring a larger
110
+ `content-length` is never read at all.
111
+
112
+ ### `maxTraces`
113
+
114
+ ```ts
115
+ maxTraces?: number // default 500
116
+ ```
117
+
118
+ The in-memory store is a capped ring; older traces fall off the end. An
119
+ unbounded store in a long-lived tab is a memory leak.
120
+
121
+ ### `envelope`
122
+
123
+ ```ts
124
+ envelope?: {
125
+ codeFields?: string[]; // default ['status', 'code']
126
+ okField?: string; // default 'success'
127
+ failFrom?: number; // default 400
128
+ } | false
129
+ ```
130
+
131
+ For APIs that answer HTTP `200` and put the real verdict in the body:
132
+
133
+ ```json
134
+ { "status": 801, "success": false, "message": "Authentication token header missing" }
135
+ ```
136
+
137
+ With an envelope configured, that trace gets `envelopeOk: false` and
138
+ `envelopeCode: 801` even though `response.status` is `200`. Pass an empty object
139
+ `{}` to accept the defaults; omit it entirely and no envelope is read.
140
+
141
+ ### `reportTo`
142
+
143
+ ```ts
144
+ reportTo?: string | boolean
145
+ ```
146
+
147
+ POST each finished trace to a console. `true` means `http://localhost:4400`.
148
+ Failures are swallowed — a console that is not running must never affect the
149
+ app — and after six consecutive failures the reporter stops trying.
150
+
151
+ ### `storage`
152
+
153
+ ```ts
154
+ storage?: TraceStorage
155
+ ```
156
+
157
+ Replaces the in-memory store. See [Storage](#storage).
158
+
159
+ ### `onTrace`
160
+
161
+ ```ts
162
+ onTrace?: (trace: ApiTrace) => void
163
+ ```
164
+
165
+ Identical to `subscribe()`, set up front. This is the one option a second
166
+ `init()` still honours, because adding a subscriber from another module is
167
+ legitimate.
168
+
169
+ ### `debug`
170
+
171
+ ```ts
172
+ debug?: boolean // default false
173
+ ```
174
+
175
+ Logs a line on init and reports subscribers that throw.
176
+
177
+ ## The `ApiTrace` model
178
+
179
+ ```ts
180
+ interface ApiTrace {
181
+ id: string;
182
+ transport: 'fetch' | 'xhr' | 'axios';
183
+ status: 'pending' | 'success' | 'error' | 'network-error' | 'cancelled';
184
+
185
+ request: {
186
+ url: string; // absolute
187
+ path: string; // origin + pathname, for grouping
188
+ method: string;
189
+ headers: Record<string, string>; // redacted
190
+ params: Record<string, unknown>; // parsed query string, redacted
191
+ body?: unknown; // parsed and redacted
192
+ bodyType: 'json' | 'formdata' | 'urlencoded' | 'text' | 'binary' | 'none';
193
+ bodyOmitted?: 'too-large' | 'stream' | 'binary' | 'unreadable';
194
+ };
195
+
196
+ response?: {
197
+ status: number;
198
+ statusText?: string;
199
+ headers: Record<string, string>; // redacted
200
+ body?: string; // raw text
201
+ bodyOmitted?: 'too-large' | 'stream' | 'binary' | 'unreadable';
202
+ size?: number;
203
+ };
204
+
205
+ timing: {
206
+ startedAt: number; // performance.now() where available
207
+ completedAt?: number;
208
+ duration?: number; // milliseconds, rounded
209
+ };
210
+
211
+ error?: { message: string; name?: string; stack?: string };
212
+
213
+ envelopeOk?: boolean;
214
+ envelopeCode?: number;
215
+ }
216
+ ```
217
+
218
+ ### What each `status` means
219
+
220
+ | | |
221
+ | --- | --- |
222
+ | `pending` | in flight; `response` and `timing.duration` are not set yet |
223
+ | `success` | HTTP 2xx |
224
+ | `error` | the server answered, but not with a 2xx |
225
+ | `network-error` | no answer: DNS failure, connection refused, timeout |
226
+ | `cancelled` | aborted by the caller — not a broken endpoint |
227
+
228
+ Subscribers are called only when a trace finishes, so a subscriber never sees
229
+ `pending`. `getTraces()` can return one.
230
+
231
+ ### Bodies
232
+
233
+ `bodyType` records what actually went on the wire, so a captured call can be
234
+ replayed the same way. A `FormData` is unpacked field by field — `JSON.stringify`
235
+ flattens one to `{}` — and files inside it become
236
+ `<file: scan.pdf, 20418 bytes>`, because a file cannot be replayed.
237
+
238
+ `bodyOmitted` says why a body is missing:
239
+
240
+ | | |
241
+ | --- | --- |
242
+ | `too-large` | over `maxBodyBytes` |
243
+ | `stream` | a `ReadableStream` request body, or a `text/event-stream` response — reading either would break it |
244
+ | `binary` | a Blob, ArrayBuffer, or a non-textual content type |
245
+ | `unreadable` | the body could not be cloned or parsed |
246
+
247
+ ## Storage
248
+
249
+ ```ts
250
+ interface TraceStorage {
251
+ add(trace: ApiTrace): void;
252
+ update(id: string, patch: Partial<ApiTrace>): ApiTrace | undefined;
253
+ get(id: string): ApiTrace | undefined;
254
+ getAll(): ApiTrace[];
255
+ remove(id: string): void;
256
+ clear(): void;
257
+ }
258
+ ```
259
+
260
+ The default is `MemoryTraceStorage`, exported so you can size it yourself:
261
+
262
+ ```ts
263
+ import { MemoryTraceStorage } from 'api-tracer-kit';
264
+
265
+ apiTracer.init({ storage: new MemoryTraceStorage(50) });
266
+ ```
267
+
268
+ `update()` returning `undefined` for an unknown id is normal, not an error: a
269
+ long-running request can be evicted by the cap while still in flight.
270
+
271
+ Nothing is persisted by default. To keep traces across reloads, supply storage
272
+ backed by `sessionStorage` or IndexedDB — but see
273
+ [Security](./security.md) first, because traces contain request and response
274
+ bodies.
275
+
276
+ ## axios
277
+
278
+ axios calls are traced two ways.
279
+
280
+ In a browser axios rides on `XMLHttpRequest`, so `apiTracer.init()` alone sees
281
+ them. `useAxios()` adds what the XHR adapter cannot:
282
+
283
+ - the `params` object as axios received it, rather than a re-parsed query string
284
+ - Node's `http` adapter, which no global patch can reach
285
+ - correct handling of axios's habit of rejecting on non-2xx while still having a
286
+ response
287
+
288
+ A call seen by both adapters is recorded **once**. axios builds and opens its
289
+ XHR synchronously inside its adapter, so the tracer raises a flag around the
290
+ adapter call that is still up when `open` runs and down before anything else can
291
+ start a request. The suppression is exact, not time-based.
292
+
293
+ `useAxios()` wraps the instance's adapter rather than only its interceptors,
294
+ because the adapter is the one place both the request and the raw response are
295
+ available. If your application later assigns `axios.defaults.adapter` — a mock,
296
+ a retry library, a custom transport — tracing survives it: the property is an
297
+ accessor that routes the assignment underneath the wrapper.
298
+
299
+ `destroy()` gives every instance back the adapter it last had, including
300
+ instances created through the patched `create()`.
301
+
302
+ ## What the tracer will not do to your app
303
+
304
+ Removing it changes nothing. Specifically:
305
+
306
+ - responses are read through `Response.clone()`, **after** the original has been
307
+ handed back, so your `.json()` still works
308
+ - `text/event-stream` responses are never buffered
309
+ - binary and non-textual responses are described, not read
310
+ - request bodies that are streams are never consumed
311
+ - errors are rethrown exactly as they arrived; aborts stay `AbortError`
312
+ - `AbortSignal`, redirects, credentials and headers pass through untouched
313
+ - nothing is retried, and no request is made twice
314
+ - XHR is observed with `addEventListener`, never by taking `onload` or
315
+ `onreadystatechange` — those belong to the caller
316
+ - a subscriber that throws is caught
317
+ - `destroy()` restores every patched global
318
+
319
+ ## Cleanup
320
+
321
+ ```ts
322
+ apiTracer.destroy();
323
+ ```
324
+
325
+ Restores `fetch`, the `XMLHttpRequest` prototype and every axios adapter, clears
326
+ the subscribers, and stops reporting. `fetch` is only restored if nothing else
327
+ has patched it since — clobbering another tool's interceptor on the way out
328
+ would be worse than leaving ours in place.
329
+
330
+ Calling `destroy()` on a tracer that was never started is safe.