milestone 33: contract closure — samples, file-authority enumeration, dead code, bundle ceiling
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
ENUM_VALUES,
|
||||
policyActionLabel,
|
||||
policyReasonLabel,
|
||||
qclassName,
|
||||
rcodeName,
|
||||
routeKindLabel,
|
||||
} from "./provenanceCopy";
|
||||
|
||||
/**
|
||||
* `tsc` proves the maps total over the union; this proves the union is the set
|
||||
* the server actually stores, and that no entry was left as its raw tag name.
|
||||
*/
|
||||
test("every stored enum value has a label of its own", () => {
|
||||
const labels = [
|
||||
...ENUM_VALUES.policyAction.map(policyActionLabel),
|
||||
...ENUM_VALUES.policyReason.map(policyReasonLabel),
|
||||
...ENUM_VALUES.routeKind.map(routeKindLabel),
|
||||
];
|
||||
for (const label of labels) {
|
||||
expect(label).not.toBe("");
|
||||
expect(label).not.toMatch(/_/);
|
||||
}
|
||||
expect(new Set(ENUM_VALUES.policyReason.map(policyReasonLabel)).size).toBe(ENUM_VALUES.policyReason.length);
|
||||
});
|
||||
|
||||
test("response codes read by name where one exists, by number where none does", () => {
|
||||
expect(rcodeName(0)).toBe("NOERROR (0)");
|
||||
expect(rcodeName(3)).toBe("NXDOMAIN (3)");
|
||||
expect(rcodeName(16)).toBe("BADVERS (16)");
|
||||
// The column holds the twelve-bit extended code, most of which is unassigned.
|
||||
expect(rcodeName(3841)).toBe("RCODE 3841");
|
||||
});
|
||||
|
||||
test("query classes read the same way", () => {
|
||||
expect(qclassName(1)).toBe("IN (1)");
|
||||
expect(qclassName(255)).toBe("ANY (255)");
|
||||
expect(qclassName(42)).toBe("CLASS 42");
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { POLICY_ACTIONS, POLICY_REASONS, ROUTE_KINDS } from "@/lib/types";
|
||||
import type { PolicyAction, PolicyReason, RouteKind } from "@/lib/types";
|
||||
|
||||
/**
|
||||
* Display names for the three stored enums. `Record` over the union, so a value
|
||||
* added to `src/storage/provenance.zig` and mirrored into `lib/types.ts` fails
|
||||
* `tsc` here instead of reaching a cell as a raw tag name.
|
||||
*/
|
||||
const POLICY_ACTION_LABELS: Record<PolicyAction, string> = {
|
||||
not_evaluated: "Not evaluated",
|
||||
allow: "Allowed",
|
||||
block: "Blocked",
|
||||
};
|
||||
|
||||
const POLICY_REASON_LABELS: Record<PolicyReason, string> = {
|
||||
rule_allow_exact: "Allow rule (exact)",
|
||||
rule_block_exact: "Block rule (exact)",
|
||||
rule_allow_wildcard: "Allow rule (wildcard)",
|
||||
rule_block_wildcard: "Block rule (wildcard)",
|
||||
rule_allow_regex: "Allow rule (regex)",
|
||||
rule_block_regex: "Block rule (regex)",
|
||||
blocklist_exception: "Blocklist exception",
|
||||
blocklist_domain: "Blocklist (domain)",
|
||||
blocklist_wildcard: "Blocklist (wildcard)",
|
||||
local_record: "Local record",
|
||||
forward_zone: "Forward zone",
|
||||
non_in_class: "Not class IN",
|
||||
paused: "Filtering paused",
|
||||
snapshot_unavailable: "No filter snapshot",
|
||||
no_match: "No match",
|
||||
protocol_error: "Protocol refusal",
|
||||
};
|
||||
|
||||
const ROUTE_KIND_LABELS: Record<RouteKind, string> = {
|
||||
blocked: "Blocked locally",
|
||||
local: "Local record",
|
||||
forward_zone: "Forward zone",
|
||||
upstream: "Upstream resolver",
|
||||
cache: "Cache",
|
||||
rejected: "Rejected",
|
||||
};
|
||||
|
||||
export function policyActionLabel(action: PolicyAction): string {
|
||||
return POLICY_ACTION_LABELS[action];
|
||||
}
|
||||
|
||||
export function policyReasonLabel(reason: PolicyReason): string {
|
||||
return POLICY_REASON_LABELS[reason];
|
||||
}
|
||||
|
||||
export function routeKindLabel(kind: RouteKind): string {
|
||||
return ROUTE_KIND_LABELS[kind];
|
||||
}
|
||||
|
||||
/** The enum value sets, for tests that prove the maps exhaustive at runtime too. */
|
||||
export const ENUM_VALUES = {
|
||||
policyAction: POLICY_ACTIONS,
|
||||
policyReason: POLICY_REASONS,
|
||||
routeKind: ROUTE_KINDS,
|
||||
} as const;
|
||||
|
||||
const RCODE_NAMES: Record<number, string> = {
|
||||
0: "NOERROR",
|
||||
1: "FORMERR",
|
||||
2: "SERVFAIL",
|
||||
3: "NXDOMAIN",
|
||||
4: "NOTIMP",
|
||||
5: "REFUSED",
|
||||
6: "YXDOMAIN",
|
||||
7: "YXRRSET",
|
||||
8: "NXRRSET",
|
||||
9: "NOTAUTH",
|
||||
10: "NOTZONE",
|
||||
16: "BADVERS",
|
||||
};
|
||||
|
||||
/**
|
||||
* The bare mnemonic, for a table cell with no room for the number. An
|
||||
* unassigned code has no mnemonic to shorten, so it keeps the same `RCODE <n>`
|
||||
* shape the long form falls back to.
|
||||
*/
|
||||
export function rcodeShortName(rcode: number): string {
|
||||
return RCODE_NAMES[rcode] ?? `RCODE ${rcode}`;
|
||||
}
|
||||
|
||||
/** The twelve-bit extended code as `NXDOMAIN (3)`; an unassigned code keeps its number. */
|
||||
export function rcodeName(rcode: number): string {
|
||||
const name = RCODE_NAMES[rcode];
|
||||
return name === undefined ? `RCODE ${rcode}` : `${name} (${rcode})`;
|
||||
}
|
||||
|
||||
const QCLASS_NAMES: Record<number, string> = {
|
||||
1: "IN",
|
||||
3: "CH",
|
||||
4: "HS",
|
||||
254: "NONE",
|
||||
255: "ANY",
|
||||
};
|
||||
|
||||
export function qclassName(qclass: number): string {
|
||||
const name = QCLASS_NAMES[qclass];
|
||||
return name === undefined ? `CLASS ${qclass}` : `${name} (${qclass})`;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Provenance, QueryRow } from "@/lib/types";
|
||||
|
||||
/**
|
||||
* Fixture builders for the provenance shapes, shared by the query-log, detail
|
||||
* and live-stream tests the way `features/activity/fakeEventSource.ts` is shared.
|
||||
*
|
||||
* The defaults describe the dullest possible query — an allowed name nothing
|
||||
* matched, answered upstream — so each test states only the fields it is about.
|
||||
*/
|
||||
type Sections = {
|
||||
[K in keyof Provenance]?: Partial<Provenance[K]>;
|
||||
};
|
||||
|
||||
export function provenance(sections: Sections = {}): Provenance {
|
||||
return {
|
||||
request: {
|
||||
time: 1_700_000_000,
|
||||
domain: "example.com",
|
||||
client: "192.0.2.10",
|
||||
qtype: 1,
|
||||
qclass: 1,
|
||||
...sections.request,
|
||||
},
|
||||
group: { id: 1, name: "default", ...sections.group },
|
||||
policy: {
|
||||
action: "allow",
|
||||
reason: "no_match",
|
||||
matched: "",
|
||||
source_id: null,
|
||||
source_name: "",
|
||||
...sections.policy,
|
||||
},
|
||||
rewrites: { cname_target: "", safe_search_target: "", ...sections.rewrites },
|
||||
route: { kind: "upstream", forward_zone: "", upstream: "https://dns.example/dns-query", ...sections.route },
|
||||
response: { rcode: 0, duration_us: 1234, ...sections.response },
|
||||
};
|
||||
}
|
||||
|
||||
/** The flat stored row of the same dull query. */
|
||||
export function queryRow(id: number, overrides: Partial<QueryRow> = {}): QueryRow {
|
||||
return {
|
||||
id,
|
||||
ts: 1_700_000_000,
|
||||
domain: "example.com",
|
||||
client_ip: "192.0.2.10",
|
||||
qtype: 1,
|
||||
qclass: 1,
|
||||
rcode: 0,
|
||||
blocked: false,
|
||||
response_time_us: 1234,
|
||||
cache_hit: false,
|
||||
upstream: "https://dns.example/dns-query",
|
||||
policy_action: "allow",
|
||||
policy_reason: "no_match",
|
||||
route_kind: "upstream",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { qtypeName } from "./qtype";
|
||||
|
||||
test("common qtype codes render as DNS type names", () => {
|
||||
expect(qtypeName(1)).toBe("A");
|
||||
expect(qtypeName(28)).toBe("AAAA");
|
||||
expect(qtypeName(5)).toBe("CNAME");
|
||||
expect(qtypeName(65)).toBe("HTTPS");
|
||||
expect(qtypeName(16)).toBe("TXT");
|
||||
});
|
||||
|
||||
test("unknown codes fall back to TYPE<n>", () => {
|
||||
expect(qtypeName(99)).toBe("TYPE99");
|
||||
expect(qtypeName(0)).toBe("TYPE0");
|
||||
});
|
||||
|
||||
test("null qtype renders as a dash", () => {
|
||||
expect(qtypeName(null)).toBe("—");
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
const QTYPE_NAMES: Record<number, string> = {
|
||||
1: "A",
|
||||
2: "NS",
|
||||
5: "CNAME",
|
||||
6: "SOA",
|
||||
12: "PTR",
|
||||
15: "MX",
|
||||
16: "TXT",
|
||||
28: "AAAA",
|
||||
33: "SRV",
|
||||
35: "NAPTR",
|
||||
43: "DS",
|
||||
46: "RRSIG",
|
||||
47: "NSEC",
|
||||
48: "DNSKEY",
|
||||
52: "TLSA",
|
||||
64: "SVCB",
|
||||
65: "HTTPS",
|
||||
255: "ANY",
|
||||
257: "CAA",
|
||||
};
|
||||
|
||||
/** DNS type name for common codes, `TYPE<n>` fallback (RFC 3597 style), em dash for null. */
|
||||
export function qtypeName(qtype: number | null): string {
|
||||
if (qtype === null) return "—";
|
||||
return QTYPE_NAMES[qtype] ?? `TYPE${qtype}`;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { LiveQueryEvent, PolicyReason, QueryRow, RouteKind } from "@/lib/types";
|
||||
|
||||
/**
|
||||
* What the query-log table renders for one row, whichever surface it came from.
|
||||
*
|
||||
* The stored list row and the live stream's provenance event describe the same
|
||||
* query in two different shapes — flat summary against nested full detail — and
|
||||
* both pages share one set of cells, so both project into this.
|
||||
*
|
||||
* `id` is null for a streamed event: the frame precedes its own insert, so no
|
||||
* row exists to link to yet.
|
||||
*/
|
||||
export interface QuerySummary {
|
||||
id: number | null;
|
||||
ts: number;
|
||||
domain: string;
|
||||
client_ip: string;
|
||||
qtype: number | null;
|
||||
blocked: boolean;
|
||||
policy_reason: PolicyReason;
|
||||
/** The twelve-bit extended code the client saw, including a synthesized SERVFAIL. */
|
||||
rcode: number;
|
||||
route_kind: RouteKind;
|
||||
response_time_us: number | null;
|
||||
cache_hit: boolean | null;
|
||||
upstream: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the cache answered, or null where it never applied. Mirrors
|
||||
* `Context.cacheHit` in src/server/handler.zig, which derives the stored
|
||||
* `cache_hit` column from the same route: a local record, a blocked answer and
|
||||
* a protocol refusal all bypass the cache, and "miss" would claim a lookup that
|
||||
* never happened.
|
||||
*/
|
||||
export function cacheHitFor(kind: RouteKind): boolean | null {
|
||||
switch (kind) {
|
||||
case "cache":
|
||||
return true;
|
||||
case "upstream":
|
||||
case "forward_zone":
|
||||
return false;
|
||||
case "local":
|
||||
case "blocked":
|
||||
case "rejected":
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function summarizeRow(row: QueryRow): QuerySummary {
|
||||
return {
|
||||
id: row.id,
|
||||
ts: row.ts,
|
||||
domain: row.domain,
|
||||
client_ip: row.client_ip,
|
||||
qtype: row.qtype,
|
||||
blocked: row.blocked,
|
||||
policy_reason: row.policy_reason,
|
||||
rcode: row.rcode,
|
||||
route_kind: row.route_kind,
|
||||
response_time_us: row.response_time_us,
|
||||
cache_hit: row.cache_hit,
|
||||
upstream: row.upstream,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The same summary out of a live frame. `blocked` and `cache_hit` are derived
|
||||
* rather than sent: the server derives the stored columns from exactly these
|
||||
* two fields (handler.zig's `Entry.init` call), so the projection reproduces
|
||||
* them instead of the DTO carrying the same fact twice.
|
||||
*/
|
||||
export function summarizeEvent(event: LiveQueryEvent): QuerySummary {
|
||||
return {
|
||||
id: null,
|
||||
ts: event.request.time,
|
||||
domain: event.request.domain,
|
||||
client_ip: event.request.client,
|
||||
qtype: event.request.qtype,
|
||||
blocked: event.policy.action === "block",
|
||||
policy_reason: event.policy.reason,
|
||||
rcode: event.response.rcode,
|
||||
route_kind: event.route.kind,
|
||||
response_time_us: event.response.duration_us,
|
||||
cache_hit: cacheHitFor(event.route.kind),
|
||||
upstream: event.route.upstream,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user