@yozz.app/tls 0.1.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@ const result = await startTls({
25
25
  transport,
26
26
  serverName: 'imap.example.com',
27
27
  trustAnchors: compileAnchors(ROOT_BUNDLE).source,
28
- validationTime: new Date(),
28
+ validationTime: Temporal.Now.instant(),
29
29
  validator: YOZZ_VALIDATOR,
30
30
  });
31
31
  if (result.ok) {
@@ -35,7 +35,14 @@ if (result.ok) {
35
35
 
36
36
  `HandshakeResult` is a discriminated union, never a throw for a refused certificate; the refusal
37
37
  says which check failed. A session handed to `onSession` can be offered once on a later
38
- connection to resume.
38
+ connection to resume. Its `receivedAt` and `authenticatedAt` are `Temporal.Instant`s, which neither
39
+ structured clone nor JSON keeps, so a store encodes them. Before 0.3 they were `Date`s: a store
40
+ written by 0.2 must convert them, or `startTls` throws on the session before a byte goes out.
41
+
42
+ **Requires a global `Temporal`.** Times are `Temporal.Instant`s (`validationTime`, the optional `now` clock, a session's
43
+ instants). Node 26, Chrome and Firefox ship it; where the runtime has none (Safari, Node 24), install
44
+ [`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill) and load it before this package is
45
+ imported (`import 'temporal-polyfill/global'`). The package imports no polyfill itself.
39
46
 
40
47
  `src/index.ts` is the whole public API and says why each export exists. The source is in
41
48
  [fishballapp/yozz](https://github.com/fishballapp/yozz), which takes issues but no pull requests.
package/dist/index.d.mts CHANGED
@@ -107,14 +107,14 @@ type TlsSession = {
107
107
  readonly preSharedKey: Uint8Array;
108
108
  /** §4.3.11.1's `ticket_age_add`, the server's random offset. */
109
109
  readonly ticketAgeAdd: number;
110
- /** A real `Date`; a persisted session must revive it. */
111
- readonly receivedAt: Date;
110
+ /** Neither structured clone nor JSON keeps a `Temporal.Instant`: a store encodes it. */
111
+ readonly receivedAt: Temporal.Instant;
112
112
  readonly lifetimeSeconds: number;
113
113
  /**
114
114
  * When the peer's certificate was last verified, carried forward unchanged through every renewal:
115
- * RFC 9846 §4.7.1 asks for a limit on the total lifetime of renewed keying material. A real `Date`.
115
+ * RFC 9846 §4.7.1 asks for a limit on the total lifetime of renewed keying material.
116
116
  */
117
- readonly authenticatedAt: Date;
117
+ readonly authenticatedAt: Temporal.Instant;
118
118
  /** Carried forward through renewals. A name rather than a code point, so it survives the caller's store. */
119
119
  readonly peerSignatureScheme: SignatureScheme;
120
120
  /** Carried forward, so a resumed handshake can re-validate the chain against today's clock and anchors (`reverifyOnResume`). */
@@ -132,7 +132,7 @@ type StartTlsOptions = {
132
132
  readonly transport: ByteDuplex;
133
133
  readonly serverName: string;
134
134
  readonly trustAnchors: TrustAnchorSource;
135
- readonly validationTime: Date;
135
+ readonly validationTime: Temporal.Instant;
136
136
  readonly validator: Validator;
137
137
  readonly expectedPeerName?: PeerName | null;
138
138
  /** In preference order; the first carries the key share. */
@@ -154,7 +154,7 @@ type StartTlsOptions = {
154
154
  /** Tickets arrive after the handshake, so this fires from `read()`. Without it, tickets are dropped. */
155
155
  readonly onSession?: (session: TlsSession) => void | Promise<void>;
156
156
  /** The running clock for ticket ages, distinct from `validationTime` (a policy input a caller may freeze). Injectable for BoGo. */
157
- readonly now?: () => Date;
157
+ readonly now?: () => Temporal.Instant;
158
158
  };
159
159
  type HandshakeResult = {
160
160
  readonly ok: true;
package/dist/index.mjs CHANGED
@@ -1839,6 +1839,7 @@ const isDerChain = (chain) => chain?.leafDer instanceof Uint8Array && Array.isAr
1839
1839
  const assertUsableSession = (session) => {
1840
1840
  if (!SUPPORTED_SIGNATURE_SCHEMES.includes(session.peerSignatureScheme)) throw new Error("the stored session names no signature scheme this client implements");
1841
1841
  if (!isDerChain(session.peerCertificateChain)) throw new Error("the stored session carries no usable peer certificate chain");
1842
+ for (const field of ["receivedAt", "authenticatedAt"]) if (!(session[field] instanceof Temporal.Instant)) throw new Error(`the stored session's ${field} is not a Temporal.Instant`);
1842
1843
  };
1843
1844
  /**
1844
1845
  * A resumed handshake verifies no signature, so a ticket it mints inherits the original
@@ -1858,10 +1859,10 @@ const inheritedAuthentication = (resumed, verifiedNow) => {
1858
1859
  return verifiedNow;
1859
1860
  };
1860
1861
  const isWithinAuthenticationCeiling = (authenticatedAt, now) => {
1861
- const age = now.getTime() - authenticatedAt.getTime();
1862
+ const age = now.epochMilliseconds - authenticatedAt.epochMilliseconds;
1862
1863
  return age >= 0 && age < MAX_AUTHENTICATION_AGE_SECONDS * 1e3;
1863
1864
  };
1864
- const ticketAgeMs = (session, now) => now.getTime() - session.receivedAt.getTime();
1865
+ const ticketAgeMs = (session, now) => now.epochMilliseconds - session.receivedAt.epochMilliseconds;
1865
1866
  /** RFC 9846 §4.3.11.1: age in milliseconds plus the server's offset, modulo 2^32. */
1866
1867
  const obfuscatedTicketAge = (session, now) => (ticketAgeMs(session, now) + session.ticketAgeAdd) % 4294967296;
1867
1868
  /** `asciiLower`, not `toLowerCase`: KELVIN SIGN folds to `k` under Unicode, which would widen a security check. */
@@ -2231,7 +2232,7 @@ const clientHelloRecords = (message, legacyVersion) => Array.from({ length: Math
2231
2232
  /** Exported for `replay.ts` only, which `index.test.ts` enforces. */
2232
2233
  const runHandshake = async (options, replay) => {
2233
2234
  const { transport, serverName } = options;
2234
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
2235
+ const now = options.now ?? (() => Temporal.Instant.fromEpochMilliseconds(Date.now()));
2235
2236
  const expectedPeerName = options.expectedPeerName !== void 0 ? options.expectedPeerName : {
2236
2237
  kind: "dns",
2237
2238
  value: serverName
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yozz.app/tls",
3
- "version": "0.1.6",
3
+ "version": "0.3.0",
4
4
  "description": "A TLS 1.3 client in TypeScript over any byte duplex: record layer, handshake, key schedule, PSK resumption, KeyUpdate, TOFU pinning. Runs in Node and browsers, no native code.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fishballapp/yozz",
@@ -29,11 +29,12 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@yozz.app/x509": "0.1.6"
32
+ "@yozz.app/x509": "0.3.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@playwright/test": "1.63.0",
35
+ "@playwright/test": "1.64.1",
36
36
  "@types/node": "^24.13.4",
37
+ "temporal-polyfill": "^1.0.8",
37
38
  "tsdown": "^0.23.0",
38
39
  "vite": "^8.3.0",
39
40
  "wrangler": "^4.131.2"