docs(core): clarify redaction scope + add nested request-body test
Address round-1 review feedback on fetchTracking redaction: - Cross-reference REDACTED_VALUE / REDACTED_HEADER_VALUE so a reader understands why two identical "[REDACTED]" constants coexist. - Document that redactBody deliberately scopes to form-encoded + JSON (multipart/binary bodies pass through; OAuth never uses multipart). - Add a tracker-path test asserting a nested sensitive key in a JSON request body is redacted in the recorded copy while the live outbound body stays byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
parent
524bf2d543
commit
775b98a019
2 changed files with 40 additions and 1 deletions
|
|
@ -477,6 +477,35 @@ describe("createFetchTracker", () => {
|
|||
expect(bodies[0]!.body).toBe('{"tools":[]}');
|
||||
});
|
||||
|
||||
it("redacts a nested sensitive key in a JSON request body through the tracker", async () => {
|
||||
let outboundInit: RequestInit | undefined;
|
||||
const baseFetch = vi.fn(
|
||||
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
outboundInit = init;
|
||||
return new Response("ok");
|
||||
},
|
||||
);
|
||||
const tracked: FetchRequestEntryBase[] = [];
|
||||
const fetcher = createFetchTracker(baseFetch as typeof fetch, {
|
||||
trackRequest: (entry) => tracked.push(entry),
|
||||
});
|
||||
const liveBody = JSON.stringify({
|
||||
params: { arguments: { access_token: "sekret", page: 2 } },
|
||||
});
|
||||
await fetcher("https://example.com/mcp", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: liveBody,
|
||||
});
|
||||
// Recorded copy has the nested secret masked; non-sensitive siblings kept.
|
||||
expect(JSON.parse(tracked[0]!.requestBody!)).toEqual({
|
||||
params: { arguments: { access_token: REDACTED_VALUE, page: 2 } },
|
||||
});
|
||||
expect(JSON.stringify(tracked[0])).not.toContain("sekret");
|
||||
// The live outbound request body is byte-identical (unredacted).
|
||||
expect(outboundInit?.body).toBe(liveBody);
|
||||
});
|
||||
|
||||
it("does not throw on a malformed body — logs it as-is", async () => {
|
||||
const baseFetch = vi.fn(async () => new Response("ok"));
|
||||
const tracked: FetchRequestEntryBase[] = [];
|
||||
|
|
|
|||
|
|
@ -41,7 +41,12 @@ const SENSITIVE_BODY_FIELDS: ReadonlySet<string> = new Set([
|
|||
"token",
|
||||
]);
|
||||
|
||||
/** Placeholder substituted for sensitive body / URL values in recorded entries. */
|
||||
/**
|
||||
* Placeholder substituted for sensitive body / URL values in recorded entries.
|
||||
* Deliberately kept separate from {@link REDACTED_HEADER_VALUE} (even though both
|
||||
* are `"[REDACTED]"` today) so the header and body/URL redaction paths can evolve
|
||||
* their sentinels independently.
|
||||
*/
|
||||
export const REDACTED_VALUE = "[REDACTED]";
|
||||
|
||||
/** Whether `name` (any casing) is a known-sensitive field / query-param name. */
|
||||
|
|
@ -124,6 +129,11 @@ function redactJsonValue(value: unknown): unknown {
|
|||
* values change. Best-effort and never throws: an empty, non-string, or
|
||||
* unparseable body is returned unchanged. Only the recorded copy is redacted;
|
||||
* the live request body is never touched.
|
||||
*
|
||||
* Scope is deliberately limited to `application/x-www-form-urlencoded` and JSON:
|
||||
* these cover the OAuth token flows this redaction targets. `multipart/form-data`
|
||||
* (and other binary/opaque bodies) are passed through verbatim — OAuth never uses
|
||||
* multipart, so the risk is low; revisit if a multipart secret path appears.
|
||||
*/
|
||||
export function redactBody(
|
||||
body: string | undefined,
|
||||
|
|
|
|||
Loading…
Reference in a new issue