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
2 changes: 1 addition & 1 deletion project-facts.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"decision": "0.2.10",
"semanticDecision": "0.3.0"
},
"testCount": 226,
"testCount": 229,
"discoveredHashRouteLiteralCount": 8,
"browserModes": [
"desktop-light",
Expand Down
3 changes: 1 addition & 2 deletions scripts/browser_context_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import http.server
import json
import os
import socketserver
import threading
from copy import deepcopy
from pathlib import Path
Expand Down Expand Up @@ -120,7 +119,7 @@ def packet_fixture() -> dict:

def run() -> None:
handler = functools.partial(QuietHandler, directory=str(SITE))
with socketserver.TCPServer(("127.0.0.1", 0), handler) as server:
with http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler) as server:
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base = f"http://127.0.0.1:{server.server_address[1]}/"
Expand Down
2 changes: 1 addition & 1 deletion site/manifest.sha256
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ada77b250aace00f9b9310d9e5447aed6c5884140b6fb1260ad9eeefb7524072 src/guided-fra
1eeba312c1d36366f056af836a990edf3f1b9fae254ba79425253c887bcdef58 src/lib/decision-hinge.js
f5a1892210355a1f2fada1f7af2d850758fb820b1e483d3c578a6a20d1485cfd src/lib/decision.js
b7214b1632b94496f09ee8fe7d0268ea420e33fac260a47dbdcc7f380634fca0 src/lib/evidence-readiness.js
acf5f06b3935f5e2bf76674e02c0f7d1c71a27edec468eafa36af68f8c94cd8b src/lib/governed-context.js
5198f58a81caf8fb239d2085358ca0286965c61644329ef0dd5a033c77148edf src/lib/governed-context.js
2b613558bcf8c5751f9ca39363699548856364f3bc988f2b742a30bc684e59fa src/lib/input-boundaries.js
ccf92df01cc80bbb10f340fbcaddccb34f0fe4d318ea414ef30e198aecd87a50 src/lib/input.js
752531dea7b1be2bddfdf00f38abe2f6cfd6739fc48f8a5f97cf4741aaf65e47 src/lib/intake.js
Expand Down
17 changes: 15 additions & 2 deletions site/src/lib/governed-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const REQUIRED_V03 = [
const SUMMARY_KEYS = ['assumed', 'contradicted', 'expired', 'known', 'unknown'];
const SUPPORTED_CLASSIFICATIONS = new Set(['PRIVATE', 'PROTECTED']);
const SHA256_RE = /^[a-f0-9]{64}$/;
// Date-based freshness checks retain their existing exclusion of leap-second timestamps.
const TIMESTAMP_RE = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
const PACKET_ID_RE = /^DCP-[A-Z0-9-]+$/;
const LEGACY_DIGEST_SCOPE = 'packet excluding provenance.generated_at and content_sha256';
const PAYLOAD_SCOPE = 'decision-relevant packet content excluding integrity, origin, freshness, and provenance';
Expand Down Expand Up @@ -47,7 +49,15 @@ function objectsOnly(value) {
}

function parseTimestamp(value) {
if (typeof value !== 'string' || !value.trim()) return null;
if (typeof value !== 'string') return null;
const parts = TIMESTAMP_RE.exec(value);
if (!parts || parts[0] !== value) return null;
const year = Number(parts[1]);
const month = Number(parts[2]);
const day = Number(parts[3]);
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
const monthDays = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (day > monthDays[month - 1]) return null;
const date = new Date(value);
return Number.isFinite(date.getTime()) ? date : null;
}
Expand Down Expand Up @@ -148,7 +158,10 @@ function freshnessState(packet, now) {
const sourceAsOf = parseTimestamp(freshness?.source_as_of);
const reviewDue = freshness?.review_due_at === null ? null : parseTimestamp(freshness?.review_due_at);
const validUntil = freshness?.valid_until === null ? null : parseTimestamp(freshness?.valid_until);
if (!issued || !sourceAsOf) return { state: 'INVALID', error: 'Packet freshness timestamps are invalid.' };
if (!issued) return { state: 'INVALID', error: 'Packet issued_at must be a supported RFC 3339 timestamp.' };
if (!sourceAsOf) return { state: 'INVALID', error: 'Packet source_as_of must be a supported RFC 3339 timestamp.' };
if (freshness.review_due_at !== null && !reviewDue) return { state: 'INVALID', error: 'Packet review_due_at must be a supported RFC 3339 timestamp or null.' };
if (freshness.valid_until !== null && !validUntil) return { state: 'INVALID', error: 'Packet valid_until must be a supported RFC 3339 timestamp or null.' };
if (sourceAsOf > issued) return { state: 'INVALID', error: 'Packet source_as_of is later than issued_at.' };
if (issued.getTime() > now.getTime() + CLOCK_SKEW_MS) return { state: 'INVALID', error: 'Packet issued_at is unacceptably future-dated.' };
if (reviewDue && reviewDue < sourceAsOf) return { state: 'INVALID', error: 'Packet review_due_at is earlier than source_as_of.' };
Expand Down
67 changes: 67 additions & 0 deletions tests/governed-context.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,73 @@ test('missing freshness boundaries are not active-eligible', async () => {
assert.equal(result.trust.freshness, 'NOT_ESTABLISHED');
});

test('freshness timestamps reject malformed formats, types, and impossible dates with recomputed integrity', async () => {
const invalid = [
'', ' ', 'not-a-date', 42, false, {}, [],
'September 16, 2026', '2026-09-16', '2026-09-16 00:00:00Z',
'2026-09-16T00:00:00', '2026-09-16T00:00Z',
'2026-09-16T00:00:00Z\n', '2026-09-16T00:00:00.Z',
'2026-02-29T00:00:00Z', '1900-02-29T00:00:00Z', '2100-02-29T00:00:00Z',
'2026-04-31T00:00:00Z', '2026-13-01T00:00:00Z', '2026-09-00T00:00:00Z',
'2026-09-32T00:00:00Z', '2026-09-16T24:00:00Z', '2026-09-16T00:60:00Z',
'2026-09-16T00:00:61Z', '2026-09-16T00:00:00+24:00', '2026-09-16T00:00:00+01:60',
];
for (const field of ['issued_at', 'source_as_of', 'review_due_at', 'valid_until']) {
for (const value of invalid) {
const packet = await packetFixture();
packet.freshness[field] = value;
packet.integrity.envelope_sha256 = await decisionContextEnvelopeDigest(packet);
const result = await validateDecisionContextPacket(packet, { now: NOW });
assert.equal(result.valid, false, `${field}: ${JSON.stringify(value)}`);
assert.equal(result.activeEligible, false);
assert.equal(result.trust.freshness, 'INVALID');
assert.ok(result.errors.some((message) => message.includes(field)));
}
}
});

test('null is permitted only for nullable freshness boundaries', async () => {
for (const field of ['issued_at', 'source_as_of']) {
const packet = await packetFixture();
packet.freshness[field] = null;
packet.integrity.envelope_sha256 = await decisionContextEnvelopeDigest(packet);
const result = await validateDecisionContextPacket(packet, { now: NOW });
assert.equal(result.valid, false);
assert.equal(result.activeEligible, false);
assert.equal(result.trust.freshness, 'INVALID');
assert.ok(result.errors.some((message) => message.includes(field)));
}
for (const field of ['review_due_at', 'valid_until']) {
const packet = await packetFixture();
packet.freshness[field] = null;
packet.integrity.envelope_sha256 = await decisionContextEnvelopeDigest(packet);
const result = await validateDecisionContextPacket(packet, { now: NOW });
assert.equal(result.valid, true);
assert.equal(result.activeEligible, true);
assert.equal(result.trust.freshness, 'CURRENT');
}
});

test('RFC 3339 offsets, lower-case separators, fractions, and leap-year dates remain valid', async () => {
const fields = ['issued_at', 'source_as_of', 'review_due_at', 'valid_until'];
for (const value of [
'2026-09-14T00:00:00Z', '2026-09-14t00:00:00z',
'2026-09-14T00:00:00+00:00', '2026-09-14T00:00:00-00:00',
'2026-09-14T05:30:00+05:30', '2026-09-13T16:00:00-08:00',
'2026-09-14T23:59:00+23:59', '2026-09-14T00:00:00.123456Z',
'2000-02-29T00:00:00Z', '2024-02-29T00:00:00Z',
]) {
const packet = await packetFixture();
for (const field of fields) packet.freshness[field] = value;
packet.integrity.envelope_sha256 = await decisionContextEnvelopeDigest(packet);
const result = await validateDecisionContextPacket(packet, { now: new Date(value) });
assert.equal(result.valid, true, value);
assert.equal(result.activeEligible, true);
assert.equal(result.trust.freshness, 'CURRENT');
assert.deepEqual(result.errors, []);
}
});

test('self-asserted authenticated origin is rejected without an external trust verifier', async () => {
const packet = await packetFixture();
packet.origin.authentication_state = 'AUTHENTICATED';
Expand Down
Loading