api-tracer-kit 1.0.1 → 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,275 @@
1
+ # Configuration
2
+
3
+ There is no required configuration. `api-tracer scan` works in a project it has
4
+ never seen: it runs every scanner preset over your codebase and keeps whichever
5
+ actually reads it, then reads your base URLs and auth header out of the source.
6
+
7
+ A config file exists for what guessing cannot cover — a bespoke service shape, a
8
+ base URL that is not written down, a sign-in flow worth automating.
9
+
10
+ ## The file
11
+
12
+ One of these in your project root, checked in order:
13
+
14
+ ```
15
+ api-tracer.config.mjs
16
+ api-tracer.config.js
17
+ api-tracer.config.json
18
+ ```
19
+
20
+ Use `.mjs` unless your project is ESM already — a `.js` file with
21
+ `export default` will not load inside a CommonJS package.
22
+
23
+ Generate one filled in with what the scan worked out:
24
+
25
+ ```bash
26
+ npx api-tracer init
27
+ ```
28
+
29
+ It prints what each preset found before writing, so you can see the evidence:
30
+
31
+ ```
32
+ 1,284 source files scanned
33
+ service-object 325
34
+ fetch-direct 1
35
+ axios-direct 0
36
+ ```
37
+
38
+ ### Every field
39
+
40
+ ```js
41
+ export default {
42
+ // shown in the console header and in Postman exports
43
+ name: 'My API',
44
+
45
+ // which preset reads this codebase; null or absent means guess
46
+ preset: 'service-object',
47
+
48
+ // for the service-object preset: the object key holding the path
49
+ pathKey: 'subUrl',
50
+
51
+ // where to look for API calls, relative to the project root
52
+ sources: ['src', 'app', 'lib', 'services', 'api'],
53
+
54
+ // never walked
55
+ ignore: ['node_modules', 'dist', 'build', 'coverage', '.git', '.next', 'out', 'vendor'],
56
+
57
+ extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'],
58
+
59
+ // environment -> base URL; guessed from the source when absent
60
+ baseUrls: {
61
+ dev: 'https://dev-api.example.com',
62
+ stage: 'https://stage-api.example.com',
63
+ },
64
+
65
+ // the header the API authenticates with
66
+ auth: { header: 'Authorization' },
67
+
68
+ // how to read pass/fail out of a 2xx body; false turns it off
69
+ envelope: { codeFields: ['status', 'code'], okField: 'success', failFrom: 400 },
70
+
71
+ // the module an endpoint belongs to; the filename by default
72
+ moduleOf: (file, root) => file.split('/').at(-2),
73
+
74
+ // your own scanner, when no preset fits
75
+ parse: (src, config) => [],
76
+
77
+ // an optional multi-step sign-in chain
78
+ login: null,
79
+ };
80
+ ```
81
+
82
+ ## Scanner presets
83
+
84
+ ### `service-object`
85
+
86
+ One exported function per endpoint, building a request object:
87
+
88
+ ```js
89
+ export const getPatients = (params) => {
90
+ const request = { subUrl: `/v1/patients.json`, params };
91
+ return get(request);
92
+ };
93
+ ```
94
+
95
+ Recognised verbs: `get`, `get2`, `getRequest`, `post`, `postRequest`, `put`,
96
+ `patch`, `delete`, `deletee`, `del`, `destroy`. The request argument may be
97
+ named `request`, `config`, `options` or `req`. Change the path key with
98
+ `pathKey` if yours is not `subUrl`.
99
+
100
+ ### `axios-direct`
101
+
102
+ ```js
103
+ export const listUsers = () => axios.get('/api/users', { params: { page: 1 } });
104
+ export const addUser = (body) => api.post(`/api/users/${body.id}`, body);
105
+ ```
106
+
107
+ Any identifier works, not just `axios` — `api`, `http`, `client`.
108
+
109
+ ### `fetch-direct`
110
+
111
+ ```js
112
+ export const load = () => fetch('/api/things');
113
+ export const save = (b) => fetch('/api/things', { method: 'POST', body: JSON.stringify(b) });
114
+ ```
115
+
116
+ ### Path placeholders
117
+
118
+ All three read `${expr}`, `:name` and `{name}` as holes, so Express- and
119
+ OpenAPI-style paths work as well as template literals.
120
+
121
+ ### Why regexes and not a parser
122
+
123
+ A parser would need a dependency per language and per flavour of syntax, and
124
+ would still not understand a template literal assembled from three variables. A
125
+ scanner that reads 90% of a codebase and says what it could not read is more
126
+ useful than a parser that refuses to start. Live recording covers the rest: a
127
+ call the scan cannot explain is adopted as an endpoint of its own.
128
+
129
+ Commented-out lines are blanked before scanning, with line numbers preserved.
130
+ Services routinely keep the previous URL directly above the live one, and
131
+ reading the first match would silently take the dead path.
132
+
133
+ ### Your own scanner
134
+
135
+ When no preset fits, supply `parse` rather than forking the tool. It is called
136
+ once per file with the source (comments already stripped) and the config, and
137
+ returns one object per endpoint:
138
+
139
+ ```js
140
+ export default {
141
+ parse(src, config) {
142
+ return [...src.matchAll(/route\("(\w+)",\s*"([^"]+)"\)/g)].map((m) => ({
143
+ name: m[2].replace(/\W+/g, '_'), // the endpoint's function name
144
+ method: m[1].toUpperCase(), // GET, POST, ...
145
+ subUrl: m[2], // the path template
146
+ usesParams: m[1] === 'get', // sends a query string
147
+ usesData: m[1] !== 'get', // sends a body
148
+ absolute: /^https?:\/\//.test(m[2]),
149
+ chunk: src.slice(m.index, m.index + 300), // scanned for auth quirks
150
+ index: m.index, // for the source line number
151
+ }));
152
+ },
153
+ };
154
+ ```
155
+
156
+ `parse` takes precedence over `preset`. Throwing on one file skips that file
157
+ rather than failing the scan.
158
+
159
+ ## Base URLs and the auth header
160
+
161
+ Both are read out of your source by default, so your environments stay in sync
162
+ with the app rather than being copied into a config that drifts.
163
+
164
+ Base URLs are found by looking for an object mapping environment names to URLs:
165
+
166
+ ```js
167
+ const apiBaseUrls = {
168
+ dev: 'https://dev-api.example.com',
169
+ prod: 'https://api.example.com',
170
+ };
171
+ ```
172
+
173
+ Recognised environment names: `dev`, `develop`, `development`, `stage`,
174
+ `staging`, `uat`, `qa`, `test`, `prod`, `production`, `local`. Failing that, the
175
+ first API-looking URL becomes `dev`.
176
+
177
+ The auth header is found by looking for a named constant — the strongest signal,
178
+ because a codebase that uses the header in more than one place tends to write it
179
+ that way:
180
+
181
+ ```js
182
+ const authTokenKey = 'AUTH_TOKEN';
183
+ ```
184
+
185
+ then for header names used as object keys or header lookups.
186
+
187
+ Set either explicitly in the config to override. Consider commenting out a
188
+ production base URL in a generated config, so nobody replays writes against live
189
+ data by selecting the wrong environment.
190
+
191
+ ## Sign-in flows
192
+
193
+ Plenty of APIs need more than one call to hand out a usable token. Rather than
194
+ make everyone paste one, describe the chain and the console will drive it.
195
+
196
+ Each step declares the fields to ask for, the call to make, and what to do with
197
+ the answer:
198
+
199
+ ```js
200
+ export default {
201
+ login: {
202
+ // optional: a short-lived signed JWT sent with every step
203
+ jwt: {
204
+ secret: process.env.API_SIGNING_KEY,
205
+ secretProd: process.env.API_SIGNING_KEY_PROD,
206
+ headerName: 'ACCESS_TOKEN',
207
+ timestampPrefix: 'MyApp-',
208
+ ttlSeconds: 600,
209
+ // or build the payload yourself
210
+ payload: ({ now, secretCode, env }) => ({ iat: now, exp: now + 600 }),
211
+ },
212
+
213
+ steps: [
214
+ {
215
+ name: 'credentials',
216
+ title: 'Sign in with your email and password.',
217
+ fields: [
218
+ { name: 'email', label: 'Email', type: 'email' },
219
+ { name: 'password', label: 'Password', type: 'password' },
220
+ ],
221
+ // the scanned endpoint id, so a path change in the app is picked up;
222
+ // `path` is the fallback when there is no catalog entry
223
+ endpointId: 'session.loginApi',
224
+ path: '/users/login.json',
225
+ body: (input) => ({ user: input }),
226
+ then: (data) => ({
227
+ state: { apiToken: data.api_token, phone: data.phone },
228
+ next: 'code',
229
+ }),
230
+ },
231
+ {
232
+ name: 'code',
233
+ title: 'Enter the code we sent you.',
234
+ fields: [{ name: 'otp', label: 'Code', inputmode: 'numeric' }],
235
+ path: '/users/verify_otp.json',
236
+ headers: (state) => ({ SECRET_TOKEN: state.apiToken }),
237
+ body: (input, state) => ({ otp: input.otp, phone: state.phone }),
238
+ then: (data) => ({
239
+ token: data.auth_token,
240
+ owner: { name: data.name, email: data.email },
241
+ }),
242
+ },
243
+ ],
244
+ },
245
+ };
246
+ ```
247
+
248
+ ### The step contract
249
+
250
+ | Field | |
251
+ | --- | --- |
252
+ | `name` | the step's id, used as `next` from the previous step |
253
+ | `title` | shown above the fields |
254
+ | `fields` | `{ name, label?, type?, inputmode?, placeholder?, options? }`. `options` renders a select. |
255
+ | `endpointId` | a scanned endpoint whose path to use |
256
+ | `path` | the path to POST to, when `endpointId` is absent or unmatched |
257
+ | `headers(state)` | extra headers for this step |
258
+ | `body(input, state)` | the request body from the submitted fields and the carried state |
259
+ | `then(data, state, input)` | what to do with the response |
260
+
261
+ `then` returns any of:
262
+
263
+ | | |
264
+ | --- | --- |
265
+ | `state` | merged into the carried state for later steps |
266
+ | `next` | the name of the step to show next |
267
+ | `choices` | options for a select on the next step |
268
+ | `token` | the auth token — ends the flow and stores it |
269
+ | `owner` | `{ name, email }`, shown on a shared console so people know whose session they are replaying as |
270
+
271
+ The password is forwarded once and is never stored, logged, or written to disk.
272
+ Only the resulting token is kept.
273
+
274
+ A project with no `login` config gets the paste-a-token form, which is all most
275
+ APIs need.
@@ -0,0 +1,428 @@
1
+ # The console
2
+
3
+ A separate process that maps every endpoint in your source, receives your app's
4
+ live traffic, and lets you fire any endpoint and read the response. It needs no
5
+ framework, no build step and — in most projects — no configuration.
6
+
7
+ ```bash
8
+ npx api-tracer start
9
+ ```
10
+
11
+ Reads your source, writes a catalog, and opens at `http://127.0.0.1:4400`.
12
+
13
+ ## Commands
14
+
15
+ | | |
16
+ | --- | --- |
17
+ | `api-tracer scan` | read the source, write the endpoint catalog |
18
+ | `api-tracer serve` | run the console |
19
+ | `api-tracer start` | scan, then serve |
20
+ | `api-tracer report` | build the insight report |
21
+ | `api-tracer init` | write a config file from what the scan guessed |
22
+
23
+ ### Options
24
+
25
+ | | |
26
+ | --- | --- |
27
+ | `--root <dir>` | the project to read (default: cwd) |
28
+ | `--data <dir>` | where captures are kept (default: `<root>/.api-tracer`) |
29
+ | `--port <n>` | default `4400` |
30
+ | `--host <addr>` | default `127.0.0.1` |
31
+ | `--preset <name>` | force a scanner preset |
32
+ | `--format md\|json` | report format (default `md`) |
33
+ | `--out <file>` | write the report to a file (default: stdout) |
34
+ | `--env <name>` | which base URL the report describes |
35
+ | `--force` | let `init` overwrite an existing config |
36
+
37
+ ## The catalog
38
+
39
+ `scan` reads your source into `<data>/endpoints.json`: one entry per endpoint,
40
+ with its module, HTTP verb, path template, the `${...}` holes in it, whether it
41
+ sends query params or a body, its auth quirk, and the file and line it came
42
+ from.
43
+
44
+ It is a plain JSON file, so a deployed console needs no source and no build —
45
+ just the file that shipped with it. Re-running `scan` where there is no source
46
+ keeps the catalog that is already there.
47
+
48
+ The scan also counts how many files outside its own module reference each
49
+ endpoint. Anything at zero is flagged **unused in app** and gets its own filter.
50
+
51
+ > This is a substring count, so `import * as services` or a re-export would hide
52
+ > a real usage. Confirm with a grep before deleting anything.
53
+
54
+ See [Configuration](./configuration.md) for how endpoints are found and what to
55
+ do when the presets do not fit your codebase.
56
+
57
+ ## Live recording
58
+
59
+ The main way to use it. Run your app normally with `reportTo` pointed at the
60
+ console, and every call it makes streams in as you click — no export, no import,
61
+ no typing payloads.
62
+
63
+ Each request lands on its endpoint in the tree with the real query params, the
64
+ real payload and the real path ids already filled in. The response is scored
65
+ too, so an endpoint your app just used successfully goes green without you
66
+ running anything.
67
+
68
+ Click **Live** in the header to pause or resume. The counter shows what has
69
+ arrived.
70
+
71
+ A call that no endpoint in the catalog explains is **not** dropped — it is
72
+ adopted as an endpoint of its own, marked *not found in source*, and behaves
73
+ like the rest. Recording only what could be matched would quietly hide real
74
+ traffic. **Clear uncatalogued** removes them.
75
+
76
+ ## Reading pass and fail
77
+
78
+ Some APIs return HTTP 200 for failures and put the real verdict in the envelope:
79
+
80
+ ```json
81
+ { "status": 801, "success": false, "message": "Authentication token header missing" }
82
+ ```
83
+
84
+ So an endpoint counts as **passing** only when the HTTP status is 2xx **and**
85
+ the body's status code is not at or above the failure threshold **and** the
86
+ success flag is not `false`. The response viewer shows the HTTP code and the
87
+ body code side by side.
88
+
89
+ Configure the convention with `envelope` in the config file, or set it to
90
+ `false` if your API uses the status line honestly.
91
+
92
+ ## Using one endpoint
93
+
94
+ Pick it in the tree. The request pane is tabbed like Postman:
95
+
96
+ - **Params** — query params, as JSON, stringified into one param, or as a raw
97
+ query string
98
+ - **Headers** — extra headers, merged on top of the token and `Content-Type` the
99
+ server adds
100
+ - **Body** — the type selector: `none` / `JSON` / `multipart/form-data` /
101
+ `x-www-form-urlencoded`. JSON gives a validated editor; the form types give
102
+ key/value rows. Switching type keeps your values.
103
+ - **Capture** — rules that store response values as `{{variables}}`
104
+
105
+ The body type is preselected from what your app actually sent, so a captured
106
+ `FormData` call opens on **multipart/form-data** with its fields already in the
107
+ table, and **Send** replays it that way. Replaying a form post as JSON would
108
+ fail for reasons that have nothing to do with the endpoint.
109
+
110
+ Also on the pane: the source location (`src/services/appointments.js:91`), the
111
+ resolved URL updating live as you type, a field per path placeholder, and after
112
+ **Send** (or Cmd/Ctrl+Enter) the status, body code, latency, size, response body
113
+ and headers. **Copy as cURL** leaves the token as a placeholder.
114
+
115
+ JSON editors are syntax highlighted with line numbers and Tab indents rather
116
+ than leaving the field — a coloured layer behind a transparent textarea, so
117
+ there is no editor library and no dependency.
118
+
119
+ ### Query params: three modes
120
+
121
+ Switching carries what you typed across.
122
+
123
+ - **JSON** — `{"page": 1, "ids": [3, 4], "filter": {"status": "new"}}`, serialized
124
+ the way axios does by default: `?page=1&ids[]=3&ids[]=4&filter[status]=new`
125
+ - **stringified** — the whole object in one encoded param,
126
+ `?params=%7B%22page%22%3A1%7D`. The param name is editable.
127
+ - **query string** — pasted straight from DevTools and sent byte-for-byte, so an
128
+ already-encoded `%2B` stays `%2B` instead of becoming `%252B`
129
+
130
+ The URL bar shows the fully resolved URL in every mode, built by the same rules
131
+ the server uses, so the preview never disagrees with what goes out.
132
+
133
+ ## Bulk runs and replay
134
+
135
+ **Run every GET endpoint** and **Run all GETs in this module** check many at
136
+ once, six in parallel.
137
+
138
+ Two deliberate limits on bulk GET runs:
139
+
140
+ - **Only GETs are ever run in bulk.** A bulk run that fired nineteen DELETEs
141
+ would be a very bad afternoon.
142
+ - **Path placeholders are left blank**, so `/appointments/${data.id}.json` is
143
+ called as `/appointments/.json`. That proves the endpoint answers; it does not
144
+ prove it behaves correctly with a real id. Treat bulk results as a smoke test.
145
+
146
+ For each placeholder a run takes the first of: the value captured from real
147
+ traffic, a variable sharing the placeholder's name, then blank. The run reports
148
+ how many endpoints went out blank, so a weak result never looks like a strong
149
+ one, and every saved result records the URL that was actually called.
150
+
151
+ ### Replay every captured call
152
+
153
+ Re-sends every endpoint that has a sample — GET and writes alike — with the
154
+ payload, query params, path ids and body type your app used.
155
+
156
+ It shows exactly what it will do before it does anything: a count per method,
157
+ the environment and base URL, and what it is leaving out. Defaults, all
158
+ changeable in that dialog:
159
+
160
+ - **DELETEs excluded.** Opt in with a checkbox; the endpoints are named.
161
+ - **Logout excluded**, since it can invalidate the token the rest depends on.
162
+ - **Endpoints needing a signed token excluded.**
163
+ - **Payloads containing a stripped secret are skipped**, because sending the
164
+ literal `<redacted>` would fail for a reason unrelated to the endpoint.
165
+ - **Runs one at a time**, in order. Captured writes often depend on each other,
166
+ and a failure order you cannot reproduce is worse than a slow run.
167
+ - **Refused entirely on a `prod` environment.**
168
+
169
+ This is not a dry run. Every POST creates a new record each time.
170
+
171
+ While it runs the dialog becomes a progress view: a pass/fail bar, a running
172
+ count, the call in flight, and the last few results. **Stop** halts after the
173
+ call in progress. Results survive navigating away and reloading, because the run
174
+ is held on the server.
175
+
176
+ A call that fails during a replay keeps its response — open that endpoint and
177
+ the response pane shows what came back, labelled *from the last replay*, until
178
+ you send it again yourself. Those endpoints are marked **failed when replayed**.
179
+
180
+ Results are saved and survive a restart, so you can see what changed since
181
+ yesterday. An endpoint that was passing and now fails is marked **broke since
182
+ last run**. Switching environment clears them, since dev results would be
183
+ misleading on stage.
184
+
185
+ ## Contract drift
186
+
187
+ A status check cannot see the failure that matters most on an API that answers
188
+ `200` for everything: a backend that quietly drops a field, or changes an id
189
+ from a number to a string, still looks green.
190
+
191
+ So every passing response is reduced to its **shape** — field paths and types,
192
+ values ignored — and compared with the endpoint's baseline. The first good
193
+ response becomes the baseline; after that a change is reported:
194
+
195
+ ```
196
+ -data[].pinned, +data[].author, data[].id: number->string
197
+ ```
198
+
199
+ Drifted endpoints get an amber dot in the tree, a panel on the endpoint, and
200
+ their own filter. When the change is intended, **This change is expected, make
201
+ it the baseline** promotes the new shape and clears the flag.
202
+
203
+ Live traffic feeds this for free: your app's own calls set and check the
204
+ baseline as you use it.
205
+
206
+ ## Coverage
207
+
208
+ The home screen shows how much of the API your app has actually exercised — how
209
+ many endpoints have ever been seen, which modules remain untouched, and how many
210
+ have drifted. Module rows show `seen/total`.
211
+
212
+ An endpoint nobody has triggered has not been proven either way, which is
213
+ different from failing. The **never captured** filter is the to-do list.
214
+
215
+ ## Variables and chaining
216
+
217
+ Use `{{name}}` in any path value, query param or payload. Set them by hand in
218
+ **Variables**, or capture them from a response: a rule like
219
+ `patient_id = data.0.id` stores that value after a passing call, for every later
220
+ call. Dotted paths walk arrays. A path that finds nothing reports an error
221
+ rather than silently storing a blank.
222
+
223
+ Variables are not a place for tokens — those stay in the console's own token
224
+ store.
225
+
226
+ ## Import
227
+
228
+ For traffic captured earlier, or from someone else's session.
229
+
230
+ 1. Open your app in Chrome, DevTools → **Network**, tick **Preserve log**.
231
+ 2. Click through the screens you care about.
232
+ 3. Right-click the request list → **Save all as HAR with content**.
233
+ 4. Drop the file anywhere on the console page.
234
+
235
+ Each request is matched back to the endpoint the scanner found, by method and
236
+ path shape, so a real call to `POST /appointments/4821/accept` lands on the
237
+ template `/appointments/${data.id}/accept`. From it the console keeps the path
238
+ id, the query params and the request body, which become the endpoint's starting
239
+ values.
240
+
241
+ For a single request, **Copy as cURL** in DevTools and paste it into the import
242
+ dialog.
243
+
244
+ The import reports what it could not place — usually an endpoint the backend has
245
+ but the frontend does not call yet.
246
+
247
+ ## Export
248
+
249
+ Every module page has **Export HAR** and **Export cURL**; the home screen has
250
+ **Export all captured**.
251
+
252
+ | | |
253
+ | --- | --- |
254
+ | **HAR** | a valid HAR 1.2 file with request, query string, payload and recorded response body. Opens in Chrome DevTools, imports into Postman, and re-imports here. |
255
+ | **cURL** | one runnable command per endpoint, labelled with its source location |
256
+ | **Postman** | a Collection v2.1. Modules become folders; each request carries the real query params and body in the right mode, so a captured form post imports as form-data, not broken JSON. Host and token are left as `{{baseUrl}}` and `{{token}}`. |
257
+
258
+ One entry per endpoint, not per call — the most recent capture wins, which keeps
259
+ an export of a busy session readable.
260
+
261
+ All exports carry `<paste your token>` in place of a real token, so an export is
262
+ safe to attach to a ticket.
263
+
264
+ ## The report
265
+
266
+ **Report** in the header builds an insight report from everything the console
267
+ already holds. No extra capture, no new run.
268
+
269
+ | Section | |
270
+ | --- | --- |
271
+ | **Coverage** | exercised vs total, per module, and which modules nobody has walked |
272
+ | **Failing** | status, body code and the message pulled out of the envelope, with regressions and replay failures marked |
273
+ | **Contract drift** | response shapes that changed, with the field-level diff |
274
+ | **Risks** | writes never exercised, captured DELETEs, 2xx responses carrying an error code, payloads whose secrets were stripped |
275
+ | **Hygiene** | endpoints referenced nowhere, duplicate routes, calls not found in the source |
276
+ | **Auth surface** | which endpoints need which token, and which third-party ones deliberately get none |
277
+ | **Latency** | median, p95 and the slowest endpoints |
278
+ | **Inventory** | request params, body fields and response fields per captured endpoint, from real traffic rather than hand-written docs |
279
+ | **Trend** | pass rate and coverage over time, one line appended per run |
280
+
281
+ **Export Markdown** for a ticket, **Export JSON** for CI. Or from the command
282
+ line:
283
+
284
+ ```bash
285
+ npx api-tracer report --format json --out api-report.json
286
+ ```
287
+
288
+ The latency numbers are one measurement per endpoint, from its most recent call
289
+ — indicative, not a benchmark.
290
+
291
+ ## Signing in
292
+
293
+ Click **Set token** and paste one. The token is held by the server, never sent
294
+ back to the page, and written owner-only (`chmod 600`) to the data directory so
295
+ a restart does not sign you out. **Sign out** deletes it.
296
+
297
+ If your API has a multi-step sign-in worth automating — password, then a
298
+ one-time code, then an organisation — describe it in the config file and the
299
+ console will drive it, so nobody has to paste anything. See
300
+ [sign-in flows](./configuration.md#sign-in-flows).
301
+
302
+ Third-party endpoints (anything flagged absolute) never receive your token.
303
+
304
+ ## Why calls go through the server
305
+
306
+ The page posts the composed request to the console and the server forwards it.
307
+ Calling the API from the page directly would be blocked by CORS, and the token
308
+ would have to sit in client-side JavaScript.
309
+
310
+ ## Getting around
311
+
312
+ - **The logo** goes back to the overview from anywhere
313
+ - **Breadcrumbs** at the top of every page, each part clickable
314
+ - **Escape** backs out one level — endpoint, then module, then overview. Ignored
315
+ while you are typing or a dialog is open.
316
+ - Filters: method, and pass/fail/not-run. Search matches function name, path and
317
+ module.
318
+
319
+ Typed path values and payloads are kept in the browser's local storage, so a
320
+ reload does not lose your work.
321
+
322
+ ## Environment variables
323
+
324
+ | | |
325
+ | --- | --- |
326
+ | `API_TRACER_DATA` | where captures are kept (default `<project>/.api-tracer`) |
327
+ | `PORT` / `HOST` | default `4400` / `127.0.0.1` |
328
+ | `BASE_PATH` | serve under a path, e.g. `/api-console`, behind a proxy |
329
+ | `ALLOWED_ORIGINS` | comma-separated origins allowed to post recordings; localhost always works |
330
+ | `CONSOLE_USER` / `CONSOLE_PASS` | basic auth on everything except `/api/record` |
331
+ | `LOCK_RECORDING=1` | recording cannot be paused from the UI |
332
+ | `LOCK_ENV=<name>` | pins the environment |
333
+
334
+ `HOST` stays on `127.0.0.1` by default, so a laptop instance is not exposed by
335
+ accident.
336
+
337
+ ## Running one for a team
338
+
339
+ Every developer whose app has the tracer posts to the console whether or not
340
+ they ever open it. The captures accumulate: one person exercises appointments,
341
+ another exercises rota, and the console ends up holding both.
342
+
343
+ Two variables stop one person's click changing things for everyone:
344
+
345
+ ```bash
346
+ LOCK_RECORDING=1 LOCK_ENV=dev npx api-tracer serve
347
+ ```
348
+
349
+ - Without `LOCK_RECORDING`, one developer clicking **Live** silently stops
350
+ recording for the whole team.
351
+ - Without `LOCK_ENV`, someone switching to stage redirects everyone's next
352
+ replay and clears the shared results.
353
+
354
+ The console also shows **whose** session the shared token belongs to, since
355
+ everyone replays as that user.
356
+
357
+ `CONSOLE_USER` / `CONSOLE_PASS` turn on basic auth. **`/api/record` stays
358
+ open**, because the app posts recordings from a browser that cannot send those
359
+ credentials. The gate protects reading captures, replaying calls and the stored
360
+ token; it does not stop someone who can reach the host from posting junk
361
+ recordings. If that matters, put the whole thing behind VPN or SSO.
362
+
363
+ It is single-user by design: one token, one environment, one set of results. Two
364
+ people on a shared instance will overwrite each other's environment and results.
365
+
366
+ ### Behind a proxy
367
+
368
+ The mount path is forwarded as-is, so tell the console where it lives:
369
+
370
+ ```bash
371
+ BASE_PATH=/api-console HOST=0.0.0.0 ALLOWED_ORIGINS=https://app.example.com npx api-tracer serve
372
+ ```
373
+
374
+ Then point the tracer at the same path:
375
+
376
+ ```ts
377
+ apiTracer.init({ reportTo: '/api-console' });
378
+ ```
379
+
380
+ Same origin means no CORS and no mixed content.
381
+
382
+ > **Mixed content.** An HTTPS app cannot post to an `http://` console; the
383
+ > browser blocks it silently. Terminate TLS in front of the console, or serve it
384
+ > under a path on the app's own origin.
385
+
386
+ ## HTTP API
387
+
388
+ Everything the UI does is available directly.
389
+
390
+ | Route | |
391
+ | --- | --- |
392
+ | `GET /api/endpoints` | catalog, base URLs, auth info, results, samples, live state |
393
+ | `POST /api/token` | store or clear a token |
394
+ | `GET/POST /api/login` | the configured sign-in flow: the next step, and submitting it |
395
+ | `POST /api/env` | switch environment |
396
+ | `POST /api/call` | send one endpoint |
397
+ | `POST /api/run` | bulk run; GETs only unless explicitly allowed |
398
+ | `POST /api/run/preview` | what a run would do |
399
+ | `GET /api/run/status`, `POST /api/run/cancel` | progress and cancellation |
400
+ | `POST /api/record` | one live call from the running app |
401
+ | `GET/POST/DELETE /api/live` | recorder state: counts, pause, reset |
402
+ | `GET/POST/DELETE /api/contracts` | response-shape baselines; POST accepts a drift |
403
+ | `GET /api/export` | `?module=&format=har\|curl\|postman` |
404
+ | `POST /api/import` | import a HAR or a cURL command |
405
+ | `DELETE /api/samples` | drop all captured samples |
406
+ | `DELETE /api/uncatalogued` | drop adopted endpoints |
407
+ | `GET/POST /api/variables` | read or set `{{variables}}` |
408
+ | `DELETE /api/results` | clear saved results |
409
+ | `GET /api/report`, `GET /api/report/export` | the report model, and `?format=md\|json` |
410
+
411
+ ## What lives in the data directory
412
+
413
+ `<project>/.api-tracer/`, or wherever `--data` points.
414
+
415
+ | | |
416
+ | --- | --- |
417
+ | `endpoints.json` | the catalog, written by `scan` |
418
+ | `samples.json` | requests captured from real traffic |
419
+ | `results.json` | the last result per endpoint |
420
+ | `contracts.json` | response-shape baselines |
421
+ | `uncatalogued.json` | endpoints adopted from traffic |
422
+ | `variables.json` | `{{variables}}` |
423
+ | `lastrun.json`, `runs.jsonl` | the last run, and one line per run for the trend |
424
+ | `tokens.json` | the session token, written `chmod 600` |
425
+
426
+ **Add `.api-tracer/` to your `.gitignore`.** It contains real request and
427
+ response bodies from the environment you are watching, and a real token. See
428
+ [Security](./security.md).