fix(redact,responses): close the credential and lifecycle-identity variant gaps - #1038
Conversation
…ixes Re-review of the merged stack found both fixes were correct in shape but too narrow, and named the exact variants that still fail. Redaction: the colon rule tokenized the value on quotes, spaces, and semicolons, so every delimiter-bearing form still leaked — `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 is masked now. `Bearer` stays the readable exception: the scheme word survives and only its token is consumed. Snapshot repair: fail-closed on a contradictory item_id was applied to output_text.done but not to its siblings. A foreign content_part.done was ignored, the item stayed open, and the terminal fabricated the entire closure sequence anyway — the same failure family, one event over. The same identity contract now covers content_part.added/done and output_text.delta. An OMITTED item_id remains legitimate and is still correlated by output_index, which is asserted so the guard cannot regress into failing closed on healthy streams.
📝 WalkthroughWalkthroughThe changes improve credential redaction and response snapshot repair. Redaction preserves Bearer formatting and masks complete credential lines. Snapshot repair rejects mismatched ChangesCredential Redaction
Response Snapshot Repair
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Follow-up within this PR. Masking the whole colon-labelled value also ate the Bearer form, which broke a Vertex diagnostic: the error text quotes a header in prose (`… Authorization: Bearer <tok> at /path/file.json`) and the trailing path marker disappeared with the token. Bearer is now excluded from the colon rule by a lookahead placed BEFORE the whitespace is consumed — a greedy whitespace class inside the capture moved the lookahead's evaluation point past the scheme word, which is why the first two attempts silently kept matching. The dedicated Bearer rule handles that case and now preserves the space it matched, so `Authorization:` keeps its separator.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 30360ea60a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (open && itemId !== undefined && itemId !== open.itemId) { | ||
| taintAndRelease(); | ||
| return [changed ? jsonBlock(nextEvent) : block]; |
There was a problem hiding this comment.
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 👍 / 👎.
| // useful and the dedicated rule above already masks its token, so the scheme | ||
| // word is preserved and only what follows is consumed here. Other schemes | ||
| // (Basic, Digest, …) are masked whole, since their payload is the credential. | ||
| [/\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*(?:Bearer\s+)?)(?!\s*$)[^\r\n]+/gi, `$1${REDACTED_SECRET}`], |
There was a problem hiding this comment.
Preserve trailing JSON after redacting colon labels
When redactSecretString is given raw JSON/error text containing a colon-labelled credential inside a string, this end-of-line match consumes the closing quote and the rest of the JSON line; for example {"error":{"message":"x-api-key: secret"}} becomes an unterminated {"error":{"message":"x-api-key: [REDACTED]. Several callers redact raw upstream error text before returning diagnostics, so this can mangle otherwise parseable provider payloads; keep the whole-header behavior only for actual header lines or preserve trailing JSON punctuation.
Useful? React with 👍 / 👎.
| if (open && itemId !== undefined && itemId !== open.itemId) { | ||
| taintAndRelease(); | ||
| return [changed ? jsonBlock(nextEvent) : block]; |
There was a problem hiding this comment.
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 👍 / 👎.
| // useful and the dedicated rule above already masks its token, so the scheme | ||
| // word is preserved and only what follows is consumed here. Other schemes | ||
| // (Basic, Digest, …) are masked whole, since their payload is the credential. | ||
| [/\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*(?:Bearer\s+)?)(?!\s*$)[^\r\n]+/gi, `$1${REDACTED_SECRET}`], |
There was a problem hiding this comment.
Cover set-cookie2 in colon-labelled redaction
This replacement adds set-cookie to the colon-labelled header list but leaves set-cookie2 out, even though the rest of the redaction module already treats set-cookie2 as sensitive. When an upstream error echoes a Set-Cookie2: session=...; ... line, the delimiter-bearing cookie value still survives into client-visible diagnostics; include set-cookie2 alongside set-cookie or derive this list from the sensitive-header set.
AGENTS.md reference: AGENTS.md:L218-L223
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with 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.
Inline comments:
In `@src/lib/redact.ts`:
- 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.
- 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.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a1cb50a7-ba17-4d2c-bcc3-1fb9ff554b74
📒 Files selected for processing (4)
src/lib/redact.tssrc/server/responses-snapshot-repair.tstests/redact.test.tstests/responses-snapshot-repair.test.ts
|
|
||
| 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}`], |
There was a problem hiding this comment.
🔒 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))
PYRepository: 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))
PYRepository: 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)}));
}
JSRepository: 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]"),
}));
}
JSRepository: 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 onlyBearer [REDACTED], and addset-cookie2to the key alternatives. Add regression tests for short, quoted, padded Bearer values andSet-Cookie2lines.
🤖 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
| // 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}`], |
There was a problem hiding this comment.
🔒 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)))
PYRepository: 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)))
PYRepository: 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") }));
}
JSRepository: 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") }));
}
JSRepository: 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
…isory All thirteen campaign PRs are merged and every merge commit is an ancestor of dev. The two review follow-ups (#1038, #1040) are recorded with the reason they exist, and the one thing deliberately left unfinished — UTS #39 confusable coverage — points at the draft security advisory rather than a public issue, since it describes a redaction weakness and not a shipped fix.
Re-review of lidge-jun#1038. Exempting the bare scheme word from the colon rule meant a credential only had to be prefixed with `Bearer` to pass through untouched, because the dedicated Bearer rule matches a single opaque `[A-Za-z0-9._~+/=-]{8,}` token and nothing else. Anything it could not parse survived: - `x-api-key: Bearer "smuggledcredential123456"` (quoted) - `Authorization: Bearer custom:credential123456` (punctuation) - `x-api-key: Bearer short` (under the length floor) All three were reachable through formatErrorBody on the sidecar bridge. The exemption now matches only the SANITIZED result — `Bearer [REDACTED]` — so a value the Bearer rule could not sanitize is masked whole by the colon rule. The readable case is unchanged: `Authorization: Bearer <token>` still renders as `Bearer [REDACTED]` with trailing diagnostics intact.
Follow-up to the bug-stack campaign. An adversarial re-review of the merged stack found that two of the fixes in it were right in shape but too narrow, and named the exact variants that still fail. This closes both.
Redaction: the colon rule stopped at the first delimiter
redactSecretStringgained a colon-labelled rule in #1020 so that an upstream error body quotingx-api-key: <value>back at the caller would not leak the credential. Its value class excluded quotes and stopped at whitespace and semicolons, which left every delimiter-bearing form intact. Probed against the merged tip:x-api-key: "quotedcredential123456"Authorization: Basic dXNlcjpwYXNzAuthorization: [REDACTED] dXNlcjpwYXNzCookie: session=secret-one; csrf=secret-twoCookie: [REDACTED]; csrf=secret-twoformatAnthropicErrorBodyreturns this text to a client, so those suffixes were reachable.A credential header's value is the rest of the line, so that is what gets masked now.
Bearerremains the one readable exception — an auth scheme is diagnostically useful and its token is already masked by the dedicated rule, so the scheme word survives and only what follows is consumed. Other schemes are masked whole, since their payload is the credential.Ordinary diagnostics are unaffected:
model: gpt-5.5,status: 429,request: ocx-abc123pass through unchanged, and a multi-line body masks each credential line independently without collapsing the lines between them.Snapshot repair: fail-closed covered one event, not its siblings
#1025 made a contradictory
item_idonresponse.output_text.donefail closed. The same contradiction onresponse.content_part.donewas still merely ignored — the item stayed open and the completed terminal went on to fabricate the entire closure sequence (content_part.added→output_text.done→content_part.done→output_item.done) on a stream whose identity model we had already observed to be wrong.response.output_text.deltahad the same hole, where reconstructing from only the accepted deltas would ship a message the upstream never assembled that way.The identity contract now covers all of them. An omitted
item_idstays legitimate and is still correlated byoutput_index— that is the common shape for gateways that do not echo ids, and a test asserts it so the guard cannot regress into failing closed on healthy streams.Verification
bun run typecheckclean.Summary by CodeRabbit