Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions src/lib/redact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const REDACTED_SECRET = "[REDACTED]";
const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn)$/i;

const SECRET_VALUE_PATTERNS: Array<[RegExp, string]> = [
[/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/gi, `Bearer ${REDACTED_SECRET}`],
[/\b(Bearer)(\s+)[A-Za-z0-9._~+/=-]{8,}\b/gi, `$1$2${REDACTED_SECRET}`],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- src/lib/redact.ts ---'
cat -n src/lib/redact.ts

printf '%s\n' '--- relevant tests ---'
cat -n tests/redact.test.ts | sed -n '1,155p'

printf '%s\n' '--- standalone regex behavior probe ---'
python3 - <<'PY'
import re

bearer = re.compile(r'\b(Bearer)(\s+)[A-Za-z0-9._~+/=-]{8,}\b', re.I)
redacted = '[REDACTED]'

def bearer_pass(value):
    first = bearer.sub(lambda m: f'{m.group(1)}{m.group(2)}{redacted}', value)
    # Model the colon-labelled skip described in the review: preserve the
    # whole value when it starts with Bearer.
    m = re.match(r'(?i)([^:\r\n]+):([ \t]*)(Bearer\b.*)$', value)
    return value if m else first

cases = [
    'Authorization: Bearer abc',
    'Authorization: Bearer abcdefgh',
    'Authorization: Bearer abcdefgh=',
    'Authorization: Bearer "abcdefgh"',
    'Authorization: Bearer abcdefgh,',
]
for case in cases:
    print(case, '=>', bearer_pass(case))
PY

Repository: lidge-jun/opencodex

Length of output: 15775


🏁 Script executed:

#!/bin/bash
set -eu

cat -n src/lib/redact.ts
cat -n tests/redact.test.ts | sed -n '1,155p'

python3 - <<'PY'
import re

pattern = re.compile(r'\b(Bearer)(\s+)[A-Za-z0-9._~+/=-]{8,}\b', re.I)

for value in [
    'Authorization: Bearer abc',
    'Authorization: Bearer abcdefgh',
    'Authorization: Bearer abcdefgh=',
    'Authorization: Bearer "abcdefgh"',
]:
    print(value, '=>', pattern.sub(r'\1\2[REDACTED]', value))
PY

Repository: lidge-jun/opencodex

Length of output: 15623


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const REDACTED = "[REDACTED]";

const current = [
  [/\b(Bearer)(\s+)[A-Za-z0-9._~+/=-]{8,}\b/gi, "$1$2" + REDACTED],
  [/\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[-_]?token|accessToken|refresh[-_]?token|refreshToken|id[-_]?token|client[-_]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token)\s*:)(?![^\S\r\n]*(?:Bearer\b|\[REDACTED\]|\r?\n|$))([^\S\r\n]*)[^\r\n]+/gi, "$1$2" + REDACTED],
];

const proposed = [
  [/\b(Bearer)(\s+)[A-Za-z0-9._~+/=-]+/gi, "$1$2" + REDACTED],
  [/\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[-_]?token|accessToken|refresh[-_]?token|refreshToken|id[-_]?token|client[-_]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token)\s*:)(?![^\S\r\n]+(?:Bearer\b[^\S\r\n]+\[REDACTED\]|\[REDACTED\]|\r?\n|$))([^\S\r\n]*)[^\r\n]+/gi, "$1$2" + REDACTED],
];

function apply(rules, input) {
  return rules.reduce((value, [pattern, replacement]) =>
    value.replace(pattern, replacement), input);
}

const cases = [
  "Authorization: Bearer abc",
  "Authorization: Bearer abcdefgh12345678",
  "Authorization: Bearer abcdefgh=",
  'Authorization: Bearer "abcdefgh"',
  "Authorization: Bearer abcdefgh,",
  "Set-Cookie2: session=secret",
];

for (const input of cases) {
  console.log(JSON.stringify({input, current: apply(current, input), proposed: apply(proposed, input)}));
}
JS

Repository: lidge-jun/opencodex

Length of output: 948


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const REDACTED = "[REDACTED]";
const pattern = /\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[-_]?token|accessToken|refresh[-_]?token|refreshToken|id[-_]?token|client[-_]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token)\s*:)(?![^\S\r\n]*(?:Bearer\b|\[REDACTED\]|\r?\n|$))([^\S\r\n]*)[^\r\n]+/gi;

for (const input of [
  "Authorization: Bearer abc",
  "Authorization: Bearer abcdefgh=",
  'Authorization: Bearer "abcdefgh"',
  "Set-Cookie2: session=secret",
]) {
  const bearer = input.replace(
    /\b(Bearer)(\s+)[A-Za-z0-9._~+/=-]{8,}\b/gi,
    "$1$2[REDACTED]",
  );
  console.log(JSON.stringify({
    input,
    afterBearerRule: bearer,
    final: bearer.replace(pattern, "$1$2[REDACTED]"),
  }));
}
JS

Repository: lidge-jun/opencodex

Length of output: 692


Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  src/server/responses-snapshot-repair.ts:45
  isPlainObject
│
▼
● Sink
  src/lib/redact.ts

Complete Bearer and Set-Cookie2 redaction.

  • In src/lib/redact.ts:6, remove {8,} and the trailing \b. The current rule leaks short tokens, quoted values, and padding such as =.
  • In src/lib/redact.ts:39, skip only Bearer [REDACTED], and add set-cookie2 to the key alternatives. Add regression tests for short, quoted, padded Bearer values and Set-Cookie2 lines.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/redact.ts` at line 6, Update the Bearer redaction pattern in
redact.ts to match all token lengths and terminate without the trailing word
boundary, covering short, quoted, and padding-character values. In the redaction
logic near the existing Bearer skip condition, skip only the exact Bearer
[REDACTED] form and include set-cookie2 among the recognized key alternatives.
Add regression coverage for short, quoted, and padded Bearer values plus
Set-Cookie2 lines.

Source: Path instructions

[/\b(sk-[A-Za-z0-9][A-Za-z0-9._-]{6,})\b/g, REDACTED_SECRET],
// GitHub tokens (classic + fine-grained + OAuth/refresh): ghp_/gho_/ghu_/ghs_/ghr_/github_pat_.
[/\b(gh[pousr]_[A-Za-z0-9_]{8,}|github_pat_[A-Za-z0-9_]{20,})\b/g, REDACTED_SECRET],
Expand All @@ -13,12 +13,30 @@ const SECRET_VALUE_PATTERNS: Array<[RegExp, string]> = [
[/\btid=[A-Za-z0-9-]+(?:;[A-Za-z0-9_.-]+=[^;\s"']*)+(?::[A-Za-z0-9+/=_-]+)?/g, REDACTED_SECRET],
[/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)=)([^&\s"',;]+)/gi, `$1${REDACTED_SECRET}`],
// Colon-labelled credentials. Upstream error bodies quote the offending header
// or field back at us ("x-api-key: abc…"), and the `=` rule above never fires
// for that shape, so the credential survived into client-visible error text.
// Header-style names are included because that is exactly what a provider
// echoes when it rejects a request. A `Bearer <token>` value is left to the
// dedicated rule above so its scheme prefix stays readable in diagnostics.
[/\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|password|secret|token)\s*:\s*)(?!\s)(?!Bearer\b)([^\s"',;]+)/gi, `$1${REDACTED_SECRET}`],
// or field back at us ("x-api-key: abc…"), and the `=` rules never fire for
// that shape, so the credential survived into client-visible error text.
//
// The value class deliberately runs to end-of-line rather than stopping at a
// quote, space, or semicolon. A first attempt tokenized on those characters
// and leaked every delimiter-bearing variant: `x-api-key: "quoted…"` kept the
// whole quoted secret, `Authorization: Basic dXNlcjpwYXNz` kept the payload
// after the scheme, and `Cookie: a=1; b=2` kept everything after the first
// `;`. A credential header's value IS the rest of the line, so that is what
// gets masked.
//
// `Bearer` is the one readable exception, and it is handled by the dedicated
// Bearer rule ABOVE rather than here: an auth scheme is diagnostically useful,
// and its token is a single opaque word, so consuming the rest of the line
// there would swallow trailing diagnostics that follow a quoted header in
// prose (`… Authorization: Bearer <tok> at /path/file.json`). Every other
// scheme (Basic, Digest, …) carries its credential as the payload, so those
// are masked whole by this rule.
//
// The rules run in order, so by the time this one fires the Bearer rule has
// already replaced `Bearer <tok>` with `Bearer [REDACTED]`. Skipping a value
// that is already redacted keeps this rule from eating that result — and from
// eating the trailing diagnostics after it.
[/\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token)\s*:)(?![^\S\r\n]*(?:Bearer\b|\[REDACTED\]|\r?\n|$))([^\S\r\n]*)[^\r\n]+/gi, `$1$2${REDACTED_SECRET}`],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- src/lib/redact.ts ---'
sed -n '1,180p' src/lib/redact.ts

printf '%s\n' '--- relevant redact tests ---'
sed -n '1,155p' tests/redact.test.ts

printf '%s\n' '--- focused behavioral probe of the redaction rules ---'
python3 - <<'PY'
from pathlib import Path
import re

text = Path("src/lib/redact.ts").read_text()
line = next(
    line.strip()
    for line in text.splitlines()
    if "x-api-key|x-goog-api-key" in line and "set-cookie" in line
)
pattern_text = line.split("[/", 1)[1].split("/gi", 1)[0]
pattern = re.compile(pattern_text, re.IGNORECASE)
replacement = r"\1\2[REDACTED]"

for value in [
    "Set-Cookie: session=secret",
    "Set-Cookie2: session=secret",
    "set-cookie2: session=secret\nstatus: 429",
]:
    print(repr(value), "=>", repr(pattern.sub(replacement, value)))
PY

Repository: lidge-jun/opencodex

Length of output: 13590


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- src/lib/redact.ts ---'
sed -n '1,180p' src/lib/redact.ts

printf '%s\n' '--- relevant redact tests ---'
sed -n '1,155p' tests/redact.test.ts

printf '%s\n' '--- focused behavioral probe ---'
python3 - <<'PY'
from pathlib import Path
import re

text = Path("src/lib/redact.ts").read_text()
line = next(line.strip() for line in text.splitlines()
            if "x-api-key|x-goog-api-key" in line and "set-cookie" in line)
pattern_text = line.split("[/", 1)[1].split("/gi", 1)[0]
pattern = re.compile(pattern_text, re.IGNORECASE)

for value in [
    "Set-Cookie: session=secret",
    "Set-Cookie2: session=secret",
]:
    print(repr(value), "=>", repr(pattern.sub(r"\1\2[REDACTED]", value)))
PY

Repository: lidge-jun/opencodex

Length of output: 13566


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const REDACTED_SECRET = "[REDACTED]";
const patterns = [
  [/\b(Bearer)(\s+)[A-Za-z0-9._~+/=-]{8,}\b/gi, `$1$2${REDACTED_SECRET}`],
  [/\b(sk-[A-Za-z0-9][A-Za-z0-9._-]{6,})\b/g, REDACTED_SECRET],
  [/\b(gh[pousr]_[A-Za-z0-9_]{8,}|github_pat_[A-Za-z0-9_]{20,})\b/g, REDACTED_SECRET],
  [/\btid=[A-Za-z0-9-]+(?:;[A-Za-z0-9_.-]+=[^;\s"']*)+(?::[A-Za-z0-9+/=_-]+)?/g, REDACTED_SECRET],
  [/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)=)([^&\s"',;]+)/gi, `$1${REDACTED_SECRET}`],
  [/\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token)\s*:)(?![^\S\r\n]*(?:Bearer\b|\[REDACTED\]|\r?\n|$))([^\S\r\n]*)[^\r\n]+/gi, `$1$2${REDACTED_SECRET}`],
  [/(("token"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$4`],
];

function redact(value) {
  return patterns.reduce((result, [pattern, replacement]) =>
    result.replace(pattern, replacement), value);
}

for (const input of [
  "Set-Cookie: session=secret",
  "Set-Cookie2: session=secret",
  "set-cookie2: session=secret\nstatus: 429",
]) {
  const output = redact(input);
  console.log(JSON.stringify({ input, output, leaked: output.includes("session=secret") }));
}
JS

Repository: lidge-jun/opencodex

Length of output: 457


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const REDACTED_SECRET = "[REDACTED]";
const pattern = /\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token)\s*:)(?![^\S\r\n]*(?:Bearer\b|\[REDACTED\]|\r?\n|$))([^\S\r\n]*)[^\r\n]+/gi;

for (const input of [
  "Set-Cookie: session=secret",
  "Set-Cookie2: session=secret",
  "set-cookie2: session=secret\nstatus: 429",
]) {
  const output = input.replace(pattern, `$1$2${REDACTED_SECRET}`);
  console.log(JSON.stringify({ input, output, leaked: output.includes("session=secret") }));
}
JS

Repository: lidge-jun/opencodex

Length of output: 457


Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor

Reachability: External

Reachability path
● Entry
  src/server/responses-snapshot-repair.ts:45
  isPlainObject
│
▼
● Sink
  src/lib/redact.ts

Add set-cookie2 to the colon-labelled credential rule.

redactSecretString("Set-Cookie2: session=secret") currently returns the secret unchanged. Add set-cookie2 to the alternative list in src/lib/redact.ts:39 and add a regression assertion in tests/redact.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/redact.ts` at line 39, Add set-cookie2 to the colon-labelled
credential alternative list in the redaction regex used by redactSecretString,
preserving the existing case-insensitive matching and replacement behavior. Add
a regression assertion in the redactSecretString tests confirming that
“Set-Cookie2: session=secret” is redacted.

Source: Path instructions

[/((?:"(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$3`],
// Raw JSON "token" field values (Copilot token exchange bodies echo the credential here).
[/(("token"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$4`],
Expand Down
20 changes: 20 additions & 0 deletions src/server/responses-snapshot-repair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,18 @@ export function createResponsesSnapshotBlockRewrite(
&& isPlainObject(event.part)) {
if (outputIndex !== undefined) {
const open = openItems.get(outputIndex);
// A PRESENT-but-mismatched item_id is contradictory lifecycle evidence,
// exactly like output_item.done and output_text.done: the stream is
// telling us our identity model for this index is wrong. Merely
// ignoring it left the item open and let the terminal fabricate a full
// closure sequence (content_part.added → output_text.done →
// content_part.done → output_item.done) on top of a stream we do not
// understand. Go fail-closed instead. An OMITTED item_id stays
// legitimate and is still correlated by output_index.
if (open && itemId !== undefined && itemId !== open.itemId) {
taintAndRelease();
return [changed ? jsonBlock(nextEvent) : block];
Comment on lines +379 to +381

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check content-part identity before validating part

When a sparse/malformed provider sends response.content_part.done or .added with a wrong item_id but omits or mangles part, this new mismatch check never runs because the whole block is gated by isPlainObject(event.part). The tracked item then stays open, so a later valid delta plus response.completed can still synthesize the closure sequence this patch is trying to suppress; move the identity check ahead of the part structural check or taint malformed content-part events that carry a foreign item_id.

Useful? React with 👍 / 👎.

}
// Correlate by item_id when present: a mismatched event must not
// mutate (or suppress injections for) the tracked item (#893 review).
if (open && (itemId === undefined || itemId === open.itemId)) {
Expand Down Expand Up @@ -398,6 +410,14 @@ export function createResponsesSnapshotBlockRewrite(
}
if (type === "response.output_text.delta" && typeof event.delta === "string" && outputIndex !== undefined) {
const open = openItems.get(outputIndex);
// Same identity contract as the *.done terminals: a present-but-foreign
// item_id on a tracked index means our model of this index is wrong, and
// reconstructing from the text we DID accept would ship a message the
// upstream never assembled that way.
if (open && itemId !== undefined && itemId !== open.itemId) {
taintAndRelease();
return [changed ? jsonBlock(nextEvent) : block];
Comment on lines +417 to +419

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Taint foreign deltas even when the delta is malformed

This foreign-item_id guard only exists inside the typeof event.delta === "string" branch, so a malformed response.output_text.delta with the tracked output_index but a different item_id leaves the original item open. A later completed terminal can still synthesize output_text.done/output_item.done for that item, even though the stream already contradicted its identity; perform the mismatch check for any response.output_text.delta with an output_index before validating the delta payload.

Useful? React with 👍 / 👎.

}
if (open && (itemId === undefined || itemId === open.itemId)) {
const deltaBytes = Buffer.byteLength(event.delta, "utf8");
open.text += event.delta;
Expand Down
24 changes: 24 additions & 0 deletions tests/redact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,30 @@ describe("redactSecretString", () => {
expect(redactSecretString("model: gpt-5.5\nstatus: 429\nrequest: ocx-abc123"))
.toBe("model: gpt-5.5\nstatus: 429\nrequest: ocx-abc123");
});

test("masks the WHOLE colon-labelled value, including delimiter-bearing forms", () => {
// Re-review of the first fix: tokenizing the value on quotes, spaces, and
// semicolons leaked every variant that contains one. A credential header's
// value is the rest of the line, so that is what must be masked.
expect(redactSecretString('x-api-key: "quotedcredential123456"'))
.toBe(`x-api-key: ${REDACTED_SECRET}`);
expect(redactSecretString("Authorization: Basic dXNlcjpwYXNz"))
.toBe(`Authorization: ${REDACTED_SECRET}`);
expect(redactSecretString("Cookie: session=secret-one; csrf=secret-two"))
.toBe(`Cookie: ${REDACTED_SECRET}`);
});

test("keeps the Bearer scheme readable while masking its token", () => {
// An auth scheme is diagnostically useful; the credential after it is not.
expect(redactSecretString("Authorization: Bearer abcdefgh12345678"))
.toBe(`Authorization: Bearer ${REDACTED_SECRET}`);
});

test("masks each credential line independently without eating the next", () => {
// End-of-line, not end-of-string: a multi-line error body must not collapse.
expect(redactSecretString("x-api-key: one-secret\nmodel: gpt-5.5\ncookie: two=secret"))
.toBe(`x-api-key: ${REDACTED_SECRET}\nmodel: gpt-5.5\ncookie: ${REDACTED_SECRET}`);
});
});

describe("redactSecrets", () => {
Expand Down
55 changes: 55 additions & 0 deletions tests/responses-snapshot-repair.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,61 @@ describe("createResponsesSnapshotBlockRewrite", () => {
expect(Object.hasOwn(terminal.response as Record<string, unknown>, "output")).toBe(false);
});

test("a mismatched item_id on content_part.done also goes fail-closed", () => {
// Re-review: fixing output_text.done alone left the sibling terminal open.
// A foreign content_part.done was ignored, the item stayed open, and the
// completed terminal fabricated the whole closure sequence
// (content_part.added → output_text.done → content_part.done →
// output_item.done) on a stream whose identity model was already wrong.
const rewrite = createResponsesSnapshotBlockRewrite();
rewrite(dataBlock(ISSUE_FIXTURE.itemAdded));
rewrite(dataBlock({
type: "response.content_part.done",
item_id: "msg_OTHER",
output_index: 0,
part: { type: "output_text", text: "foreign" },
}));
rewrite(dataBlock(ISSUE_FIXTURE.delta));
const out = rewrite(dataBlock(ISSUE_FIXTURE.completed));
expect(typesOf(out)).not.toContain("response.content_part.added");
expect(typesOf(out)).not.toContain("response.content_part.done");
expect(typesOf(out)).not.toContain("response.output_text.done");
expect(typesOf(out)).not.toContain("response.output_item.done");
const terminal = eventsOf(out).find(event => event.type === "response.completed")!;
expect(Object.hasOwn(terminal.response as Record<string, unknown>, "output")).toBe(false);
});

test("a mismatched item_id on a text delta goes fail-closed instead of being dropped", () => {
// Reconstructing from only the deltas we accepted would ship a message the
// upstream never assembled that way.
const rewrite = createResponsesSnapshotBlockRewrite();
rewrite(dataBlock(ISSUE_FIXTURE.itemAdded));
rewrite(dataBlock({
type: "response.output_text.delta",
item_id: "msg_OTHER",
output_index: 0,
delta: "foreign",
}));
const out = rewrite(dataBlock(ISSUE_FIXTURE.completed));
expect(typesOf(out)).not.toContain("response.output_item.done");
const terminal = eventsOf(out).find(event => event.type === "response.completed")!;
expect(Object.hasOwn(terminal.response as Record<string, unknown>, "output")).toBe(false);
});

test("an omitted item_id stays legitimate on every correlated event", () => {
// The taint must fire on a PRESENT-but-wrong id only. A gateway that omits
// item_id is common and correlates by output_index alone; breaking that
// would fail closed on healthy streams.
const rewrite = createResponsesSnapshotBlockRewrite();
rewrite(dataBlock(ISSUE_FIXTURE.itemAdded));
rewrite(dataBlock({ type: "response.content_part.added", output_index: 0, part: { type: "output_text", text: "" } }));
rewrite(dataBlock({ type: "response.output_text.delta", output_index: 0, delta: "hello" }));
const out = rewrite(dataBlock(ISSUE_FIXTURE.completed));
expect(typesOf(out)).toContain("response.output_item.done");
const injected = eventsOf(out).filter(event => event.type === "response.output_text.done");
expect(injected.some(event => event.text === "hello")).toBe(true);
});

test("a completed terminal with absent output and zero items gets the canonical empty list", () => {
const rewrite = createResponsesSnapshotBlockRewrite();
const out = rewrite(dataBlock({ type: "response.completed", response: { id: "r" } }));
Expand Down
Loading