feat(core-shared): truncateIp helper (/24 IPv4, /48 IPv6) per DPA

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-11 16:03:45 +02:00
parent 1247e1804a
commit cc4de8eb75
2 changed files with 64 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
import { describe, it, expect } from "vitest";
import { truncateIp } from "./truncate-ip";
describe("truncateIp", () => {
describe("IPv4", () => {
it("truncates to /24 (zeros the last octet)", () => {
expect(truncateIp("192.168.1.42")).toBe("192.168.1.0");
expect(truncateIp("10.0.0.255")).toBe("10.0.0.0");
expect(truncateIp("8.8.8.8")).toBe("8.8.8.0");
});
it("throws on malformed input", () => {
expect(() => truncateIp("192.168.1")).toThrow(/malformed IPv4/);
expect(() => truncateIp("192.168.1.foo")).toThrow(/malformed IPv4/);
expect(() => truncateIp("a.b.c.d")).toThrow(/malformed IPv4/);
});
it("throws on empty string", () => {
expect(() => truncateIp("")).toThrow(/malformed IPv4/);
});
});
describe("IPv6", () => {
it("truncates to /48 (keeps first 3 hextets)", () => {
expect(truncateIp("2001:0db8:1234:5678:abcd:ef00:1234:5678")).toBe("2001:0db8:1234::");
expect(truncateIp("2001:0db8:abcd:1234::")).toBe("2001:0db8:abcd::");
});
it("lowercases hextets", () => {
expect(truncateIp("2001:0DB8:ABCD:5678::")).toBe("2001:0db8:abcd::");
});
it("throws on too-few hextets", () => {
expect(() => truncateIp("2001:0db8")).toThrow(/malformed IPv6/);
});
});
});

View File

@@ -0,0 +1,27 @@
/**
* Truncates an IP address per DPA:
* IPv4 → /24 ("192.168.1.42" → "192.168.1.0")
* IPv6 → /48 ("2001:0db8:1234:5678:..." → "2001:0db8:1234::")
*
* Throws on malformed input rather than silently returning the raw value —
* compliance regimes prefer hard failures over partial scrubbing.
*/
export function truncateIp(raw: string): string {
if (raw.includes(":")) {
// IPv6: keep first 3 hextets (48 bits)
const parts = raw.toLowerCase().split(":").filter((p) => p !== "");
if (parts.length < 3) {
throw new Error(`truncateIp: malformed IPv6 address "${raw}"`);
}
return `${parts[0]}:${parts[1]}:${parts[2]}::`;
}
// IPv4: keep first 3 octets (24 bits)
const parts = raw.split(".");
if (
parts.length !== 4 ||
parts.some((p) => p === "" || isNaN(Number(p)) || !/^\d+$/.test(p))
) {
throw new Error(`truncateIp: malformed IPv4 address "${raw}"`);
}
return `${parts[0]}.${parts[1]}.${parts[2]}.0`;
}