Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project are documented here. Format follows

### Fixed

- Transcript parsing now rejects calendar-impossible RFC 3339 timestamps instead of
letting JavaScript normalize values such as 2026-02-31 into March before deadline
guards see them; valid leap days, offsets and fractional seconds remain accepted (#188).

- `tclk_post_frame` now accepts exact decimal-string nonces in addition to safe integer
numbers, so signed Technocore nonces above JavaScript's safe-integer range are preserved
without precision loss. Unsafe numeric nonces (> 2^53 - 1) are rejected at the MCP schema
Expand Down
14 changes: 14 additions & 0 deletions src/transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ const SIGNATURE = /^[A-Za-z0-9_-]{85}[AQgw]$/;
const TIMESTAMP = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
const DID_PREFIX = "did:key:z";

function hasValidCalendarDate(timestamp: string): boolean {
const match = /^(\d{4})-(\d{2})-(\d{2})T/.exec(timestamp);
if (match === null) return false;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return day <= monthDays[month - 1]!;
}

/**
* One normalized technocore record. `line` is the exact stored text; `sender`, `nonce`
* and `signature` authenticate it for `room`. `timestampMs` and `seq` are venue metadata,
Expand Down Expand Up @@ -128,6 +139,9 @@ export function transcriptRecord(room: string, value: unknown): TranscriptRecord
if (!TIMESTAMP.test(message.ts)) {
throw new Error("tclk: transcript message timestamp must be timezone-qualified RFC 3339");
}
if (!hasValidCalendarDate(message.ts)) {
throw new Error("tclk: transcript message timestamp has an invalid calendar date");
}
const timestampMs = Date.parse(message.ts);
if (!Number.isSafeInteger(timestampMs) || timestampMs < 0) {
throw new Error("tclk: transcript message timestamp is invalid");
Expand Down
17 changes: 17 additions & 0 deletions tests/transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,23 @@ describe("trusted transcript records", () => {
});
expect(() => parseTranscriptExport(BOARD, withoutTimezone)).toThrow(/timezone-qualified/);
expect(() => parseTranscriptExport(BOARD, localeTimestamp)).toThrow(/timezone-qualified/);


for (const ts of [
"2026-02-29T00:00:00Z",
"2026-02-31T00:00:00Z",
"2026-04-31T00:00:00+01:00",
]) {
const impossibleDate = JSON.stringify({ ...JSON.parse(raw), ts });
expect(() => parseTranscriptExport(BOARD, impossibleDate)).toThrow(/invalid calendar date/);
}

const leapDay = JSON.stringify({
...JSON.parse(raw),
ts: "2024-02-29T03:04:05.123+02:30",
});
expect(parseTranscriptExport(BOARD, leapDay)[0]?.timestampMs)
.toBe(Date.parse("2024-02-29T03:04:05.123+02:30"));
});

it("never synthesizes offer-before-accept order while selecting a board handshake", () => {
Expand Down