har2sdk 0.6.3 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.4
4
+
5
+ A privacy fix in the mock's fake data, an Auth section in the README, and tests that pin both.
6
+
7
+ **Security**
8
+ - When a fake drawn for a recorded value happened to equal it (about 1 in 26³ for a three-letter name), the fallback reversed it: the mock served `"adA"` for `"Ada"`, `"7x"` for `"x7"`, the recorded value backwards and in the wrong shape. A collision now draws again from the same hash stream, so the fake never derives from the recorded value; a last-resort bump of one character remains for the case where 9 draws in a row all return it. Outputs without a collision are unchanged. Found by a CI failure in `review.test.ts`; `test/scrubber.test.ts` has the seeds that reproduce it and a generated run over 20,000 seeds (#4).
9
+
10
+ **Docs**
11
+ - README: an Auth section. The recorded token never reaches `client.ts`; which auth options a client gets; the token is sent with the scheme the app used (bare if it was sent bare, `Bearer` if no auth was seen); a token function is called before every request; a 401 throws `ApiError` without retry or refresh, with a refresh recipe; the mock doesn't check auth (#3).
12
+
13
+ **Tests**
14
+ - `test/auth.test.ts` pins every claim in the Auth section and runs its refresh recipe as written. `test/nullability.test.ts` records one field as a string, as `null` and left out, in every order, and checks `client.ts`, `openapi.json`, typechecking under `exactOptionalPropertyTypes` and the mock (#3).
15
+
3
16
  ## 0.6.3
4
17
 
5
18
  Fixes from an eighth review, the last audit before publishing. Every finding has a test in `test/review8.test.ts` (written first and seen failing), and the second suite of the generated privacy test puts the person's name in a GraphQL comment and in a free-text `operationName`.
package/README.md CHANGED
@@ -76,6 +76,40 @@ Needs Node 20+. Reads standard HAR 1.2: tested with Chrome-style exports and Pla
76
76
 
77
77
  The generated `client.ts` has **zero dependencies**, uses the global `fetch` (Node 20+ and browsers), and compiles under `strict`, `exactOptionalPropertyTypes` and `erasableSyntaxOnly`. Non-2xx responses throw `ApiError` with the parsed body.
78
78
 
79
+ ## Auth
80
+
81
+ The capture tells har2sdk *how* the app authenticated, never *with what*. The token, cookie or key you recorded is not copied into `client.ts` (if it were, the [leak guard](#privacy) would refuse to write the file). You pass your own:
82
+
83
+ ```ts
84
+ new AcmeShopClient({ token: process.env.TOKEN }); // a string
85
+ new AcmeShopClient({ token: () => getToken() }); // or a function, sync or async
86
+ ```
87
+
88
+ - **Which options you get** depends on what the capture used: `token` (sent in `Authorization` with the scheme the app used, e.g. `Bearer …` or `Token …`, or bare if the app sent it bare; `Bearer` if no auth was seen), `basicAuth`, `apiKey` (in the header or query parameter the app used) and `cookie`. If no auth was seen, you get `token`.
89
+ - **A token function is called before every request**, so it can hand out a fresh token each time. For anything else computed per request (a CSRF header, a signature), pass `headers: async () => ({ … })`.
90
+ - **Refreshing is up to you.** On a `401` the client doesn't retry or refresh: it throws `ApiError` with `status`, the parsed `body` and the `operation` that failed. One way to handle it:
91
+
92
+ ```ts
93
+ import { AcmeShopClient, ApiError } from "./har2sdk-out/client.js";
94
+
95
+ let token = await login();
96
+ const shop = new AcmeShopClient({ token: () => token });
97
+
98
+ async function withRefresh<T>(call: () => Promise<T>): Promise<T> {
99
+ try {
100
+ return await call();
101
+ } catch (err) {
102
+ if (!(err instanceof ApiError) || err.status !== 401) throw err;
103
+ token = await refresh();
104
+ return call();
105
+ }
106
+ }
107
+
108
+ const me = await withRefresh(() => shop.getMe());
109
+ ```
110
+
111
+ - **The mock doesn't check auth.** Any token, or none, gets the same response.
112
+
79
113
  ## Privacy
80
114
 
81
115
  HAR files contain live session cookies, tokens and often other people's data. har2sdk:
package/dist/codegen.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * runtime dependencies (uses the global `fetch`, Node 18+ or any browser).
4
4
  */
5
5
  import type { Plan, PlannedOp } from "./plan.js";
6
- export declare const VERSION = "0.6.3";
6
+ export declare const VERSION = "0.6.4";
7
7
  export declare function generateTypeScript(plan: Plan): string;
8
8
  /** Just the type declarations (used for prompts and MCP responses). */
9
9
  export declare function generateTypesOnly(plan: Plan, onlyOps?: PlannedOp[]): string;
package/dist/codegen.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { printTs, printTsInterface } from "./types.js";
2
2
  import { propertyKey } from "./words.js";
3
- export const VERSION = "0.6.3";
3
+ export const VERSION = "0.6.4";
4
4
  export function generateTypeScript(plan) {
5
5
  const { model, clientName, ops } = plan;
6
6
  const out = [];
package/dist/redact.js CHANGED
@@ -576,7 +576,9 @@ export class Scrubber {
576
576
  h ^= h << 5;
577
577
  return h >>> 0;
578
578
  };
579
- const body = [...v.slice(prefix.length)].map((ch, i) => {
579
+ const rest = v.slice(prefix.length);
580
+ const draw = () => [...rest]
581
+ .map((ch, i) => {
580
582
  if (/[0-9]/.test(ch))
581
583
  return String((next() % 9) + (i === 0 ? 1 : 0));
582
584
  if (/[a-f]/.test(ch) && /^[0-9a-f-]+$/i.test(v))
@@ -586,13 +588,19 @@ export class Scrubber {
586
588
  if (/[A-Z]/.test(ch))
587
589
  return String.fromCharCode(65 + (next() % 26));
588
590
  return ch;
589
- });
590
- f = prefix + body.join("");
591
- if (f === v)
592
- f = prefix + body.reverse().join("");
593
- // Still the recorded value (a palindrome like "66"): bump the first digit or letter.
594
- if (f === v)
595
- f = prefix + body.join("").replace(/[0-9a-zA-Z]/, (c) => (/\d/.test(c) ? String((Number(c) + 1) % 10) : c === "z" ? "a" : c === "Z" ? "A" : String.fromCharCode(c.charCodeAt(0) + 1)));
591
+ })
592
+ .join("");
593
+ // Drew the recorded value itself: draw again from the same stream. Never derive the
594
+ // fake from the recorded value (reversing it kept it readable and broke its shape).
595
+ let body = draw();
596
+ for (let tries = 0; body === rest && tries < 8; tries++)
597
+ body = draw();
598
+ // Still the recorded value (only separators, or a very unlucky run): bump the first digit or letter.
599
+ // This bump is still derived from the recorded value, but it only runs after 9 draws in a row
600
+ // all returned it, which is next to impossible for any value with a digit or letter in it.
601
+ if (body === rest)
602
+ body = body.replace(/[0-9a-zA-Z]/, (c) => (/\d/.test(c) ? String((Number(c) + 1) % 10) : c === "z" ? "a" : c === "Z" ? "A" : String.fromCharCode(c.charCodeAt(0) + 1)));
603
+ f = prefix + body;
596
604
  this.fakes.set(v, f);
597
605
  return f;
598
606
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "har2sdk",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Turn browser network traffic (HAR) into a typed TypeScript SDK, an OpenAPI 3.1 spec and a mock server. Local, deterministic, no API key. Ships an MCP server so Claude can name and document every endpoint.",
5
5
  "keywords": [
6
6
  "har",