Skip to content

Commit 5753151

Browse files
committed
Merge tag '2.3.3'
LogTape 2.3.3
2 parents e1e92aa + 18eb201 commit 5753151

4 files changed

Lines changed: 289 additions & 26 deletions

File tree

CHANGES.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ To be released.
4848
[#199]: https://github.com/dahlia/logtape/issues/199
4949

5050

51+
Version 2.3.3
52+
-------------
53+
54+
Released on September 5, 2026.
55+
56+
### @logtape/redaction
57+
58+
- Fixed `CREDIT_CARD_NUMBER_PATTERN` to redact Luhn-valid credit card numbers
59+
with 13–19 digits, including unseparated numbers and common space- and
60+
hyphen-separated formats. Numbers that fit these formats but fail the
61+
Luhn check are no longer redacted.
62+
[[#210], [#211]]
63+
64+
[#210]: https://github.com/dahlia/logtape/issues/210
65+
[#211]: https://github.com/dahlia/logtape/pull/211
66+
67+
5168
Version 2.3.2
5269
-------------
5370

@@ -268,6 +285,19 @@ Released on July 30, 2026.
268285
Claude Code plugin marketplace.
269286

270287

288+
Version 2.2.6
289+
-------------
290+
291+
Released on September 5, 2026.
292+
293+
### @logtape/redaction
294+
295+
- Fixed `CREDIT_CARD_NUMBER_PATTERN` to redact Luhn-valid credit card numbers
296+
with 13–19 digits, including unseparated numbers and common space- and
297+
hyphen-separated formats. Numbers that fit these formats but fail the
298+
Luhn check are no longer redacted. [[#210], [#211]]
299+
300+
271301
Version 2.2.5
272302
-------------
273303

@@ -557,6 +587,19 @@ Released on June 22, 2026.
557587
[#176]: https://github.com/dahlia/logtape/issues/176
558588

559589

590+
Version 2.1.10
591+
--------------
592+
593+
Released on September 5, 2026.
594+
595+
### @logtape/redaction
596+
597+
- Fixed `CREDIT_CARD_NUMBER_PATTERN` to redact Luhn-valid credit card numbers
598+
with 13–19 digits, including unseparated numbers and common space- and
599+
hyphen-separated formats. Numbers that fit these formats but fail the
600+
Luhn check are no longer redacted. [[#210], [#211]]
601+
602+
560603
Version 2.1.9
561604
-------------
562605

@@ -846,6 +889,19 @@ Released on May 17, 2026.
846889
[#164]: https://github.com/dahlia/logtape/pull/164
847890

848891

892+
Version 2.0.19
893+
--------------
894+
895+
Released on September 5, 2026.
896+
897+
### @logtape/redaction
898+
899+
- Fixed `CREDIT_CARD_NUMBER_PATTERN` to redact Luhn-valid credit card numbers
900+
with 13–19 digits, including unseparated numbers and common space- and
901+
hyphen-separated formats. Numbers that fit these formats but fail the
902+
Luhn check are no longer redacted. [[#210], [#211]]
903+
904+
849905
Version 2.0.18
850906
--------------
851907

docs/manual/redaction.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ import {
121121
~~~~
122122

123123
- `EMAIL_ADDRESS_PATTERN`: Redacts email addresses
124-
- `CREDIT_CARD_NUMBER_PATTERN`: Redacts credit card numbers
124+
- `CREDIT_CARD_NUMBER_PATTERN`: Redacts Luhn-valid credit card numbers with
125+
13–19 digits, including common space- and hyphen-separated formats. Numbers
126+
that fit these formats but fail the Luhn check are left unchanged
125127
- `JWT_PATTERN`: Redacts JSON Web Tokens
126128
- `US_SSN_PATTERN`: Redacts U.S. Social Security numbers
127129
- `KR_RRN_PATTERN`: Redacts South Korean resident registration numbers

packages/redaction/src/pattern.test.ts

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ const domainPartArb: fc.Arbitrary<string> = fc.stringMatching(
2727
const digitArb: fc.Arbitrary<string> = fc.integer({ min: 0, max: 9 }).map(
2828
String,
2929
);
30+
const luhnValidCardDigitsArb: fc.Arbitrary<readonly string[]> = fc.array(
31+
digitArb,
32+
{ minLength: 15, maxLength: 15 },
33+
).map(appendLuhnCheckDigit);
3034

3135
test("EMAIL_ADDRESS_PATTERN", () => {
3236
const { pattern, replacement } = EMAIL_ADDRESS_PATTERN;
@@ -101,34 +105,87 @@ test("EMAIL_ADDRESS_PATTERN redacts generated email addresses", () => {
101105
test("CREDIT_CARD_NUMBER_PATTERN", () => {
102106
const { pattern, replacement } = CREDIT_CARD_NUMBER_PATTERN;
103107

104-
// Test valid credit card numbers with dashes
105-
assert.match("1234-5678-9012-3456", pattern); // Regular 16-digit card
106-
pattern.lastIndex = 0;
107-
assert.match("1234-5678-901234", pattern); // American Express format
108-
pattern.lastIndex = 0;
108+
const redact = (value: string): string =>
109+
typeof replacement === "string"
110+
? value.replaceAll(pattern, replacement)
111+
: value.replaceAll(pattern, replacement);
112+
113+
const validNumbers = [
114+
"4222222222222",
115+
"4222 2222 2222 2",
116+
"30569309025904",
117+
"3056 9309 025904",
118+
"3056-9309-025904",
119+
"3056-930902-5904",
120+
"1354 12345 678911",
121+
"1354-12345-678911",
122+
"378282246310005",
123+
"3782 822463 10005",
124+
"3782-822463-10005",
125+
"4111111111111111",
126+
"4111 1111 1111 1111",
127+
"4111 1111 1111 1111",
128+
"4111-1111-1111-1111",
129+
"5500005555555559",
130+
"4000000000000000006",
131+
"4000 0000 0000 0000 006",
132+
"4000-0000-0000-0000-006",
133+
];
134+
135+
for (const number of validNumbers) {
136+
assert.strictEqual(
137+
redact(`Card: ${number}`),
138+
"Card: XXXX-XXXX-XXXX-XXXX",
139+
);
140+
}
109141

110-
// Test replacements
111142
assert.strictEqual(
112-
"Card: 1234-5678-9012-3456".replaceAll(pattern, replacement as string),
113-
"Card: XXXX-XXXX-XXXX-XXXX",
143+
redact("Cards: 4111111111111111 and 3782-822463-10005"),
144+
"Cards: XXXX-XXXX-XXXX-XXXX and XXXX-XXXX-XXXX-XXXX",
114145
);
115146
assert.strictEqual(
116-
"AmEx: 1234-5678-901234".replaceAll(pattern, replacement as string),
117-
"AmEx: XXXX-XXXX-XXXX-XXXX",
147+
redact("Payment: 4111-1111-1111-1111-12-28"),
148+
"Payment: XXXX-XXXX-XXXX-XXXX-12-28",
118149
);
119150
assert.strictEqual(
120-
"Cards: 1234-5678-9012-3456 and 1234-5678-901234".replaceAll(
121-
pattern,
122-
replacement as string,
123-
),
124-
"Cards: XXXX-XXXX-XXXX-XXXX and XXXX-XXXX-XXXX-XXXX",
151+
redact("Payment: 4111 1111 1111 1111 12/28"),
152+
"Payment: XXXX-XXXX-XXXX-XXXX 12/28",
153+
);
154+
assert.strictEqual(
155+
redact("Order 0001 4111-1111-1111-1111"),
156+
"Order 0001 XXXX-XXXX-XXXX-XXXX",
125157
);
158+
assert.strictEqual(
159+
redact("card_4111-1111-1111-1111_token"),
160+
"card_XXXX-XXXX-XXXX-XXXX_token",
161+
);
162+
assert.strictEqual(
163+
redact("Ref 1234 5678 4111 1111 1111 1111"),
164+
"Ref 1234 5678 XXXX-XXXX-XXXX-XXXX",
165+
);
166+
assert.strictEqual(
167+
redact("Cards: 4111 1111 1111 1111 5500 0055 5555 5559"),
168+
"Cards: XXXX-XXXX-XXXX-XXXX XXXX-XXXX-XXXX-XXXX",
169+
);
170+
171+
const invalidNumbers = [
172+
"123456789012",
173+
"12345678901234",
174+
"1234-5678-901234",
175+
"4111111111111112",
176+
"4111 1111 1111 1112",
177+
"12345678901234567890",
178+
];
179+
180+
for (const number of invalidNumbers) {
181+
assert.strictEqual(redact(`Number: ${number}`), `Number: ${number}`);
182+
}
126183
});
127184

128-
test("CREDIT_CARD_NUMBER_PATTERN redacts generated dashed card numbers", () => {
185+
test("CREDIT_CARD_NUMBER_PATTERN redacts generated Luhn-valid cards", () => {
129186
fc.assert(
130187
fc.property(
131-
fc.array(digitArb, { minLength: 16, maxLength: 16 }),
188+
luhnValidCardDigitsArb,
132189
(digits) => {
133190
const card = `${digits.slice(0, 4).join("")}-${
134191
digits.slice(4, 8).join("")
@@ -279,10 +336,10 @@ test("redactByPattern(TextFormatter)", () => {
279336
level: "info",
280337
category: ["test"],
281338
message: [
282-
"Sensitive info: email = user@example.com, cc = 1234-5678-9012-3456, ssn = 123-45-6789",
339+
"Sensitive info: email = user@example.com, cc = 4111-1111-1111-1111, ssn = 123-45-6789",
283340
],
284341
rawMessage:
285-
"Sensitive info: email = user@example.com, cc = 1234-5678-9012-3456, ssn = 123-45-6789",
342+
"Sensitive info: email = user@example.com, cc = 4111-1111-1111-1111, ssn = 123-45-6789",
286343
timestamp: Date.now(),
287344
properties: {},
288345
};
@@ -383,7 +440,7 @@ test("redactByPattern(ConsoleFormatter)", async () => {
383440
{
384441
name: "John Doe",
385442
email: "john@example.com",
386-
creditCard: "1234-5678-9012-3456",
443+
creditCard: "4111-1111-1111-1111",
387444
},
388445
],
389446
rawMessage: "User data: [object Object]",
@@ -431,8 +488,8 @@ test("redactByPattern(ConsoleFormatter)", async () => {
431488
},
432489
payment: {
433490
cards: [
434-
"1234-5678-9012-3456",
435-
"8765-4321-8765-4321",
491+
"4111-1111-1111-1111",
492+
"5500-0055-5555-5559",
436493
],
437494
},
438495
documents: {
@@ -736,3 +793,16 @@ function record(): LogRecord {
736793
properties: {},
737794
};
738795
}
796+
797+
function appendLuhnCheckDigit(digits: readonly string[]): readonly string[] {
798+
let checksum = 0;
799+
for (let i = 0; i < digits.length; i++) {
800+
let digit = Number(digits[i]);
801+
if (i % 2 === 0) {
802+
digit *= 2;
803+
if (digit > 9) digit -= 9;
804+
}
805+
checksum += digit;
806+
}
807+
return [...digits, String((10 - checksum % 10) % 10)];
808+
}

0 commit comments

Comments
 (0)