# Expiry rule (intended semantics, enforced by mdip-agent-kit verifyResponse; relaxed in 0.1.5 for stock keymaster):
#   a response MUST expire no more than 2 h after the challenge it answers:
#     response.validUntil <= challenge.validUntil + 2 h        (RESPONSE_EXPIRY_SKEW_MS)
#   Stock @mdip/keymaster 2.2.0 sets challenge.validUntil = created + 1 h and response.validUntil = responded + 1 h,
#   so its responses expire a few seconds to ~1 h AFTER the challenge (2 h covers its local-time setHours across a
#   DST fall-back). Those are ACCEPTED; this kit's own respond still caps at exactly the challenge's validUntil.
#   2 h + 1 ns later is REJECTED, as are far-future responses. A missing response validUntil is REJECTED when the
#   challenge has one.
#   Separately, at verify time BOTH must still be in the future (now < validUntil); that needs a clock, not a vector,
#   and is what actually bounds when a response can be accepted (verify-with-kit.js checks it with fixed `now`s).
# Timestamps: strict RFC 3339 (MDIP writes UTC with ms): YYYY-MM-DDTHH:MM:SS[.1-9 digits](Z|+hh:mm|-hh:mm),
#   uppercase T and Z, a real calendar date, hour 00-23, min/sec 00-59. Compared EXACTLY (to the nanosecond) as an
#   instant, never as strings. Anything else is rejected (fail closed), identically in verify-openssl.sh (pure bash)
#   and checkResponseExpiry/parseTimestampNs (JS). A malformed response validUntil is rejected even when the
#   challenge has none.
# Columns are separated by "|" (a timestamp may itself contain spaces); surrounding spaces are trimmed; "-" = absent.
# challenge.validUntil        | response.validUntil            | expect | note
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.930Z       | accept | equal (mdip-challenge-1/-2 are this case)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.929Z       | accept | 1 ms earlier
2026-10-26T16:41:19.930Z      | 2026-10-19T16:41:19.930Z       | accept | a week earlier
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.931Z       | accept | 1 ms later (within the 2 h skew)
2026-10-26T16:41:19.930Z      | 2026-10-26T17:41:19.930Z       | accept | 1 h later (keymaster's raw default, uncapped)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:24.112Z       | accept | stock keymaster skew: response 4.182 s after the challenge
2026-10-26T16:41:19.930Z      | 2026-10-26T18:41:19.930Z       | accept | exactly 2 h later (skew bound, equal allowed)
2026-10-26T16:41:19.930Z      | 2026-10-26T18:41:19.931Z       | reject | 2 h + 1 ms later
2026-10-26T16:41:19.930Z      | 2026-10-26T18:41:19.930000001Z | reject | 2 h + 1 ns later (exact, not truncated)
2026-10-26T16:41:19.930Z      | 2026-10-26T18:41:19.930-00:01  | reject | 2 h + 1 min later written with an offset
2026-10-26T16:41:19.930Z      | 2026-10-26T18:41:19.9305Z      | reject | 2 h + 0.5 ms later (sub-ms compared, not truncated)
2026-10-26T16:41:19.930Z      | 2026-10-26T19:41:19.930+01:00  | accept | exactly 2 h later written with an offset
2026-10-26T16:41:19.930Z      | 2099-01-01T00:00:00Z           | reject | 2099 (forged far future)
2026-10-26T16:41:19.930Z      | 2027-01-01T00:00:00.000Z       | reject | next year (far future)
2026-10-26T16:41:19.930Z      | -                              | reject | response has no validUntil
-                             | 2026-10-26T16:41:19.930Z       | accept | challenge has no validUntil (nothing to cap)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19Z           | accept | response without ms (0.930 s earlier)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:20Z           | accept | response without ms, 70 ms later (within skew)
2026-10-26T16:41:19.930Z      | 2026-10-26T09:41:19.930-07:00  | accept | same instant written with an offset
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.930        | reject | response with no timezone (fail closed)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.9305Z      | accept | 0.5 ms later (within skew)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.930z       | reject | lowercase z (RFC 3339 allows it; we fail closed)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.9295Z      | accept | 0.5 ms earlier (sub-ms kept, not rejected or truncated)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.930000001Z | accept | 1 ns later (9 fraction digits, within skew)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.9300000000Z | reject | 10 fraction digits (unparseable)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.930+0000   | reject | offset without colon (RFC 3339 needs +hh:mm)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.930-00:00  | accept | -00:00 is the same UTC instant
2026-10-27T01:41:19.930+09:00 | 2026-10-26T16:41:19.930Z       | accept | challenge with offset, same instant (date rolls over)
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.930+23:59  | accept | extreme offset, ~1 day earlier
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19.930+24:00  | reject | offset hour 24 (out of range)
2026-10-26T16:41:19.930Z      | 2026-10-26 16:41:19.930Z       | reject | space instead of T
2026-10-26T16:41:19.930Z      | 2026-10-26t16:41:19.930Z       | reject | lowercase t
2026-10-26T16:41:19.930Z      | 2026-10-26T16:41:19,930Z       | reject | comma decimal
2026-12-31T23:59:59.999Z      | 2026-12-31T23:59:60Z           | reject | leap second :60 (fail closed)
2026-12-31T23:59:59.999Z      | 2026-12-31T24:00:00Z           | reject | 24:00 (fail closed, not next midnight)
2026-03-02T00:00:00Z          | 2026-02-30T00:00:00Z           | reject | Feb 30 (not rolled over to Mar 2)
2027-01-01T00:00:00Z          | 2026-00-15T00:00:00Z           | reject | month 00
2027-02-01T00:00:00Z          | 2026-13-01T00:00:00Z           | reject | month 13 (not rolled over to 2027-01)
2026-12-01T00:00:00Z          | 2026-11-31T00:00:00Z           | reject | Nov 31
2026-12-01T00:00:00Z          | 2026-11-00T00:00:00Z           | reject | day 00
2028-03-01T00:00:00Z          | 2028-02-29T23:59:59.999Z       | accept | Feb 29 in a leap year
2100-03-01T00:00:00Z          | 2100-02-29T00:00:00Z           | reject | Feb 29 in 2100 (not a leap year)
2026-10-26T16:41:19.930Z      | +002026-10-26T16:41:19.930Z    | reject | expanded year
2026-10-26T16:41:19.930Z      | Mon, 26 Oct 2026 16:41:19 GMT  | reject | RFC 2822 date
2026-10-26T16:41:19.930z      | 2026-10-26T16:41:19.930Z       | reject | challenge with lowercase z
-                             | 2026-10-26T16:41:19.930        | reject | no challenge cap, but response has no timezone
-                             | -                              | accept | neither has validUntil
