@kagal/taistamp 0.1.1 → 0.2.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
@@ -69,9 +69,10 @@ absent (no echo, no signature) per [spec §5.4][spec-nonce].
69
69
  Response headers on success:
70
70
 
71
71
  | Header | Value |
72
- |--------|-------|
72
+ | ------ | ----- |
73
73
  | `Content-Type` | `application/tai64n` |
74
74
  | `Content-Length` | `25` |
75
+ | `Content-Disposition` | `inline` — browsers render the label in place rather than download it |
75
76
  | `Cache-Control` | `no-store` |
76
77
  | `TAI-Leap-Seconds` | decimal count (e.g. `37`), always present |
77
78
 
@@ -94,13 +95,19 @@ entirely.
94
95
  newTaistampHandler(); // cors: '*' (default)
95
96
  newTaistampHandler({ cors: 'https://example.com' }); // scoped origin
96
97
  newTaistampHandler({ cors: false }); // CORS-specific headers off
98
+ newTaistampHandler({ corsMaxAge: 86400 }); // pre-flight cache 24h
97
99
  ```
98
100
 
101
+ `corsMaxAge` (seconds, default `600`) sets the pre-flight
102
+ `Access-Control-Max-Age`. The spec §5.2 floor is 600, so a
103
+ smaller value clamps up to it; the field is ignored when
104
+ `cors` is `false`.
105
+
99
106
  When CORS is enabled, responses carry:
100
107
 
101
108
  | Response | CORS headers added | `Vary: Origin` (scoped origin only) |
102
- |----------|--------------------|------|
103
- | `OPTIONS` 200 | `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods: GET, HEAD`, `Access-Control-Allow-Headers: TAI-Nonce`, `Access-Control-Expose-Headers: TAI-Leap-Seconds, TAI-Nonce, TAI-Key-Selector, TAI-Signature`, `Access-Control-Max-Age: 600` | yes |
109
+ | -------- | ------------------ | ----------------------------------- |
110
+ | `OPTIONS` 200 | `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods: GET, HEAD`, `Access-Control-Allow-Headers: TAI-Nonce`, `Access-Control-Expose-Headers: TAI-Leap-Seconds, TAI-Nonce, TAI-Key-Selector, TAI-Signature`, `Access-Control-Max-Age` (default `600`, see `corsMaxAge`) | yes |
104
111
  | `GET` / `HEAD` 200 | `Access-Control-Allow-Origin`, `Access-Control-Expose-Headers` (so browser JS can read the `TAI-*` headers) | yes |
105
112
  | `405` | `Access-Control-Allow-Origin` | yes |
106
113
 
@@ -142,10 +149,27 @@ factory are re-exported from
142
149
  package and don't need to depend on the underlying
143
150
  package directly.
144
151
 
152
+ Operators usually hold a `selector:base64` seed
153
+ secret rather than a `CryptoKey`. `parseSecretToKey`
154
+ (also re-exported) parses it into a `KeyConfig` whose
155
+ `selector` and `signer` plug straight into the
156
+ handler config:
157
+
158
+ ```typescript
159
+ import {
160
+ newTaistampHandler,
161
+ parseSecretToKey,
162
+ } from '@kagal/taistamp';
163
+
164
+ const { selector, signer } =
165
+ await parseSecretToKey(env.TAISTAMP_SECRET);
166
+ const taistamp = newTaistampHandler({ selector, signer });
167
+ ```
168
+
145
169
  `signer` and `selector` are co-required: pass both to
146
170
  sign, neither for an unsigned handler. Construction
147
171
  throws if only one is supplied, or if `selector` does
148
- not match `/^[A-Za-z](?:[\dA-Za-z_-]{0,61}[\dA-Za-z])?$/`
172
+ not match `[A-Za-z]([A-Za-z0-9_-]{0,61}[A-Za-z0-9])?`
149
173
  (a single DNS-safe label that starts with a letter,
150
174
  ends with a letter or digit, and is also a valid
151
175
  Structured Field token).
@@ -215,7 +239,7 @@ v=tai1; k=ed25519; p=<base64-of-32-raw-pubkey-bytes>
215
239
  ```
216
240
 
217
241
  | Tag | Value |
218
- |-----|-------|
242
+ | --- | ----- |
219
243
  | `v` | Protocol version. `tai1` for the framing in this README. |
220
244
  | `k` | Key algorithm. `ed25519` for the only algorithm currently defined. |
221
245
  | `p` | Public key, standard base64. For Ed25519: 32 raw bytes → 43-44 chars. |
@@ -238,22 +262,42 @@ or fall back to a strict-verify library such as
238
262
 
239
263
  ```typescript
240
264
  import {
241
- asNonce,
242
265
  composeSignaturePayload,
243
266
  extractLeapSeconds,
267
+ extractNonce,
268
+ extractSignature,
269
+ newNonce,
270
+ parseRecordToVerifier,
244
271
  readLabel,
272
+ tai64nLabelToUTC,
245
273
  } from '@kagal/taistamp';
246
274
 
275
+ // Mint the request nonce. `newNonce` returns a branded
276
+ // `Nonce` — conformant sf-binary by construction — so
277
+ // the same value flows into the signing path below.
278
+ const nonce = newNonce();
247
279
  const response = await fetch(taistampURL, {
248
- headers: { 'TAI-Nonce': clientNonce },
280
+ headers: { 'TAI-Nonce': nonce },
249
281
  });
250
282
  // `application/tai64n` is octet-typed, not text. `readLabel`
251
283
  // reads the raw body and decodes the 25-octet ASCII label;
252
284
  // never `response.text()`, which UTF-8-decodes and would
253
285
  // mangle a non-ASCII octet instead of surfacing it.
254
286
  const label = await readLabel(response);
255
- const selector = response.headers.get('TAI-Key-Selector')!;
256
- const sigSf = response.headers.get('TAI-Signature')!;
287
+ const selector = response.headers.get('TAI-Key-Selector');
288
+ if (!selector) {
289
+ throw new Error('TAI-Key-Selector missing');
290
+ }
291
+
292
+ // Spec §6: `TAI-Signature` carries the raw 64-octet
293
+ // Ed25519 signature as an sf-binary item.
294
+ // `extractSignature` returns the decoded bytes, or
295
+ // `undefined` whenever the field is missing,
296
+ // duplicated, malformed, or the wrong length.
297
+ const signature = extractSignature(response.headers);
298
+ if (signature === undefined) {
299
+ throw new Error('TAI-Signature missing or malformed');
300
+ }
257
301
 
258
302
  // Spec §5.3: a `TAI-Leap-Seconds` value outside the
259
303
  // signed-payload u32 range MUST be treated as unsigned.
@@ -267,20 +311,33 @@ if (leap === undefined) {
267
311
  throw new Error('TAI-Leap-Seconds out of range; treat as unsigned');
268
312
  }
269
313
 
270
- // Brand the recorded nonce so it can flow into the
271
- // signing path. `asNonce` returns `undefined` for any
272
- // value that fails sf-binary syntax or the wire
273
- // length range the same "treat as absent" verdict
274
- // the server applied per spec §5.4.
275
- const nonce = asNonce(clientNonce);
276
- if (nonce === undefined) {
277
- throw new Error('client nonce is not a valid sf-binary item');
314
+ // The response must echo `TAI-Nonce`. With no echo there
315
+ // is no proof of freshness the response may be a replay
316
+ // of an earlier exchange (Plain, trust level 0, spec §8),
317
+ // and any signature present MUST be ignored. This is
318
+ // strictly worse than a fresh-but-unsigned response, not
319
+ // equivalent to it.
320
+ const echo = extractNonce(response.headers);
321
+ if (echo === undefined) {
322
+ throw new Error('no TAI-Nonce echo: response is unverifiable');
323
+ }
324
+ // A present echo that differs from what we sent binds the
325
+ // response to a different exchange (Inconsistent, level
326
+ // -1) and MUST be rejected.
327
+ if (echo !== nonce) {
328
+ throw new Error('TAI-Nonce echo does not match the nonce sent');
278
329
  }
279
330
 
280
- // Look up the public key in DNS at
281
- // `${selector}._taistamp.${host}` and parse the
282
- // `p=` tag from the TXT record.
283
- const publicKey = await loadPublicKey(host, selector);
331
+ // Look up the TXT record in DNS at
332
+ // `${selector}._taistamp.${host}`; `parseRecordToVerifier`
333
+ // parses it and imports the published key into a
334
+ // ready-to-use `Verifier` in `p`. A revoked record
335
+ // (empty `p=`) carries through as `p: undefined`.
336
+ const txtValue = await loadTXT(host, selector);
337
+ const record = await parseRecordToVerifier(txtValue);
338
+ if (record.p === undefined) {
339
+ throw new Error('key record is revoked');
340
+ }
284
341
 
285
342
  const payload = composeSignaturePayload(
286
343
  label,
@@ -288,31 +345,46 @@ const payload = composeSignaturePayload(
288
345
  selector,
289
346
  nonce,
290
347
  );
291
- const valid = await crypto.subtle.verify(
292
- 'Ed25519',
293
- publicKey,
294
- sfBinaryDecode(sigSf), // strip leading/trailing ':' then base64-decode
295
- payload,
296
- );
348
+ const valid = await record.p.verify(signature, payload);
349
+ if (!valid) {
350
+ throw new Error('signature does not verify');
351
+ }
352
+
353
+ // The signature checks out, so the label is trustworthy.
354
+ // `tai64nLabelToUTC` decodes it to `Date.now()`-shaped
355
+ // milliseconds — the server's signed time — honouring the
356
+ // leap count the response declared.
357
+ const verifiedAt = tai64nLabelToUTC(label, leap);
358
+ if (verifiedAt === undefined) {
359
+ throw new Error('TAI64N label malformed');
360
+ }
297
361
  ```
298
362
 
299
363
  `composeSignaturePayload(label, leapSeconds, selector,
300
364
  nonce)` reconstructs the exact byte sequence the
301
- server signed; the verifier supplies only the public
302
- key and an sf-binary decoder. `leapSeconds` must be a
365
+ server signed; the verifier supplies only the DNS
366
+ lookup `extractSignature` hands the raw signature
367
+ bytes straight to the `Verifier`. `leapSeconds` must be a
303
368
  branded `LeapSeconds` — obtain one from
304
369
  `extractLeapSeconds(headers)` (the verifier path) or
305
- `asLeapSeconds(number)` (when you already have the
370
+ `asLeapSeconds(number | undefined)` (when you already have the
306
371
  value). Both return `undefined` for out-of-range
307
372
  input, collapsing every "treat as unsigned" case in
308
373
  [spec §5.3][spec-leap] into one verdict. `nonce` must be a branded
309
- `Nonce` — wrap the recorded client nonce with
374
+ `Nonce` — `newNonce()` mints one directly; a nonce
375
+ recorded as a plain string is branded with
310
376
  `asNonce(value)`, which returns `undefined` for any
311
377
  value that would have been treated as absent on the
312
378
  server (missing, empty, malformed sf-binary, or out
313
379
  of length range — see [spec §5.4][spec-nonce]).
314
- Comparing the verifier's recorded nonce against the
315
- response's `TAI-Nonce` defends against replay.
380
+ `extractNonce(response.headers)` reads the echoed
381
+ `TAI-Nonce`. With no echo the response carries no proof
382
+ of freshness — it may be a replay of an earlier exchange,
383
+ so any signature MUST be ignored; this is weaker than a
384
+ fresh-but-unsigned response, not equivalent to it. A
385
+ present echo that differs from the nonce you sent binds
386
+ the response to a different exchange and MUST be
387
+ rejected.
316
388
 
317
389
  ## API
318
390
 
@@ -327,12 +399,14 @@ response's `TAI-Nonce` defends against replay.
327
399
  [Signing the response](#signing-the-response) for
328
400
  signed responses, and
329
401
  [CORS](#cors) for cross-origin policy.
330
- - `TaistampHandlerConfig` — `{ cors?, selector?,
331
- signer? }`. `cors` accepts `'*'` (default), a
332
- specific origin string, or `false`; `signer` and
333
- `selector` are co-required.
402
+ - `TaistampHandlerConfig` — `{ cors?, corsMaxAge?,
403
+ selector?, signer? }`. `cors` accepts `'*'`
404
+ (default), a specific origin string, or `false`;
405
+ `corsMaxAge` is the pre-flight `Access-Control-Max-Age`
406
+ in seconds (default `600`, clamped up to a 600 floor);
407
+ `signer` and `selector` are co-required.
334
408
 
335
- ### Signer
409
+ ### Signer and verifier
336
410
 
337
411
  Re-exported from `@kagal/ed25519-secret`:
338
412
 
@@ -341,6 +415,26 @@ Re-exported from `@kagal/ed25519-secret`:
341
415
  - `newEd25519Signer(key)` — WebCrypto Ed25519
342
416
  signer factory. Pass an Ed25519 private
343
417
  `CryptoKey` with `'sign'` in `usages`.
418
+ - `parseSecretToKey(secret, context?)` — parse a
419
+ `selector:base64` seed secret into a `KeyConfig`;
420
+ its `selector` and `signer` plug straight into
421
+ `newTaistampHandler`.
422
+ - `parseSecretsToKeys(secrets, strict?, context?)` —
423
+ the same for a string carrying several secrets
424
+ separated by whitespace or punctuation. Strict mode
425
+ (the default) rejects the whole call on a malformed
426
+ entry; lenient mode skips it.
427
+ - `KeyConfig` — parsed secret: the `selector`,
428
+ ready-to-use `signer` / `verifier`, and the
429
+ underlying key material.
430
+ - `parseRecordToVerifier(record, context?)` — parse a
431
+ DNS TXT key record into a `KeyRecord` whose `p` is
432
+ a ready-to-use `Verifier`, or `undefined` for a
433
+ revoked record (empty `p=`).
434
+ - `KeyRecord` — parsed TXT record: the tag-value
435
+ pairs with `p` upgraded to the parse target.
436
+ - `Verifier` — `{ verify: (signature, message) =>
437
+ Promise<boolean> }`.
344
438
 
345
439
  ### Verification helpers
346
440
 
@@ -360,9 +454,9 @@ For verifier-side validation of a signed response
360
454
  - `composeSignaturePayload(label, leapSeconds,
361
455
  selector, nonce)` — reconstructs the exact byte
362
456
  sequence the server signed.
363
- - `asLeapSeconds(number)` — brand a numeric
364
- leap-second count; returns `undefined` for
365
- out-of-range input.
457
+ - `asLeapSeconds(number | undefined)` — brand a numeric
458
+ leap-second count; returns `undefined` for an absent
459
+ or out-of-range input.
366
460
  - `extractLeapSeconds(headers)` — parse
367
461
  `TAI-Leap-Seconds` from response headers; returns
368
462
  `undefined` if missing, non-numeric, non-integer,
@@ -373,42 +467,107 @@ For verifier-side validation of a signed response
373
467
  returns `undefined` for any value that fails
374
468
  sf-binary syntax or the length range checked
375
469
  per [spec §5.4][spec-nonce].
470
+ - `extractNonce(headers)` — read and brand the
471
+ `TAI-Nonce` echo from a response (or the request,
472
+ server-side); returns `undefined` when the field is
473
+ missing or fails `asNonce`. On the verifying side
474
+ `undefined` means no proof of freshness — the response
475
+ may be a replay and any signature MUST be ignored,
476
+ weaker than a fresh-but-unsigned response; a present
477
+ echo must match the nonce you sent.
478
+ - `newNonce(byteLength?, context?)` — mint a fresh
479
+ client nonce: random bytes framed as an sf-binary
480
+ item, returned already branded — conformant by
481
+ construction, no `asNonce` round-trip. `byteLength`
482
+ defaults to 16; throws `TypeError` outside
483
+ [spec §5.4][spec-nonce]'s 7..129 decoded octets.
376
484
  - `Nonce` — branded sf-binary nonce accepted by
377
485
  `composeSignaturePayload`.
486
+ - `asSignature(value)` — decode a recorded
487
+ `TAI-Signature` value ([spec §6][spec-sign]) to the
488
+ raw 64-octet Ed25519 signature; returns `undefined`
489
+ for any value that fails sf-binary syntax or does
490
+ not decode to exactly 64 octets.
491
+ - `extractSignature(headers)` — read `TAI-Signature`
492
+ from response headers; returns `undefined` if the
493
+ field is missing or fails `asSignature` validation.
494
+ - `tai64nLabelFromUTC(utc)` — the TAI64N label for a
495
+ UTC millisecond timestamp. Labels are fixed-width
496
+ hex and order lexicographically, so a received
497
+ label can be freshness-checked between the labels
498
+ of `Date.now() - skew` and `Date.now() + skew`
499
+ without decoding it.
500
+ - `tai64nLabelToUTC(label, leapSeconds?)` — the
501
+ inverse: recover the `Date.now()`-shaped UTC
502
+ milliseconds behind a verified label, so a checked
503
+ response yields a usable time. Returns `undefined`
504
+ for a malformed label; `leapSeconds` defaults to the
505
+ current `TAI_LEAP_SECONDS`.
506
+
507
+ ### sf-binary helpers
508
+
509
+ `TAI-Nonce` and `TAI-Signature` travel as sf-binary
510
+ items ([RFC 9651 §3.3.5][sf-binary]) — standard base64
511
+ wrapped in colons. The `@kagal/taistamp/utils` subpath
512
+ exposes the framing helpers the handler uses:
513
+
514
+ ```typescript
515
+ import { decodeSFBinary } from '@kagal/taistamp/utils';
516
+ ```
517
+
518
+ | Export | Description |
519
+ | ------ | ----------- |
520
+ | `SF_BINARY_PATTERN` | `RegExp` matching the full sf-binary syntax |
521
+ | `encodeSFBinary(bytes)` | Bytes → `:base64:` item |
522
+ | `decodeSFBinary(value, context?)` | `:base64:` item → bytes; throws `TypeError` on invalid syntax; `context` prefixes the message |
523
+
524
+ `decodeSFBinary` enforces the full syntax — anything
525
+ `SF_BINARY_PATTERN` rejects throws — so decoding a
526
+ wire value needs no separate validation step; the
527
+ pattern stands alone for validating without decoding.
378
528
 
379
529
  ### TAI64N helpers
380
530
 
381
- The handler uses these primitives internally; they
382
- are re-exported for callers that need raw TAI64N
383
- construction:
531
+ Constructing TAI64N timestamps and their labels is
532
+ a core capability of the package the
533
+ `@kagal/taistamp/utils` subpath exposes it for
534
+ direct use, and the handler builds on the same
535
+ primitives:
536
+
537
+ ```typescript
538
+ import { tai64nLabel } from '@kagal/taistamp/utils';
539
+ ```
384
540
 
385
541
  | Export | Description |
386
- |--------|-------------|
542
+ | ------ | ----------- |
387
543
  | `now()` | Current TAI as `{ sec, nano, offset }` |
388
544
  | `fromUTC(utc)` | `Date.now()`-shaped milliseconds → TAI timestamp |
389
545
  | `tai64nLabel(t?)` | 25-byte label string for a timestamp (or `now()`) |
390
- | `tai64nLabelFromUTC(utc)` | Shortcut for `tai64nLabel(fromUTC(utc))` |
546
+ | `tai64nLabelFromUTC(utc)` | Shortcut for `tai64nLabel(fromUTC(utc))`; also on the main export |
547
+ | `tai64nLabelToUTC(label, leapSeconds?)` | Inverse of `tai64nLabelFromUTC` — label → `Date.now()`-shaped ms, or `undefined` if malformed; also on the main export |
548
+ | `TAI64N_LABEL_PATTERN` | `RegExp` matching the TAI64N label wire form (`@` + 24 hex digits) |
549
+ | `TAI64N_LABEL_LENGTH` | `25` — byte length of a TAI64N label |
550
+ | `TAI64N_CONTENT_TYPE` | `application/tai64n` — media type of a label body |
551
+ | `TAI64N_EPOCH_HI` | `0x40000000` — TAI64 epoch high word folded into a label's seconds field |
391
552
 
392
553
  `fromUTC` applies the constant `TAI_LEAP_SECONDS`
393
- (currently 37 seconds). Historic UTC timestamps
394
- spanning a leap-second boundary need caller-side
395
- adjustment the constant tracks the present, not
396
- history.
554
+ (currently 37 seconds). These helpers deal in the
555
+ current time: the offset tracks the present, and
556
+ anything before the unix epoch is out of scope.
397
557
 
398
558
  ### Constants
399
559
 
400
560
  | Name | Value |
401
- |------|-------|
561
+ | ---- | ----- |
402
562
  | `TAISTAMP_PATH` | `/.well-known/taistamp` |
403
- | `TAI64N_CONTENT_TYPE` | `application/tai64n` |
404
- | `TAI64N_CONTENT_LENGTH` | `25` |
405
- | `TAI64N_HEADER_KEY_SELECTOR` | `TAI-Key-Selector` |
406
- | `TAI64N_HEADER_LEAP_SECONDS` | `TAI-Leap-Seconds` |
407
- | `TAI64N_HEADER_NONCE` | `TAI-Nonce` |
408
- | `TAI64N_HEADER_SIGNATURE` | `TAI-Signature` |
563
+ | `TAISTAMP_CONTENT_TYPE` | `application/tai64n` |
564
+ | `TAISTAMP_CONTENT_LENGTH` | `25` |
565
+ | `TAISTAMP_HEADER_KEY_SELECTOR` | `TAI-Key-Selector` |
566
+ | `TAISTAMP_HEADER_LEAP_SECONDS` | `TAI-Leap-Seconds` |
567
+ | `TAISTAMP_HEADER_NONCE` | `TAI-Nonce` |
568
+ | `TAISTAMP_HEADER_SIGNATURE` | `TAI-Signature` |
409
569
  | `TAI_LEAP_SECONDS` | `37` (current TAI − UTC offset) |
410
570
  | `TAI_LEAP_SECONDS_MAX` | `0xFFFFFFFF` (signed-payload u32 cap) |
411
- | `TAI64_EPOCH_HI` | `0x40000000` |
412
571
 
413
572
  ## Licence
414
573
 
@@ -423,7 +582,9 @@ history.
423
582
  [npm-badge]: https://img.shields.io/npm/v/@kagal/taistamp.svg
424
583
  [npm-url]: https://www.npmjs.com/package/@kagal/taistamp
425
584
  [rfc-repo]: https://github.com/karasz/rfc-taistamp
585
+ [sf-binary]: https://datatracker.ietf.org/doc/html/rfc9651#section-3.3.5
426
586
  [spec-leap]: https://datatracker.ietf.org/doc/html/draft-mery-nagy-taistamp-00#section-5.3
427
587
  [spec-nonce]: https://datatracker.ietf.org/doc/html/draft-mery-nagy-taistamp-00#section-5.4
588
+ [spec-sign]: https://datatracker.ietf.org/doc/html/draft-mery-nagy-taistamp-00#section-6
428
589
  [spec-verify]: https://datatracker.ietf.org/doc/html/draft-mery-nagy-taistamp-00#section-9
429
590
  [tai64n]: https://cr.yp.to/libtai/tai64.html
@@ -0,0 +1,144 @@
1
+ /** `@` followed by 24 hex digits — the TAI64N label wire form. */
2
+ declare const TAI64N_LABEL_PATTERN: RegExp;
3
+ /** Byte length of a TAI64N label: `@` + 16 sec + 8 nano hex digits. */
4
+ declare const TAI64N_LABEL_LENGTH: number;
5
+ /** Media type of a TAI64N label body. */
6
+ declare const TAI64N_CONTENT_TYPE = "application/tai64n";
7
+ /** High 32 bits of the TAI64 second count at the unix epoch. */
8
+ declare const TAI64N_EPOCH_HI = 1073741824;
9
+ /** @deprecated Renamed to {@link TAI64N_EPOCH_HI}. */
10
+ declare const TAI64_EPOCH_HI = 1073741824;
11
+ declare const TAISTAMP_PATH = "/.well-known/taistamp";
12
+ /** The taistamp response `Content-Type`. */
13
+ declare const TAISTAMP_CONTENT_TYPE = "application/tai64n";
14
+ /** The taistamp response `Content-Length`. */
15
+ declare const TAISTAMP_CONTENT_LENGTH: number;
16
+ declare const TAISTAMP_HEADER_KEY_SELECTOR = "TAI-Key-Selector";
17
+ declare const TAISTAMP_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
18
+ declare const TAISTAMP_HEADER_NONCE = "TAI-Nonce";
19
+ declare const TAISTAMP_HEADER_SIGNATURE = "TAI-Signature";
20
+ /** @deprecated Renamed to {@link TAISTAMP_PATH}. */
21
+ declare const TAI64N_PATH = "/.well-known/taistamp";
22
+ /** @deprecated Renamed to {@link TAISTAMP_CONTENT_LENGTH}. */
23
+ declare const TAI64N_CONTENT_LENGTH: number;
24
+ /** @deprecated Renamed to {@link TAISTAMP_HEADER_KEY_SELECTOR}. */
25
+ declare const TAI64N_HEADER_KEY_SELECTOR = "TAI-Key-Selector";
26
+ /** @deprecated Renamed to {@link TAISTAMP_HEADER_LEAP_SECONDS}. */
27
+ declare const TAI64N_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
28
+ /** @deprecated Renamed to {@link TAISTAMP_HEADER_NONCE}. */
29
+ declare const TAI64N_HEADER_NONCE = "TAI-Nonce";
30
+ /** @deprecated Renamed to {@link TAISTAMP_HEADER_SIGNATURE}. */
31
+ declare const TAI64N_HEADER_SIGNATURE = "TAI-Signature";
32
+ /**
33
+ * Upper bound for `leapSeconds` in the taistamp signed
34
+ * payload. The framing encodes the value as a 4-byte
35
+ * big-endian unsigned integer, so any input outside
36
+ * `[0, 2^32-1]` cannot be represented. Verifiers MUST
37
+ * treat an out-of-range `TAI-Leap-Seconds` response
38
+ * header as unsigned, per spec §5.3.
39
+ */
40
+ declare const TAI_LEAP_SECONDS_MAX = 4294967295;
41
+ declare const LeapSecondsBrand: unique symbol;
42
+ /**
43
+ * `number` that has been confirmed to fit the
44
+ * `[0, TAI_LEAP_SECONDS_MAX]` u32be range required by
45
+ * the taistamp signed-payload framing. Construct only
46
+ * via {@link extractLeapSeconds} or {@link asLeapSeconds};
47
+ * the brand prevents an arbitrary number from reaching
48
+ * the signing path.
49
+ */
50
+ type LeapSeconds = number & {
51
+ readonly [LeapSecondsBrand]: never;
52
+ };
53
+ /**
54
+ * Coerce a `number` to a {@link LeapSeconds}. Returns
55
+ * `undefined` when `value` is absent, non-integer,
56
+ * negative, or exceeds {@link TAI_LEAP_SECONDS_MAX}.
57
+ */
58
+ declare const asLeapSeconds: (value: number | undefined) => LeapSeconds | undefined;
59
+ /**
60
+ * Current TAI − UTC offset in whole seconds, used by
61
+ * `fromUTC()` and emitted in the `TAI-Leap-Seconds`
62
+ * response header. The value 37 has been in force
63
+ * since 2017-01-01; update on the next IERS leap-second
64
+ * announcement.
65
+ *
66
+ * @remarks
67
+ * Stays a single `LeapSeconds` until a leap-seconds
68
+ * table is added so the offset can be computed for any
69
+ * TAI second; this constant becomes redundant then.
70
+ */
71
+ declare const TAI_LEAP_SECONDS: LeapSeconds;
72
+ /**
73
+ * Extract a usable leap-seconds count from response
74
+ * headers. Returns `undefined` when the
75
+ * `TAI-Leap-Seconds` field is missing, empty,
76
+ * non-numeric, non-integer, negative, or out-of-range
77
+ * — every "treat as unsigned" case in spec §5.3
78
+ * collapsed into one verdict.
79
+ */
80
+ declare const extractLeapSeconds: (headers: Headers) => LeapSeconds | undefined;
81
+ type timestamp = {
82
+ nano: number;
83
+ sec: number;
84
+ offset?: number;
85
+ };
86
+ /**
87
+ * Convert a UTC timestamp in milliseconds (the
88
+ * `Date.now()` shape) to TAI seconds and nanoseconds,
89
+ * applying the current `TAI_LEAP_SECONDS` offset. The
90
+ * package deals in the current time: the offset tracks
91
+ * the present, and anything before the unix epoch is
92
+ * out of scope.
93
+ */
94
+ declare const fromUTC: (utc: number) => timestamp;
95
+ /**
96
+ * The current TAI time as seconds and nanoseconds —
97
+ * {@link fromUTC} applied to `Date.now()`.
98
+ */
99
+ declare const now: () => timestamp;
100
+ /**
101
+ * Format a TAI timestamp as the 25-byte TAI64N label
102
+ * served at `/.well-known/taistamp`: `@` followed by
103
+ * 16 hex digits of TAI64 seconds (the 2^62 epoch
104
+ * offset applied) and 8 hex digits of nanoseconds.
105
+ * Defaults to {@link now} when no value is given.
106
+ */
107
+ declare const tai64nLabel: (value?: timestamp) => string;
108
+ /**
109
+ * The 25-byte TAI64N label for a UTC timestamp in
110
+ * milliseconds — shorthand for
111
+ * `tai64nLabel(fromUTC(utc))`.
112
+ *
113
+ * Labels are fixed-width hex, so they order
114
+ * lexicographically: a verifier can bounds-check a
115
+ * received label between the labels of
116
+ * `Date.now() - skew` and `Date.now() + skew` without
117
+ * decoding it.
118
+ */
119
+ declare const tai64nLabelFromUTC: (utc: number) => string;
120
+ /**
121
+ * Recover a UTC timestamp in milliseconds (the
122
+ * `Date.now()` shape) from a TAI64N label — the inverse
123
+ * of {@link tai64nLabelFromUTC}. A label minted from a
124
+ * millisecond value round-trips back to it exactly; a
125
+ * `Date` is one `new Date(ms)` away.
126
+ *
127
+ * Returns `undefined` for any value that is not `@`
128
+ * followed by 24 hex digits (either case) — the
129
+ * verify-side "malformed is absent" collapse shared with
130
+ * `asSignature` and `asNonce`, so it drops straight into
131
+ * a gate pipeline.
132
+ *
133
+ * `leapSeconds` is the TAI − UTC offset removed when
134
+ * mapping TAI back to UTC; it defaults to the current
135
+ * {@link TAI_LEAP_SECONDS}, mirroring {@link fromUTC}.
136
+ * Pass a response's `extractLeapSeconds(headers)` to
137
+ * honour a server that declared a different count — an
138
+ * absent or malformed header yields `undefined` there
139
+ * and falls through to the default, while a genuine `0`
140
+ * is honoured.
141
+ */
142
+ declare const tai64nLabelToUTC: (label: string, leapSeconds?: LeapSeconds) => number | undefined;
143
+ export { LeapSeconds, TAI64N_CONTENT_LENGTH, TAI64N_CONTENT_TYPE, TAI64N_EPOCH_HI, TAI64N_HEADER_KEY_SELECTOR, TAI64N_HEADER_LEAP_SECONDS, TAI64N_HEADER_NONCE, TAI64N_HEADER_SIGNATURE, TAI64N_LABEL_LENGTH, TAI64N_LABEL_PATTERN, TAI64N_PATH, TAI64_EPOCH_HI, TAISTAMP_CONTENT_LENGTH, TAISTAMP_CONTENT_TYPE, TAISTAMP_HEADER_KEY_SELECTOR, TAISTAMP_HEADER_LEAP_SECONDS, TAISTAMP_HEADER_NONCE, TAISTAMP_HEADER_SIGNATURE, TAISTAMP_PATH, TAI_LEAP_SECONDS, TAI_LEAP_SECONDS_MAX, asLeapSeconds, extractLeapSeconds, fromUTC, now, tai64nLabel, tai64nLabelFromUTC, tai64nLabelToUTC };
144
+ //# sourceMappingURL=time.d.mts.map
@@ -0,0 +1,68 @@
1
+ import { decodeBase64, encodeBase64, isInRange } from "@kagal/ed25519-secret";
2
+ const TAI64N_LABEL_PATTERN = /^@[\da-fA-F]{24}$/;
3
+ const TAI64N_LABEL_LENGTH = 25;
4
+ const TAI64N_CONTENT_TYPE = "application/tai64n";
5
+ const TAI64N_EPOCH_HI = 1073741824;
6
+ const TAI64_EPOCH_HI = TAI64N_EPOCH_HI;
7
+ const TAISTAMP_PATH = "/.well-known/taistamp";
8
+ const TAISTAMP_CONTENT_TYPE = TAI64N_CONTENT_TYPE;
9
+ const TAISTAMP_CONTENT_LENGTH = 25;
10
+ const TAISTAMP_HEADER_KEY_SELECTOR = "TAI-Key-Selector";
11
+ const TAISTAMP_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
12
+ const TAISTAMP_HEADER_NONCE = "TAI-Nonce";
13
+ const TAISTAMP_HEADER_SIGNATURE = "TAI-Signature";
14
+ const TAI64N_PATH = TAISTAMP_PATH;
15
+ const TAI64N_CONTENT_LENGTH = 25;
16
+ const TAI64N_HEADER_KEY_SELECTOR = TAISTAMP_HEADER_KEY_SELECTOR;
17
+ const TAI64N_HEADER_LEAP_SECONDS = TAISTAMP_HEADER_LEAP_SECONDS;
18
+ const TAI64N_HEADER_NONCE = TAISTAMP_HEADER_NONCE;
19
+ const TAI64N_HEADER_SIGNATURE = TAISTAMP_HEADER_SIGNATURE;
20
+ const TAI_LEAP_SECONDS_MAX = 4294967295;
21
+ const asLeapSeconds = (value) => {
22
+ if (!isInRange(value, 0, 4294967295)) return void 0;
23
+ return value;
24
+ };
25
+ const TAI_LEAP_SECONDS = 37;
26
+ const DECIMAL_INTEGER = /^(?:0|[1-9]\d*)$/;
27
+ const extractLeapSeconds = (headers) => {
28
+ const raw = headers.get(TAISTAMP_HEADER_LEAP_SECONDS);
29
+ if (!raw || !DECIMAL_INTEGER.test(raw)) return void 0;
30
+ return asLeapSeconds(Number(raw));
31
+ };
32
+ const SF_BINARY_PATTERN = /^:(?:(?:[\d+/A-Za-z]{4})*(?:[\d+/A-Za-z]{4}|[\d+/A-Za-z]{3}=|[\d+/A-Za-z]{2}==))?:$/;
33
+ const encodeSFBinary = (bytes) => `:${encodeBase64(bytes)}:`;
34
+ const decodeSFBinary = (value, context) => {
35
+ if (!SF_BINARY_PATTERN.test(value)) {
36
+ const prefix = context ? `${context}: ` : "";
37
+ throw new TypeError(`${prefix}invalid sf-binary`);
38
+ }
39
+ return decodeBase64(value.slice(1, -1), context);
40
+ };
41
+ const fromUTC = (utc) => {
42
+ return {
43
+ sec: Math.floor(utc / 1e3) + 37,
44
+ nano: utc % 1e3 * 1e6,
45
+ offset: 37
46
+ };
47
+ };
48
+ const now = () => {
49
+ return fromUTC(Date.now());
50
+ };
51
+ const tai64nLabel = (value) => {
52
+ const { sec, nano } = value ?? now();
53
+ const secHi = Math.trunc(sec / u32Range) + TAI64N_EPOCH_HI;
54
+ const secLo = sec % u32Range;
55
+ return `@${secHi.toString(16).padStart(8, "0")}${secLo.toString(16).padStart(8, "0")}${nano.toString(16).padStart(8, "0")}`;
56
+ };
57
+ const tai64nLabelFromUTC = (utc) => tai64nLabel(fromUTC(utc));
58
+ const tai64nLabelToUTC = (label, leapSeconds = 37) => {
59
+ if (!TAI64N_LABEL_PATTERN.test(label)) return void 0;
60
+ const secHi = Number.parseInt(label.slice(1, 9), 16) - TAI64N_EPOCH_HI;
61
+ const secLo = Number.parseInt(label.slice(9, 17), 16);
62
+ const nano = Number.parseInt(label.slice(17, 25), 16);
63
+ return (secHi * u32Range + secLo - leapSeconds) * 1e3 + nano / 1e6;
64
+ };
65
+ const u32Range = 4294967296;
66
+ export { SF_BINARY_PATTERN, TAI64N_CONTENT_LENGTH, TAI64N_CONTENT_TYPE, TAI64N_EPOCH_HI, TAI64N_HEADER_KEY_SELECTOR, TAI64N_HEADER_LEAP_SECONDS, TAI64N_HEADER_NONCE, TAI64N_HEADER_SIGNATURE, TAI64N_LABEL_LENGTH, TAI64N_LABEL_PATTERN, TAI64N_PATH, TAI64_EPOCH_HI, TAISTAMP_CONTENT_LENGTH, TAISTAMP_CONTENT_TYPE, TAISTAMP_HEADER_KEY_SELECTOR, TAISTAMP_HEADER_LEAP_SECONDS, TAISTAMP_HEADER_NONCE, TAISTAMP_HEADER_SIGNATURE, TAISTAMP_PATH, TAI_LEAP_SECONDS, TAI_LEAP_SECONDS_MAX, asLeapSeconds, decodeSFBinary, encodeSFBinary, extractLeapSeconds, fromUTC, now, tai64nLabel, tai64nLabelFromUTC, tai64nLabelToUTC };
67
+
68
+ //# sourceMappingURL=time.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.mjs","names":[],"sources":["../../src/const.ts","../../src/leap-seconds.ts","../../src/sf-binary.ts","../../src/time.ts"],"sourcesContent":["//\n// tai64n format\n//\n// Constants describing the TAI64N label itself, surfaced on\n// the `@kagal/taistamp/utils` subpath.\n//\n\n/** `@` followed by 24 hex digits — the TAI64N label wire form. */\nexport const TAI64N_LABEL_PATTERN = /^@[\\da-fA-F]{24}$/;\n\n/** Byte length of a TAI64N label: `@` + 16 sec + 8 nano hex digits. */\nexport const TAI64N_LABEL_LENGTH = 1 + 16 + 8;\n\n/** Media type of a TAI64N label body. */\nexport const TAI64N_CONTENT_TYPE = 'application/tai64n';\n\n/** High 32 bits of the TAI64 second count at the unix epoch. */\nexport const TAI64N_EPOCH_HI = 0x40_00_00_00;\n\n/** @deprecated Renamed to {@link TAI64N_EPOCH_HI}. */\nexport const TAI64_EPOCH_HI = TAI64N_EPOCH_HI;\n\n//\n// taistamp protocol\n//\n// Constants describing the taistamp HTTP exchange, surfaced\n// on the main entry point.\n//\n\nexport const TAISTAMP_PATH = '/.well-known/taistamp';\n\n/** The taistamp response `Content-Type`. */\nexport const TAISTAMP_CONTENT_TYPE = TAI64N_CONTENT_TYPE;\n\n/** The taistamp response `Content-Length`. */\nexport const TAISTAMP_CONTENT_LENGTH = TAI64N_LABEL_LENGTH;\n\nexport const TAISTAMP_HEADER_KEY_SELECTOR = 'TAI-Key-Selector';\nexport const TAISTAMP_HEADER_LEAP_SECONDS = 'TAI-Leap-Seconds';\nexport const TAISTAMP_HEADER_NONCE = 'TAI-Nonce';\nexport const TAISTAMP_HEADER_SIGNATURE = 'TAI-Signature';\n\n//\n// Back-compat aliases for the released TAI64N_-prefixed\n// protocol names, surfaced on the /utils subpath.\n//\n\n/** @deprecated Renamed to {@link TAISTAMP_PATH}. */\nexport const TAI64N_PATH = TAISTAMP_PATH;\n\n/** @deprecated Renamed to {@link TAISTAMP_CONTENT_LENGTH}. */\nexport const TAI64N_CONTENT_LENGTH = TAISTAMP_CONTENT_LENGTH;\n\n/** @deprecated Renamed to {@link TAISTAMP_HEADER_KEY_SELECTOR}. */\nexport const TAI64N_HEADER_KEY_SELECTOR = TAISTAMP_HEADER_KEY_SELECTOR;\n\n/** @deprecated Renamed to {@link TAISTAMP_HEADER_LEAP_SECONDS}. */\nexport const TAI64N_HEADER_LEAP_SECONDS = TAISTAMP_HEADER_LEAP_SECONDS;\n\n/** @deprecated Renamed to {@link TAISTAMP_HEADER_NONCE}. */\nexport const TAI64N_HEADER_NONCE = TAISTAMP_HEADER_NONCE;\n\n/** @deprecated Renamed to {@link TAISTAMP_HEADER_SIGNATURE}. */\nexport const TAI64N_HEADER_SIGNATURE = TAISTAMP_HEADER_SIGNATURE;\n","// cspell:words IERS\n\nimport { isInRange } from '@kagal/ed25519-secret';\n\nimport { TAISTAMP_HEADER_LEAP_SECONDS } from './const';\n\n/**\n * Upper bound for `leapSeconds` in the taistamp signed\n * payload. The framing encodes the value as a 4-byte\n * big-endian unsigned integer, so any input outside\n * `[0, 2^32-1]` cannot be represented. Verifiers MUST\n * treat an out-of-range `TAI-Leap-Seconds` response\n * header as unsigned, per spec §5.3.\n */\nexport const TAI_LEAP_SECONDS_MAX = 0xFF_FF_FF_FF;\n\ndeclare const LeapSecondsBrand: unique symbol;\n\n/**\n * `number` that has been confirmed to fit the\n * `[0, TAI_LEAP_SECONDS_MAX]` u32be range required by\n * the taistamp signed-payload framing. Construct only\n * via {@link extractLeapSeconds} or {@link asLeapSeconds};\n * the brand prevents an arbitrary number from reaching\n * the signing path.\n */\nexport type LeapSeconds = number & { readonly [LeapSecondsBrand]: never };\n\n/**\n * Coerce a `number` to a {@link LeapSeconds}. Returns\n * `undefined` when `value` is absent, non-integer,\n * negative, or exceeds {@link TAI_LEAP_SECONDS_MAX}.\n */\nexport const asLeapSeconds = (\n value: number | undefined,\n): LeapSeconds | undefined => {\n if (!isInRange(value, 0, TAI_LEAP_SECONDS_MAX)) return undefined;\n return value as LeapSeconds;\n};\n\n/**\n * Current TAI − UTC offset in whole seconds, used by\n * `fromUTC()` and emitted in the `TAI-Leap-Seconds`\n * response header. The value 37 has been in force\n * since 2017-01-01; update on the next IERS leap-second\n * announcement.\n *\n * @remarks\n * Stays a single `LeapSeconds` until a leap-seconds\n * table is added so the offset can be computed for any\n * TAI second; this constant becomes redundant then.\n */\nexport const TAI_LEAP_SECONDS: LeapSeconds = 37 as LeapSeconds;\n\n/**\n * Strict decimal integer: a single `0` or a non-zero\n * leading digit followed by digits. Rejects hex\n * (`0x25`), float-style integers (`37.0`), signs,\n * whitespace, exponential notation, and leading zeros\n * — every input `Number()` would silently coerce to\n * an integer despite not being a canonical decimal.\n */\nconst DECIMAL_INTEGER = /^(?:0|[1-9]\\d*)$/;\n\n/**\n * Extract a usable leap-seconds count from response\n * headers. Returns `undefined` when the\n * `TAI-Leap-Seconds` field is missing, empty,\n * non-numeric, non-integer, negative, or out-of-range\n * — every \"treat as unsigned\" case in spec §5.3\n * collapsed into one verdict.\n */\nexport const extractLeapSeconds = (\n headers: Headers,\n): LeapSeconds | undefined => {\n const raw = headers.get(TAISTAMP_HEADER_LEAP_SECONDS);\n if (!raw || !DECIMAL_INTEGER.test(raw)) return undefined;\n return asLeapSeconds(Number(raw));\n};\n","import { type Bytes, decodeBase64, encodeBase64 } from '@kagal/ed25519-secret';\n\n/**\n * sf-binary item per RFC 9651 §3.3.5: standard base64\n * with `=` padding, wrapped in a leading and trailing\n * colon. The empty payload (`::`) is valid sf-binary;\n * consumers impose their own length bounds. The\n * alphabet contains no `,`, so a duplicated field\n * (joined by the Web `Headers` API with `,`) fails the\n * same check.\n */\nexport const SF_BINARY_PATTERN =\n /^:(?:(?:[\\d+/A-Za-z]{4})*(?:[\\d+/A-Za-z]{4}|[\\d+/A-Za-z]{3}=|[\\d+/A-Za-z]{2}==))?:$/;\n\n/**\n * Encode bytes as an sf-binary item (RFC 9651 §3.3.5):\n * standard base64 wrapped in colons. The output\n * satisfies {@link SF_BINARY_PATTERN} and round-trips\n * through {@link decodeSFBinary}.\n */\nexport const encodeSFBinary = (bytes: Readonly<Uint8Array>): string =>\n `:${encodeBase64(bytes)}:`;\n\n/**\n * Decode an sf-binary item back into bytes. Enforces\n * the full RFC 9651 §3.3.5 syntax: throws `TypeError`\n * when `value` does not satisfy\n * {@link SF_BINARY_PATTERN}; the thrown message is\n * optionally prefixed `<context>: `.\n */\nexport const decodeSFBinary = (\n value: string,\n context?: string,\n): Bytes => {\n if (!SF_BINARY_PATTERN.test(value)) {\n const prefix = context ? `${context}: ` : '';\n throw new TypeError(`${prefix}invalid sf-binary`);\n }\n // the pattern guarantees decodable standard base64\n return decodeBase64(value.slice(1, -1), context);\n};\n","import { TAI64N_EPOCH_HI, TAI64N_LABEL_PATTERN } from './const';\nimport { type LeapSeconds, TAI_LEAP_SECONDS } from './leap-seconds';\n\ntype timestamp = {\n nano: number\n sec: number\n\n offset?: number\n};\n\n/**\n * Convert a UTC timestamp in milliseconds (the\n * `Date.now()` shape) to TAI seconds and nanoseconds,\n * applying the current `TAI_LEAP_SECONDS` offset. The\n * package deals in the current time: the offset tracks\n * the present, and anything before the unix epoch is\n * out of scope.\n */\nexport const fromUTC = (utc: number): timestamp => {\n // TODO: leap seconds table\n const sec = Math.floor(utc / 1000) + TAI_LEAP_SECONDS;\n const nano = (utc % 1000) * 1e6;\n return { sec, nano, offset: TAI_LEAP_SECONDS };\n};\n\n/**\n * The current TAI time as seconds and nanoseconds —\n * {@link fromUTC} applied to `Date.now()`.\n */\nexport const now = (): timestamp => {\n const utc = Date.now();\n return fromUTC(utc);\n};\n\n/**\n * Format a TAI timestamp as the 25-byte TAI64N label\n * served at `/.well-known/taistamp`: `@` followed by\n * 16 hex digits of TAI64 seconds (the 2^62 epoch\n * offset applied) and 8 hex digits of nanoseconds.\n * Defaults to {@link now} when no value is given.\n */\nexport const tai64nLabel = (value?: timestamp): string => {\n const { sec, nano } = value ?? now();\n\n const secHi = Math.trunc(sec / u32Range) + TAI64N_EPOCH_HI;\n const secLo = sec % u32Range;\n\n const secHiHex = secHi.toString(16).padStart(8, '0');\n const secLoHex = secLo.toString(16).padStart(8, '0');\n const nanoHex = nano.toString(16).padStart(8, '0');\n\n return `@${secHiHex}${secLoHex}${nanoHex}`;\n};\n\n/**\n * The 25-byte TAI64N label for a UTC timestamp in\n * milliseconds — shorthand for\n * `tai64nLabel(fromUTC(utc))`.\n *\n * Labels are fixed-width hex, so they order\n * lexicographically: a verifier can bounds-check a\n * received label between the labels of\n * `Date.now() - skew` and `Date.now() + skew` without\n * decoding it.\n */\nexport const tai64nLabelFromUTC = (utc: number): string =>\n tai64nLabel(fromUTC(utc));\n\n/**\n * Recover a UTC timestamp in milliseconds (the\n * `Date.now()` shape) from a TAI64N label — the inverse\n * of {@link tai64nLabelFromUTC}. A label minted from a\n * millisecond value round-trips back to it exactly; a\n * `Date` is one `new Date(ms)` away.\n *\n * Returns `undefined` for any value that is not `@`\n * followed by 24 hex digits (either case) — the\n * verify-side \"malformed is absent\" collapse shared with\n * `asSignature` and `asNonce`, so it drops straight into\n * a gate pipeline.\n *\n * `leapSeconds` is the TAI − UTC offset removed when\n * mapping TAI back to UTC; it defaults to the current\n * {@link TAI_LEAP_SECONDS}, mirroring {@link fromUTC}.\n * Pass a response's `extractLeapSeconds(headers)` to\n * honour a server that declared a different count — an\n * absent or malformed header yields `undefined` there\n * and falls through to the default, while a genuine `0`\n * is honoured.\n */\nexport const tai64nLabelToUTC = (\n label: string,\n leapSeconds: LeapSeconds = TAI_LEAP_SECONDS,\n): number | undefined => {\n if (!TAI64N_LABEL_PATTERN.test(label)) return undefined;\n\n // The 64-bit seconds field must be read as two 32-bit\n // words: parsing all 16 hex digits at once yields\n // 2^62 + sec ≈ 4.6e18, whose float64 ULP (1024) has\n // already discarded sec's low bits before the epoch\n // base could be subtracted. Removing TAI64N_EPOCH_HI on\n // the high word alone keeps every term exact.\n const secHi = Number.parseInt(label.slice(1, 9), 16) - TAI64N_EPOCH_HI;\n const secLo = Number.parseInt(label.slice(9, 17), 16);\n const nano = Number.parseInt(label.slice(17, 25), 16);\n\n const sec = secHi * u32Range + secLo;\n return (sec - leapSeconds) * 1000 + nano / 1e6;\n};\n\nconst u32Range = 0x1_00_00_00_00;\n"],"mappings":";AAQA,MAAa,uBAAuB;AAGpC,MAAa,sBAAsB;AAGnC,MAAa,sBAAsB;AAGnC,MAAa,kBAAkB;AAG/B,MAAa,iBAAiB;AAS9B,MAAa,gBAAgB;AAG7B,MAAa,wBAAwB;AAGrC,MAAa,0BAAA;AAEb,MAAa,+BAA+B;AAC5C,MAAa,+BAA+B;AAC5C,MAAa,wBAAwB;AACrC,MAAa,4BAA4B;AAQzC,MAAa,cAAc;AAG3B,MAAa,wBAAA;AAGb,MAAa,6BAA6B;AAG1C,MAAa,6BAA6B;AAG1C,MAAa,sBAAsB;AAGnC,MAAa,0BAA0B;;;;;;ACjDvC,MAAa,mBAAA;;;;CAmBb,IAAA,CAAa,OAAA,CAAA,gBACX,KAC4B,GAAA,GAAA,OAAA,KAAA;CAC5B,OAAK,cAAU,OAAO,GAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;CAoCxB,OAAa,IAAA,MAAA,SAAA,EACX,EAAA,SAC4B,GAAA,GAAA,IAAA,MAAA,SAAA,EAAA,EAAA,SAAA,GAAA,GAAA,IAAA,KAAA,SAAA,EAAA,EAAA,SAAA,GAAA,GAAA;;MAG5B,sBAAqB,QAAW,YAAA,QAAA,GAAA,CAAA"}