@kagal/taistamp 0.0.1 → 0.0.2

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
@@ -36,11 +36,12 @@ app.get(TAI64N_PATH, (c) => taistamp(c.req.raw));
36
36
  `newTaistampHandler()` returns an
37
37
  `async (request) => Response`. `GET` and `HEAD` succeed
38
38
  with a fresh 25-byte TAI64N label
39
- (`@<sec-hi><sec-lo><nano>`); other methods return `405`
40
- with `Allow: GET, HEAD`. A request that carries more
41
- than one `TAI-Nonce` header is rejected with `400` —
42
- stricter than the spec's "treat as absent" rule, since
43
- a duplicated singleton field is malformed input.
39
+ (`@<sec-hi><sec-lo><nano>`); `OPTIONS` returns `200`
40
+ with `Allow: GET, HEAD, OPTIONS`; other methods return
41
+ `405` with the same `Allow`. A `TAI-Nonce` that is
42
+ missing, empty, duplicated, not a valid sf-binary
43
+ value, or outside the 14–174 octet range is treated as
44
+ absent (no echo, no signature) per spec §5.2.
44
45
 
45
46
  Response headers on success:
46
47
 
@@ -58,6 +59,44 @@ the corresponding `GET` but never include
58
59
  payload covers the response body, so a `HEAD` cannot
59
60
  be verified.
60
61
 
62
+ ## CORS
63
+
64
+ The handler is cross-origin permissive by default.
65
+ Pass a specific origin to scope the policy, or
66
+ `false` to disable the CORS-specific headers
67
+ entirely.
68
+
69
+ ```typescript
70
+ newTaistampHandler(); // cors: '*' (default)
71
+ newTaistampHandler({ cors: 'https://example.com' }); // scoped origin
72
+ newTaistampHandler({ cors: false }); // CORS-specific headers off
73
+ ```
74
+
75
+ When CORS is enabled, responses carry:
76
+
77
+ | Response | CORS headers added | `Vary: Origin` (scoped origin only) |
78
+ |----------|--------------------|------|
79
+ | `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` | yes |
80
+ | `GET` / `HEAD` 200 | `Access-Control-Allow-Origin`, `Access-Control-Expose-Headers` (so browser JS can read the `TAI-*` headers) | yes |
81
+ | `405` | `Access-Control-Allow-Origin` | yes |
82
+
83
+ `Vary: Origin` lands on every response when the
84
+ configured origin is anything other than `'*'`, so
85
+ caches can keep per-origin variants distinct. The
86
+ `Allow: GET, HEAD, OPTIONS` and `Access-Control-Allow-Methods:
87
+ GET, HEAD` lists are intentionally different — the
88
+ former is RFC 9110 §9.3.7 method discovery (includes
89
+ `OPTIONS` itself), the latter is the Fetch CORS list
90
+ of methods JS would ever preflight (so `OPTIONS` is
91
+ omitted).
92
+
93
+ With `cors: false` none of the `Access-Control-*` or
94
+ `Vary` headers are emitted, but `OPTIONS` is still
95
+ answered with `200` and
96
+ `Allow: GET, HEAD, OPTIONS` — method discovery
97
+ (RFC 9110 §9.3.7) is independent of cross-origin
98
+ policy.
99
+
61
100
  ## Signing
62
101
 
63
102
  ```typescript
@@ -79,16 +118,17 @@ not match `[A-Za-z][A-Za-z0-9_-]{0,62}` (a single
79
118
  DNS-safe label that starts with a letter and is also a
80
119
  valid Structured Field token).
81
120
 
82
- When the request is a `GET` carrying a `TAI-Nonce` of
83
- 14–174 octets *and* a signer is configured, the
84
- response gains:
121
+ When the request is a `GET` carrying a valid
122
+ `TAI-Nonce` (see Handler section for the
123
+ "treat as absent" rules) *and* a signer is configured,
124
+ the response gains:
85
125
 
86
126
  - `TAI-Key-Selector: <selector>`
87
- - `TAI-Signature: :<base64>:` (sf-binary, RFC 8941)
127
+ - `TAI-Signature: :<base64>:` (sf-binary, RFC 9651)
88
128
  over the framed payload.
89
129
 
90
- `HEAD`, `405`, nonce-less, and out-of-range-nonce
91
- responses are never signed.
130
+ `HEAD`, `405`, and nonce-less responses are never
131
+ signed.
92
132
 
93
133
  The framed payload is:
94
134
 
@@ -154,26 +194,51 @@ signatures stay verifiable until their TXT is removed.
154
194
  ## Verifying
155
195
 
156
196
  ```typescript
157
- import { taistampSignedPayload } from '@kagal/taistamp';
197
+ import {
198
+ asNonce,
199
+ extractLeapSeconds,
200
+ composeSignaturePayload,
201
+ } from '@kagal/taistamp';
158
202
 
159
203
  const response = await fetch(taistampURL, {
160
204
  headers: { 'TAI-Nonce': clientNonce },
161
205
  });
162
206
  const label = await response.text();
163
- const leap = Number(response.headers.get('TAI-Leap-Seconds'));
164
207
  const selector = response.headers.get('TAI-Key-Selector')!;
165
208
  const sigSf = response.headers.get('TAI-Signature')!;
166
209
 
210
+ // Spec §5.1: a `TAI-Leap-Seconds` value outside the
211
+ // signed-payload u32 range MUST be treated as unsigned.
212
+ // `extractLeapSeconds` returns `undefined` whenever
213
+ // the field is missing, empty, non-numeric, non-integer,
214
+ // negative, or out-of-range; the branded `LeapSeconds`
215
+ // it yields is the only type `composeSignaturePayload`
216
+ // accepts.
217
+ const leap = extractLeapSeconds(response.headers);
218
+ if (leap === undefined) {
219
+ throw new Error('TAI-Leap-Seconds out of range; treat as unsigned');
220
+ }
221
+
222
+ // Brand the recorded nonce so it can flow into the
223
+ // signing path. `asNonce` returns `undefined` for any
224
+ // value that fails sf-binary syntax or the 14..174
225
+ // octet range — the same "treat as absent" verdict
226
+ // the server applied.
227
+ const nonce = asNonce(clientNonce);
228
+ if (nonce === undefined) {
229
+ throw new Error('client nonce is not a valid sf-binary item');
230
+ }
231
+
167
232
  // Look up the public key in DNS at
168
233
  // `${selector}._taistamp.${host}` and parse the
169
234
  // `p=` tag from the TXT record.
170
235
  const publicKey = await loadPublicKey(host, selector);
171
236
 
172
- const payload = taistampSignedPayload(
237
+ const payload = composeSignaturePayload(
173
238
  label,
174
239
  leap,
175
240
  selector,
176
- clientNonce,
241
+ nonce,
177
242
  );
178
243
  const valid = await crypto.subtle.verify(
179
244
  'Ed25519',
@@ -183,10 +248,21 @@ const valid = await crypto.subtle.verify(
183
248
  );
184
249
  ```
185
250
 
186
- `taistampSignedPayload(label, leapSeconds, selector,
251
+ `composeSignaturePayload(label, leapSeconds, selector,
187
252
  nonce)` reconstructs the exact byte sequence the
188
253
  server signed; the verifier supplies only the public
189
- key and an sf-binary decoder. Comparing the verifier's
254
+ key and an sf-binary decoder. `leapSeconds` must be a
255
+ branded `LeapSeconds` — obtain one from
256
+ `extractLeapSeconds(headers)` (the verifier path) or
257
+ `asLeapSeconds(number)` (when you already have the
258
+ value). Both return `undefined` for out-of-range
259
+ input, collapsing every "treat as unsigned" case in
260
+ spec §5.1 into one verdict. `nonce` must be a branded
261
+ `Nonce` — wrap the recorded client nonce with
262
+ `asNonce(value)`, which returns `undefined` for any
263
+ value that would have been treated as absent on the
264
+ server (missing, empty, malformed sf-binary, or
265
+ outside 14..174 octets). Comparing the verifier's
190
266
  recorded nonce against the response's `TAI-Nonce`
191
267
  defends against replay.
192
268
 
@@ -203,10 +279,11 @@ construction:
203
279
  | `tai64nLabel(t?)` | 25-byte label string for a timestamp (or `now()`) |
204
280
  | `tai64nLabelFromUTC(utc)` | Shortcut for `tai64nLabel(fromUTC(utc))` |
205
281
 
206
- `fromUTC` applies the constant `TAI_OFFSET` (currently
207
- 37 seconds). Historic UTC timestamps spanning a
208
- leap-second boundary need caller-side adjustment —
209
- the constant tracks the present, not history.
282
+ `fromUTC` applies the constant `TAI_LEAP_SECONDS`
283
+ (currently 37 seconds). Historic UTC timestamps
284
+ spanning a leap-second boundary need caller-side
285
+ adjustment — the constant tracks the present, not
286
+ history.
210
287
 
211
288
  ## Constants
212
289
 
@@ -219,7 +296,8 @@ the constant tracks the present, not history.
219
296
  | `TAI64N_HEADER_LEAP_SECONDS` | `TAI-Leap-Seconds` |
220
297
  | `TAI64N_HEADER_NONCE` | `TAI-Nonce` |
221
298
  | `TAI64N_HEADER_SIGNATURE` | `TAI-Signature` |
222
- | `TAI_OFFSET` | `37` |
299
+ | `TAI_LEAP_SECONDS` | `37` (current TAI − UTC offset) |
300
+ | `TAI_LEAP_SECONDS_MAX` | `0xFFFFFFFF` (signed-payload u32 cap) |
223
301
  | `TAI64_EPOCH_HI` | `0x40000000` |
224
302
 
225
303
  ## Licence
package/dist/index.d.mts CHANGED
@@ -1,4 +1,3 @@
1
- declare const TAI_OFFSET: number;
2
1
  declare const TAI64N_PATH = "/.well-known/taistamp";
3
2
  declare const TAI64N_CONTENT_TYPE = "application/tai64n";
4
3
  declare const TAI64N_CONTENT_LENGTH: number;
@@ -50,6 +49,79 @@ interface Signer {
50
49
  */
51
50
  declare const newEd25519Signer: (key: CryptoKey) => Signer;
52
51
 
52
+ /**
53
+ * Upper bound for `leapSeconds` in the taistamp signed
54
+ * payload. The framing encodes the value as a 4-byte
55
+ * big-endian unsigned integer, so any input outside
56
+ * `[0, 2^32-1]` cannot be represented. Verifiers MUST
57
+ * treat an out-of-range `TAI-Leap-Seconds` response
58
+ * header as unsigned, per spec §5.1.
59
+ */
60
+ declare const TAI_LEAP_SECONDS_MAX = 4294967295;
61
+ declare const LeapSecondsBrand: unique symbol;
62
+ /**
63
+ * `number` that has been confirmed to fit the
64
+ * `[0, TAI_LEAP_SECONDS_MAX]` u32be range required by
65
+ * the taistamp signed-payload framing. Construct only
66
+ * via {@link extractLeapSeconds} or {@link asLeapSeconds};
67
+ * the brand prevents an arbitrary number from reaching
68
+ * the signing path.
69
+ */
70
+ type LeapSeconds = number & {
71
+ readonly [LeapSecondsBrand]: never;
72
+ };
73
+ /**
74
+ * Coerce a `number` to a {@link LeapSeconds}. Returns
75
+ * `undefined` when `value` is non-integer, negative,
76
+ * or exceeds {@link TAI_LEAP_SECONDS_MAX}.
77
+ */
78
+ declare const asLeapSeconds: (value: number) => LeapSeconds | undefined;
79
+ /**
80
+ * Current TAI − UTC offset in whole seconds, used by
81
+ * `fromUTC()` and emitted in the `TAI-Leap-Seconds`
82
+ * response header. The value 37 has been in force
83
+ * since 2017-01-01; update on the next IERS leap-second
84
+ * announcement.
85
+ *
86
+ * @remarks
87
+ * Stays a single `LeapSeconds` until a leap-seconds
88
+ * table is added so the offset can be computed for any
89
+ * TAI second; this constant becomes redundant then.
90
+ */
91
+ declare const TAI_LEAP_SECONDS: LeapSeconds;
92
+ /**
93
+ * Extract a usable leap-seconds count from response
94
+ * headers. Returns `undefined` when the
95
+ * `TAI-Leap-Seconds` field is missing, empty,
96
+ * non-numeric, non-integer, negative, or out-of-range
97
+ * — every "treat as unsigned" case in spec §5.1
98
+ * collapsed into one verdict.
99
+ */
100
+ declare const extractLeapSeconds: (headers: Headers) => LeapSeconds | undefined;
101
+
102
+ declare const NonceBrand: unique symbol;
103
+ /**
104
+ * `string` that has been confirmed to satisfy the
105
+ * sf-binary syntax of RFC 9651 §3.3.5 and the
106
+ * `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]` length range
107
+ * required by spec §5.2. Construct only via
108
+ * {@link asNonce} or {@link extractNonce}; the brand
109
+ * prevents arbitrary strings from reaching the
110
+ * signing path.
111
+ */
112
+ type Nonce = string & {
113
+ readonly [NonceBrand]: never;
114
+ };
115
+ /**
116
+ * Brand `value` as a {@link Nonce} when it satisfies
117
+ * sf-binary syntax (RFC 9651 §3.3.5) and falls inside
118
+ * `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]`. Returns
119
+ * `undefined` for anything else — every "treat as
120
+ * absent" case in spec §5.2 collapsed into one
121
+ * verdict.
122
+ */
123
+ declare const asNonce: (value: string) => Nonce | undefined;
124
+
53
125
  /**
54
126
  * Compose the byte sequence covered by a TAI-Signature.
55
127
  *
@@ -62,7 +134,8 @@ declare const newEd25519Signer: (key: CryptoKey) => Signer;
62
134
  * this to look up the public key in DNS at
63
135
  * `<selector>._taistamp.<host>`
64
136
  * @param nonce - the client-supplied nonce, echoed
65
- * verbatim in `TAI-Nonce`
137
+ * verbatim in `TAI-Nonce`; brand a verifier-side
138
+ * string with {@link asNonce} before passing it in
66
139
  * @returns the byte sequence verifiers reconstruct
67
140
  * from the response and pass to their public-key
68
141
  * verify routine. The framing is the
@@ -82,7 +155,7 @@ declare const newEd25519Signer: (key: CryptoKey) => Signer;
82
155
  * a single byte (selectors are ≤ 63 chars per
83
156
  * {@link newTaistampHandler}'s validation).
84
157
  */
85
- declare const taistampSignedPayload: (label: string, leapSeconds: number, selector: string, nonce: string) => ArrayBuffer;
158
+ declare const composeSignaturePayload: (label: string, leapSeconds: LeapSeconds, selector: string, nonce: Nonce) => ArrayBuffer;
86
159
  /**
87
160
  * Configuration for {@link newTaistampHandler}.
88
161
  *
@@ -110,11 +183,30 @@ interface TaistampHandlerConfig {
110
183
  selector?: string;
111
184
  /**
112
185
  * {@link Signer} that produces `TAI-Signature` over
113
- * the framed payload from {@link taistampSignedPayload}.
186
+ * the framed payload from {@link composeSignaturePayload}.
114
187
  * Without a signer the nonce is still echoed but the
115
188
  * response is unsigned.
116
189
  */
117
190
  signer?: Signer;
191
+ /**
192
+ * CORS origin policy. Defaults to `'*'`; pass `false`
193
+ * to disable CORS entirely, or a specific origin
194
+ * (e.g. `'https://example.com'`) to scope the policy.
195
+ *
196
+ * Every response (`GET` / `HEAD` / `OPTIONS` / `405`)
197
+ * gains `Access-Control-Allow-Origin`; pre-flight
198
+ * `OPTIONS` also carries `-Allow-Methods`,
199
+ * `-Allow-Headers`, and `-Expose-Headers`; success
200
+ * `GET` / `HEAD` carry `-Expose-Headers` so browser
201
+ * JS can read the `TAI-*` response headers. A
202
+ * non-`'*'` value adds `Vary: Origin` so caches can
203
+ * keep per-origin variants distinct.
204
+ *
205
+ * Disabling CORS does not affect method discovery:
206
+ * `OPTIONS` is still answered with `200` and
207
+ * `Allow: GET, HEAD, OPTIONS` per RFC 9110 §9.3.7.
208
+ */
209
+ cors?: false | string;
118
210
  }
119
211
  /**
120
212
  * Build a handler for `/.well-known/taistamp`.
@@ -136,25 +228,29 @@ interface TaistampHandlerConfig {
136
228
  * Content-Type `application/tai64n`, Content-Length
137
229
  * `25`, Cache-Control `no-store`, plus
138
230
  * `TAI-Leap-Seconds` carrying the current count.
231
+ * - `OPTIONS` — `200` with `Allow: GET, HEAD, OPTIONS`.
232
+ * When CORS is enabled (the default) the response
233
+ * also carries `Access-Control-Allow-*` and
234
+ * `-Expose-Headers` per
235
+ * {@link TaistampHandlerConfig.cors}. `OPTIONS` is
236
+ * never signed.
139
237
  * - Any other method — `405 Method Not Allowed` with
140
- * `Allow: GET, HEAD`.
141
- * - Request with more than one `TAI-Nonce` header —
142
- * `400 Bad Request`. Stricter than the spec's
143
- * "treat as absent" rule: a duplicated singleton
144
- * field is malformed input, so we refuse rather
145
- * than silently down-ranking it to unsigned.
238
+ * `Allow: GET, HEAD, OPTIONS`.
146
239
  * - Request `TAI-Nonce` — the value is echoed verbatim
147
- * in the response.
240
+ * in the response. A missing, empty, duplicated,
241
+ * structurally malformed, or out-of-range
242
+ * (14..174 octets) field is treated as absent (no
243
+ * echo, no signature) per spec §5.2 — see
244
+ * {@link extractNonce}.
148
245
  * - Request `TAI-Nonce` *and* `signer` configured *and*
149
- * the request method is `GET` *and* the nonce field
150
- * value is between 14 and 174 octets — adds
246
+ * the request method is `GET` adds
151
247
  * `TAI-Key-Selector` and `TAI-Signature` (sf-binary)
152
248
  * over the bytes produced by
153
- * {@link taistampSignedPayload}. The
249
+ * {@link composeSignaturePayload}. The
154
250
  * domain-separation tag means the same key cannot
155
251
  * be tricked into producing valid signatures for
156
- * other protocols. `HEAD` and `405` responses are
157
- * never signed.
252
+ * other protocols. `HEAD`, `OPTIONS`, and `405`
253
+ * responses are never signed.
158
254
  *
159
255
  * The corresponding public key is expected to be
160
256
  * published out-of-band as a DNS TXT record at
@@ -181,5 +277,5 @@ declare const tai64nLabelFromUTC: (utc: number) => string;
181
277
  /** Package version from package.json. */
182
278
  declare const VERSION: string;
183
279
 
184
- export { TAI64N_CONTENT_LENGTH, TAI64N_CONTENT_TYPE, TAI64N_HEADER_KEY_SELECTOR, TAI64N_HEADER_LEAP_SECONDS, TAI64N_HEADER_NONCE, TAI64N_HEADER_SIGNATURE, TAI64N_PATH, TAI64_EPOCH_HI, TAI_OFFSET, VERSION, fromUTC, newEd25519Signer, newTaistampHandler, now, tai64nLabel, tai64nLabelFromUTC, taistampSignedPayload };
185
- export type { Signer, TaistampHandlerConfig };
280
+ export { TAI64N_CONTENT_LENGTH, TAI64N_CONTENT_TYPE, TAI64N_HEADER_KEY_SELECTOR, TAI64N_HEADER_LEAP_SECONDS, TAI64N_HEADER_NONCE, TAI64N_HEADER_SIGNATURE, TAI64N_PATH, TAI64_EPOCH_HI, TAI_LEAP_SECONDS, TAI_LEAP_SECONDS_MAX, VERSION, asLeapSeconds, asNonce, composeSignaturePayload, extractLeapSeconds, fromUTC, newEd25519Signer, newTaistampHandler, now, tai64nLabel, tai64nLabelFromUTC };
281
+ export type { LeapSeconds, Nonce, Signer, TaistampHandlerConfig };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- declare const TAI_OFFSET: number;
2
1
  declare const TAI64N_PATH = "/.well-known/taistamp";
3
2
  declare const TAI64N_CONTENT_TYPE = "application/tai64n";
4
3
  declare const TAI64N_CONTENT_LENGTH: number;
@@ -50,6 +49,79 @@ interface Signer {
50
49
  */
51
50
  declare const newEd25519Signer: (key: CryptoKey) => Signer;
52
51
 
52
+ /**
53
+ * Upper bound for `leapSeconds` in the taistamp signed
54
+ * payload. The framing encodes the value as a 4-byte
55
+ * big-endian unsigned integer, so any input outside
56
+ * `[0, 2^32-1]` cannot be represented. Verifiers MUST
57
+ * treat an out-of-range `TAI-Leap-Seconds` response
58
+ * header as unsigned, per spec §5.1.
59
+ */
60
+ declare const TAI_LEAP_SECONDS_MAX = 4294967295;
61
+ declare const LeapSecondsBrand: unique symbol;
62
+ /**
63
+ * `number` that has been confirmed to fit the
64
+ * `[0, TAI_LEAP_SECONDS_MAX]` u32be range required by
65
+ * the taistamp signed-payload framing. Construct only
66
+ * via {@link extractLeapSeconds} or {@link asLeapSeconds};
67
+ * the brand prevents an arbitrary number from reaching
68
+ * the signing path.
69
+ */
70
+ type LeapSeconds = number & {
71
+ readonly [LeapSecondsBrand]: never;
72
+ };
73
+ /**
74
+ * Coerce a `number` to a {@link LeapSeconds}. Returns
75
+ * `undefined` when `value` is non-integer, negative,
76
+ * or exceeds {@link TAI_LEAP_SECONDS_MAX}.
77
+ */
78
+ declare const asLeapSeconds: (value: number) => LeapSeconds | undefined;
79
+ /**
80
+ * Current TAI − UTC offset in whole seconds, used by
81
+ * `fromUTC()` and emitted in the `TAI-Leap-Seconds`
82
+ * response header. The value 37 has been in force
83
+ * since 2017-01-01; update on the next IERS leap-second
84
+ * announcement.
85
+ *
86
+ * @remarks
87
+ * Stays a single `LeapSeconds` until a leap-seconds
88
+ * table is added so the offset can be computed for any
89
+ * TAI second; this constant becomes redundant then.
90
+ */
91
+ declare const TAI_LEAP_SECONDS: LeapSeconds;
92
+ /**
93
+ * Extract a usable leap-seconds count from response
94
+ * headers. Returns `undefined` when the
95
+ * `TAI-Leap-Seconds` field is missing, empty,
96
+ * non-numeric, non-integer, negative, or out-of-range
97
+ * — every "treat as unsigned" case in spec §5.1
98
+ * collapsed into one verdict.
99
+ */
100
+ declare const extractLeapSeconds: (headers: Headers) => LeapSeconds | undefined;
101
+
102
+ declare const NonceBrand: unique symbol;
103
+ /**
104
+ * `string` that has been confirmed to satisfy the
105
+ * sf-binary syntax of RFC 9651 §3.3.5 and the
106
+ * `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]` length range
107
+ * required by spec §5.2. Construct only via
108
+ * {@link asNonce} or {@link extractNonce}; the brand
109
+ * prevents arbitrary strings from reaching the
110
+ * signing path.
111
+ */
112
+ type Nonce = string & {
113
+ readonly [NonceBrand]: never;
114
+ };
115
+ /**
116
+ * Brand `value` as a {@link Nonce} when it satisfies
117
+ * sf-binary syntax (RFC 9651 §3.3.5) and falls inside
118
+ * `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]`. Returns
119
+ * `undefined` for anything else — every "treat as
120
+ * absent" case in spec §5.2 collapsed into one
121
+ * verdict.
122
+ */
123
+ declare const asNonce: (value: string) => Nonce | undefined;
124
+
53
125
  /**
54
126
  * Compose the byte sequence covered by a TAI-Signature.
55
127
  *
@@ -62,7 +134,8 @@ declare const newEd25519Signer: (key: CryptoKey) => Signer;
62
134
  * this to look up the public key in DNS at
63
135
  * `<selector>._taistamp.<host>`
64
136
  * @param nonce - the client-supplied nonce, echoed
65
- * verbatim in `TAI-Nonce`
137
+ * verbatim in `TAI-Nonce`; brand a verifier-side
138
+ * string with {@link asNonce} before passing it in
66
139
  * @returns the byte sequence verifiers reconstruct
67
140
  * from the response and pass to their public-key
68
141
  * verify routine. The framing is the
@@ -82,7 +155,7 @@ declare const newEd25519Signer: (key: CryptoKey) => Signer;
82
155
  * a single byte (selectors are ≤ 63 chars per
83
156
  * {@link newTaistampHandler}'s validation).
84
157
  */
85
- declare const taistampSignedPayload: (label: string, leapSeconds: number, selector: string, nonce: string) => ArrayBuffer;
158
+ declare const composeSignaturePayload: (label: string, leapSeconds: LeapSeconds, selector: string, nonce: Nonce) => ArrayBuffer;
86
159
  /**
87
160
  * Configuration for {@link newTaistampHandler}.
88
161
  *
@@ -110,11 +183,30 @@ interface TaistampHandlerConfig {
110
183
  selector?: string;
111
184
  /**
112
185
  * {@link Signer} that produces `TAI-Signature` over
113
- * the framed payload from {@link taistampSignedPayload}.
186
+ * the framed payload from {@link composeSignaturePayload}.
114
187
  * Without a signer the nonce is still echoed but the
115
188
  * response is unsigned.
116
189
  */
117
190
  signer?: Signer;
191
+ /**
192
+ * CORS origin policy. Defaults to `'*'`; pass `false`
193
+ * to disable CORS entirely, or a specific origin
194
+ * (e.g. `'https://example.com'`) to scope the policy.
195
+ *
196
+ * Every response (`GET` / `HEAD` / `OPTIONS` / `405`)
197
+ * gains `Access-Control-Allow-Origin`; pre-flight
198
+ * `OPTIONS` also carries `-Allow-Methods`,
199
+ * `-Allow-Headers`, and `-Expose-Headers`; success
200
+ * `GET` / `HEAD` carry `-Expose-Headers` so browser
201
+ * JS can read the `TAI-*` response headers. A
202
+ * non-`'*'` value adds `Vary: Origin` so caches can
203
+ * keep per-origin variants distinct.
204
+ *
205
+ * Disabling CORS does not affect method discovery:
206
+ * `OPTIONS` is still answered with `200` and
207
+ * `Allow: GET, HEAD, OPTIONS` per RFC 9110 §9.3.7.
208
+ */
209
+ cors?: false | string;
118
210
  }
119
211
  /**
120
212
  * Build a handler for `/.well-known/taistamp`.
@@ -136,25 +228,29 @@ interface TaistampHandlerConfig {
136
228
  * Content-Type `application/tai64n`, Content-Length
137
229
  * `25`, Cache-Control `no-store`, plus
138
230
  * `TAI-Leap-Seconds` carrying the current count.
231
+ * - `OPTIONS` — `200` with `Allow: GET, HEAD, OPTIONS`.
232
+ * When CORS is enabled (the default) the response
233
+ * also carries `Access-Control-Allow-*` and
234
+ * `-Expose-Headers` per
235
+ * {@link TaistampHandlerConfig.cors}. `OPTIONS` is
236
+ * never signed.
139
237
  * - Any other method — `405 Method Not Allowed` with
140
- * `Allow: GET, HEAD`.
141
- * - Request with more than one `TAI-Nonce` header —
142
- * `400 Bad Request`. Stricter than the spec's
143
- * "treat as absent" rule: a duplicated singleton
144
- * field is malformed input, so we refuse rather
145
- * than silently down-ranking it to unsigned.
238
+ * `Allow: GET, HEAD, OPTIONS`.
146
239
  * - Request `TAI-Nonce` — the value is echoed verbatim
147
- * in the response.
240
+ * in the response. A missing, empty, duplicated,
241
+ * structurally malformed, or out-of-range
242
+ * (14..174 octets) field is treated as absent (no
243
+ * echo, no signature) per spec §5.2 — see
244
+ * {@link extractNonce}.
148
245
  * - Request `TAI-Nonce` *and* `signer` configured *and*
149
- * the request method is `GET` *and* the nonce field
150
- * value is between 14 and 174 octets — adds
246
+ * the request method is `GET` adds
151
247
  * `TAI-Key-Selector` and `TAI-Signature` (sf-binary)
152
248
  * over the bytes produced by
153
- * {@link taistampSignedPayload}. The
249
+ * {@link composeSignaturePayload}. The
154
250
  * domain-separation tag means the same key cannot
155
251
  * be tricked into producing valid signatures for
156
- * other protocols. `HEAD` and `405` responses are
157
- * never signed.
252
+ * other protocols. `HEAD`, `OPTIONS`, and `405`
253
+ * responses are never signed.
158
254
  *
159
255
  * The corresponding public key is expected to be
160
256
  * published out-of-band as a DNS TXT record at
@@ -181,5 +277,5 @@ declare const tai64nLabelFromUTC: (utc: number) => string;
181
277
  /** Package version from package.json. */
182
278
  declare const VERSION: string;
183
279
 
184
- export { TAI64N_CONTENT_LENGTH, TAI64N_CONTENT_TYPE, TAI64N_HEADER_KEY_SELECTOR, TAI64N_HEADER_LEAP_SECONDS, TAI64N_HEADER_NONCE, TAI64N_HEADER_SIGNATURE, TAI64N_PATH, TAI64_EPOCH_HI, TAI_OFFSET, VERSION, fromUTC, newEd25519Signer, newTaistampHandler, now, tai64nLabel, tai64nLabelFromUTC, taistampSignedPayload };
185
- export type { Signer, TaistampHandlerConfig };
280
+ export { TAI64N_CONTENT_LENGTH, TAI64N_CONTENT_TYPE, TAI64N_HEADER_KEY_SELECTOR, TAI64N_HEADER_LEAP_SECONDS, TAI64N_HEADER_NONCE, TAI64N_HEADER_SIGNATURE, TAI64N_PATH, TAI64_EPOCH_HI, TAI_LEAP_SECONDS, TAI_LEAP_SECONDS_MAX, VERSION, asLeapSeconds, asNonce, composeSignaturePayload, extractLeapSeconds, fromUTC, newEd25519Signer, newTaistampHandler, now, tai64nLabel, tai64nLabelFromUTC };
281
+ export type { LeapSeconds, Nonce, Signer, TaistampHandlerConfig };
package/dist/index.mjs CHANGED
@@ -1,8 +1,7 @@
1
- const version = "0.0.1";
1
+ const version = "0.0.2";
2
2
  const pkg = {
3
3
  version: version};
4
4
 
5
- const TAI_OFFSET = 37;
6
5
  const TAI64N_PATH = "/.well-known/taistamp";
7
6
  const TAI64N_CONTENT_TYPE = "application/tai64n";
8
7
  const TAI64N_CONTENT_LENGTH = 1 + 16 + 8;
@@ -12,10 +11,69 @@ const TAI64N_HEADER_NONCE = "TAI-Nonce";
12
11
  const TAI64N_HEADER_SIGNATURE = "TAI-Signature";
13
12
  const TAI64_EPOCH_HI = 1073741824;
14
13
 
14
+ const CORS_ALLOW_METHODS = "GET, HEAD";
15
+ const CORS_ALLOW_HEADERS = TAI64N_HEADER_NONCE;
16
+ const CORS_EXPOSE_HEADERS = [
17
+ TAI64N_HEADER_LEAP_SECONDS,
18
+ TAI64N_HEADER_NONCE,
19
+ TAI64N_HEADER_KEY_SELECTOR,
20
+ TAI64N_HEADER_SIGNATURE
21
+ ].join(", ");
22
+ const buildCORSHeaders = (cors) => {
23
+ if (cors === false) {
24
+ return { error: {}, preflight: {}, response: {} };
25
+ }
26
+ const origin = cors || "*";
27
+ const vary = origin === "*" ? {} : { vary: "Origin" };
28
+ return {
29
+ error: {
30
+ "access-control-allow-origin": origin,
31
+ ...vary
32
+ },
33
+ preflight: {
34
+ "access-control-allow-origin": origin,
35
+ "access-control-allow-methods": CORS_ALLOW_METHODS,
36
+ "access-control-allow-headers": CORS_ALLOW_HEADERS,
37
+ "access-control-expose-headers": CORS_EXPOSE_HEADERS,
38
+ ...vary
39
+ },
40
+ response: {
41
+ "access-control-allow-origin": origin,
42
+ "access-control-expose-headers": CORS_EXPOSE_HEADERS,
43
+ ...vary
44
+ }
45
+ };
46
+ };
47
+
48
+ const TAI_LEAP_SECONDS_MAX = 4294967295;
49
+ const asLeapSeconds = (value) => {
50
+ if (!Number.isInteger(value) || value < 0 || value > TAI_LEAP_SECONDS_MAX) return void 0;
51
+ return value;
52
+ };
53
+ const TAI_LEAP_SECONDS = 37;
54
+ const DECIMAL_INTEGER = /^(?:0|[1-9]\d*)$/;
55
+ const extractLeapSeconds = (headers) => {
56
+ const raw = headers.get(TAI64N_HEADER_LEAP_SECONDS);
57
+ if (!raw || !DECIMAL_INTEGER.test(raw)) return void 0;
58
+ return asLeapSeconds(Number(raw));
59
+ };
60
+
61
+ const NONCE_MIN_OCTETS = 14;
62
+ const NONCE_MAX_OCTETS = 174;
63
+ const SF_BINARY_PATTERN = /^:(?:[\d+/A-Za-z]{4})*(?:[\d+/A-Za-z]{4}|[\d+/A-Za-z]{3}=|[\d+/A-Za-z]{2}==):$/;
64
+ const asNonce = (value) => {
65
+ if (!value || value.length < NONCE_MIN_OCTETS || value.length > NONCE_MAX_OCTETS || !SF_BINARY_PATTERN.test(value)) return void 0;
66
+ return value;
67
+ };
68
+ const extractNonce = (headers) => {
69
+ const value = headers.get(TAI64N_HEADER_NONCE);
70
+ return value === null ? void 0 : asNonce(value);
71
+ };
72
+
15
73
  const fromUTC = (utc) => {
16
- const sec = Math.floor(utc / 1e3) + TAI_OFFSET;
74
+ const sec = Math.floor(utc / 1e3) + TAI_LEAP_SECONDS;
17
75
  const nano = utc % 1e3 * 1e6;
18
- return { sec, nano, offset: TAI_OFFSET };
76
+ return { sec, nano, offset: TAI_LEAP_SECONDS };
19
77
  };
20
78
  const now = () => {
21
79
  const utc = Date.now();
@@ -34,8 +92,7 @@ const tai64nLabelFromUTC = (utc) => tai64nLabel(fromUTC(utc));
34
92
  const u32Range = 4294967296;
35
93
 
36
94
  const SELECTOR_PATTERN = /^[A-Za-z][\dA-Za-z_-]{0,62}$/;
37
- const NONCE_MIN_OCTETS = 14;
38
- const NONCE_MAX_OCTETS = 174;
95
+ const ALLOW_HEADER = "GET, HEAD, OPTIONS";
39
96
  const textEncoder = new TextEncoder();
40
97
  const DOMAIN_SEPARATOR = textEncoder.encode("taistamp-v1\0");
41
98
  const asBytes = (source) => {
@@ -56,7 +113,7 @@ const encodeStructuredBinary = (source) => {
56
113
  const standard = btoa(String.fromCodePoint(...bytes));
57
114
  return `:${standard}:`;
58
115
  };
59
- const taistampSignedPayload = (label, leapSeconds, selector, nonce) => {
116
+ const composeSignaturePayload = (label, leapSeconds, selector, nonce) => {
60
117
  const labelBytes = textEncoder.encode(label);
61
118
  const selectorBytes = textEncoder.encode(selector);
62
119
  const nonceBytes = textEncoder.encode(nonce);
@@ -78,53 +135,72 @@ const taistampSignedPayload = (label, leapSeconds, selector, nonce) => {
78
135
  view.set(nonceBytes, offset);
79
136
  return buffer;
80
137
  };
81
- const newTaistampHandler = (config = {}) => {
82
- const { selector, signer } = config;
138
+ const validateHandlerConfig = (config) => {
139
+ const { cors, selector, signer } = config;
83
140
  if (signer === void 0 !== (selector === void 0)) {
84
141
  throw new TypeError(
85
142
  "newTaistampHandler: signer and selector must be set together"
86
143
  );
87
144
  }
145
+ if (cors !== void 0 && cors !== false && typeof cors !== "string") {
146
+ throw new TypeError(
147
+ "newTaistampHandler: cors must be false or a string origin"
148
+ );
149
+ }
88
150
  if (selector !== void 0 && !SELECTOR_PATTERN.test(selector)) {
89
151
  throw new TypeError(
90
152
  `newTaistampHandler: selector must match ${SELECTOR_PATTERN.source}`
91
153
  );
92
154
  }
155
+ return config;
156
+ };
157
+ const fromHandlerConfig = (config) => {
158
+ const { cors, selector, signer } = validateHandlerConfig(config);
159
+ const corsHeaders = buildCORSHeaders(cors);
160
+ const addSignature = selector !== void 0 && signer !== void 0 ? async (headers, label, nonce) => {
161
+ const payload = composeSignaturePayload(
162
+ label,
163
+ TAI_LEAP_SECONDS,
164
+ selector,
165
+ nonce
166
+ );
167
+ const signature = await signer.sign(payload);
168
+ headers.set(TAI64N_HEADER_KEY_SELECTOR, selector);
169
+ headers.set(
170
+ TAI64N_HEADER_SIGNATURE,
171
+ encodeStructuredBinary(signature)
172
+ );
173
+ } : void 0;
174
+ return { addSignature, corsHeaders };
175
+ };
176
+ const newTaistampHandler = (config = {}) => {
177
+ const { addSignature, corsHeaders } = fromHandlerConfig(config);
93
178
  return async (request) => {
179
+ if (request.method === "OPTIONS") {
180
+ return new Response(void 0, {
181
+ status: 200,
182
+ headers: { allow: ALLOW_HEADER, ...corsHeaders.preflight }
183
+ });
184
+ }
94
185
  if (request.method !== "GET" && request.method !== "HEAD") {
95
186
  return new Response(void 0, {
96
187
  status: 405,
97
- headers: { allow: "GET, HEAD" }
188
+ headers: { allow: ALLOW_HEADER, ...corsHeaders.error }
98
189
  });
99
190
  }
100
- const nonce = request.headers.get(TAI64N_HEADER_NONCE);
101
- if (nonce !== null && nonce.includes(",")) {
102
- return new Response(void 0, { status: 400 });
103
- }
191
+ const nonce = extractNonce(request.headers);
104
192
  const label = tai64nLabel();
105
193
  const headers = new Headers({
106
194
  "cache-control": "no-store",
107
195
  "content-length": String(TAI64N_CONTENT_LENGTH),
108
196
  "content-type": TAI64N_CONTENT_TYPE,
109
- [TAI64N_HEADER_LEAP_SECONDS]: String(TAI_OFFSET)
197
+ [TAI64N_HEADER_LEAP_SECONDS]: String(TAI_LEAP_SECONDS),
198
+ ...corsHeaders.response
110
199
  });
111
- if (nonce !== null) {
200
+ if (nonce) {
112
201
  headers.set(TAI64N_HEADER_NONCE, nonce);
113
- const nonceLength = textEncoder.encode(nonce).length;
114
- const inRange = nonceLength >= NONCE_MIN_OCTETS && nonceLength <= NONCE_MAX_OCTETS;
115
- if (request.method === "GET" && signer !== void 0 && selector !== void 0 && inRange) {
116
- const message = taistampSignedPayload(
117
- label,
118
- TAI_OFFSET,
119
- selector,
120
- nonce
121
- );
122
- const signature = await signer.sign(message);
123
- headers.set(TAI64N_HEADER_KEY_SELECTOR, selector);
124
- headers.set(
125
- TAI64N_HEADER_SIGNATURE,
126
- encodeStructuredBinary(signature)
127
- );
202
+ if (request.method === "GET" && addSignature) {
203
+ await addSignature(headers, label, nonce);
128
204
  }
129
205
  }
130
206
  const body = request.method === "HEAD" ? void 0 : label;
@@ -138,5 +214,5 @@ const newEd25519Signer = (key) => ({
138
214
 
139
215
  const VERSION = pkg.version;
140
216
 
141
- export { TAI64N_CONTENT_LENGTH, TAI64N_CONTENT_TYPE, TAI64N_HEADER_KEY_SELECTOR, TAI64N_HEADER_LEAP_SECONDS, TAI64N_HEADER_NONCE, TAI64N_HEADER_SIGNATURE, TAI64N_PATH, TAI64_EPOCH_HI, TAI_OFFSET, VERSION, fromUTC, newEd25519Signer, newTaistampHandler, now, tai64nLabel, tai64nLabelFromUTC, taistampSignedPayload };
217
+ export { TAI64N_CONTENT_LENGTH, TAI64N_CONTENT_TYPE, TAI64N_HEADER_KEY_SELECTOR, TAI64N_HEADER_LEAP_SECONDS, TAI64N_HEADER_NONCE, TAI64N_HEADER_SIGNATURE, TAI64N_PATH, TAI64_EPOCH_HI, TAI_LEAP_SECONDS, TAI_LEAP_SECONDS_MAX, VERSION, asLeapSeconds, asNonce, composeSignaturePayload, extractLeapSeconds, fromUTC, newEd25519Signer, newTaistampHandler, now, tai64nLabel, tai64nLabelFromUTC };
142
218
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../src/const.ts","../src/utils.ts","../src/handler.ts","../src/signer.ts","../src/index.ts"],"sourcesContent":["export const TAI_OFFSET: number = 37;\n\nexport const TAI64N_PATH = '/.well-known/taistamp';\n\nexport const TAI64N_CONTENT_TYPE = 'application/tai64n';\nexport const TAI64N_CONTENT_LENGTH = 1 + 16 + 8; // '@' + sec (16 hex chars) + nano (8 hex chars)\n\nexport const TAI64N_HEADER_KEY_SELECTOR = 'TAI-Key-Selector';\nexport const TAI64N_HEADER_LEAP_SECONDS = 'TAI-Leap-Seconds';\nexport const TAI64N_HEADER_NONCE = 'TAI-Nonce';\nexport const TAI64N_HEADER_SIGNATURE = 'TAI-Signature';\n\nexport const TAI64_EPOCH_HI = 0x40_00_00_00;\n","import { TAI64_EPOCH_HI, TAI_OFFSET } from './const';\n\ntype timestamp = {\n nano: number\n sec: number\n\n offset?: number\n};\n\nexport const fromUTC = (utc: number): timestamp => {\n // TODO: leap seconds table\n const sec = Math.floor(utc / 1000) + TAI_OFFSET;\n const nano = (utc % 1000) * 1e6;\n return { sec, nano, offset: TAI_OFFSET };\n};\n\nexport const now = (): timestamp => {\n const utc = Date.now();\n return fromUTC(utc);\n};\n\nexport const tai64nLabel = (value?: timestamp): string => {\n const { sec, nano } = value ?? now();\n\n const secHi = Math.trunc(sec / u32Range) + TAI64_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\nexport const tai64nLabelFromUTC = (utc: number): string => tai64nLabel(fromUTC(utc));\n\nconst u32Range = 0x1_00_00_00_00;\n","import type { Signer } from './signer';\nimport {\n TAI64N_CONTENT_LENGTH,\n TAI64N_CONTENT_TYPE,\n TAI64N_HEADER_KEY_SELECTOR,\n TAI64N_HEADER_LEAP_SECONDS,\n TAI64N_HEADER_NONCE,\n TAI64N_HEADER_SIGNATURE,\n TAI_OFFSET,\n} from './const';\nimport { tai64nLabel } from './utils';\n\nconst SELECTOR_PATTERN = /^[A-Za-z][\\dA-Za-z_-]{0,62}$/;\n\nconst NONCE_MIN_OCTETS = 14;\nconst NONCE_MAX_OCTETS = 174;\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Domain-separation tag prepended to every signed\n * payload. Versioned so a v2 protocol can use the same\n * key without colliding with v1 signatures, and\n * NUL-terminated so the boundary between tag and\n * label is unambiguous.\n */\nconst DOMAIN_SEPARATOR = textEncoder.encode('taistamp-v1\\0');\n\nconst asBytes = (source: BufferSource): Uint8Array => {\n if (source instanceof Uint8Array) {\n return source;\n }\n if (ArrayBuffer.isView(source)) {\n return new Uint8Array(\n source.buffer,\n source.byteOffset,\n source.byteLength,\n );\n }\n return new Uint8Array(source);\n};\n\n/**\n * Encode `source` as a Structured Field Value sf-binary\n * item per [RFC 8941 §3.3.5]: standard base64 with `=`\n * padding, wrapped in a leading and trailing colon.\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc8941#name-byte-sequences}\n */\nconst encodeStructuredBinary = (source: BufferSource): string => {\n // Spread is safe for the 64-byte signatures handled\n // here; revisit if larger payloads ever land.\n const bytes = asBytes(source);\n const standard = btoa(String.fromCodePoint(...bytes));\n return `:${standard}:`;\n};\n\n/**\n * Compose the byte sequence covered by a TAI-Signature.\n *\n * @param label - the 25-byte TAI64N label string the\n * server is returning\n * @param leapSeconds - the leap-seconds count the server\n * advertises in `TAI-Leap-Seconds`\n * @param selector - the key selector the server\n * advertises in `TAI-Key-Selector`; verifiers use\n * this to look up the public key in DNS at\n * `<selector>._taistamp.<host>`\n * @param nonce - the client-supplied nonce, echoed\n * verbatim in `TAI-Nonce`\n * @returns the byte sequence verifiers reconstruct\n * from the response and pass to their public-key\n * verify routine. The framing is the\n * domain-separation tag (`taistamp-v1` plus a\n * trailing NUL byte), then the label bytes, then\n * the leap-seconds count as a 4-byte big-endian\n * unsigned integer, then a 1-byte selector length,\n * then the selector bytes, then the nonce bytes.\n *\n * @remarks\n * Binding the selector into the signed payload stops a\n * downgrade attacker from rewriting `TAI-Key-Selector`\n * to point at a compromised or weaker key — the\n * signature would no longer verify under that key.\n * `leapSeconds` is encoded as a 4-byte big-endian\n * unsigned integer; the selector is length-prefixed by\n * a single byte (selectors are ≤ 63 chars per\n * {@link newTaistampHandler}'s validation).\n */\nexport const taistampSignedPayload = (\n label: string,\n leapSeconds: number,\n selector: string,\n nonce: string,\n): ArrayBuffer => {\n const labelBytes = textEncoder.encode(label);\n const selectorBytes = textEncoder.encode(selector);\n const nonceBytes = textEncoder.encode(nonce);\n\n const buffer = new ArrayBuffer(\n DOMAIN_SEPARATOR.length +\n labelBytes.length +\n 4 +\n 1 +\n selectorBytes.length +\n nonceBytes.length,\n );\n const view = new Uint8Array(buffer);\n\n let offset = 0;\n view.set(DOMAIN_SEPARATOR, offset);\n offset += DOMAIN_SEPARATOR.length;\n view.set(labelBytes, offset);\n offset += labelBytes.length;\n new DataView(buffer).setUint32(offset, leapSeconds, false);\n offset += 4;\n view[offset] = selectorBytes.length;\n offset += 1;\n view.set(selectorBytes, offset);\n offset += selectorBytes.length;\n view.set(nonceBytes, offset);\n\n return buffer;\n};\n\n/**\n * Configuration for {@link newTaistampHandler}.\n *\n * @remarks\n * `signer` and `selector` are co-required: pass both\n * to enable authenticated responses, or neither for\n * an unsigned handler. Passing only one is rejected\n * at construction time — without the selector\n * verifiers cannot find the key in DNS, and a\n * selector without a signer is a misconfiguration.\n */\nexport interface TaistampHandlerConfig {\n /**\n * Key selector advertised in the `TAI-Key-Selector`\n * response header and bound into the signed payload.\n * Verifiers look up the public key at\n * `<selector>._taistamp.<host>` in DNS.\n *\n * Must match `[A-Za-z][A-Za-z0-9_-]{0,62}` (a single\n * DNS label starting with a letter, using\n * DKIM-compatible characters and a valid sf-token);\n * rotate by changing the selector and publishing a\n * new TXT record.\n */\n selector?: string\n\n /**\n * {@link Signer} that produces `TAI-Signature` over\n * the framed payload from {@link taistampSignedPayload}.\n * Without a signer the nonce is still echoed but the\n * response is unsigned.\n */\n signer?: Signer\n}\n\n/**\n * Build a handler for `/.well-known/taistamp`.\n *\n * @param config - optional {@link TaistampHandlerConfig}\n * @returns an `async (request) => Response` callable\n * directly as a Web `fetch` handler or as a Hono\n * route handler.\n *\n * @throws TypeError if `signer` and `selector` are not\n * both set or both unset, or if `selector` does not\n * match `[A-Za-z][A-Za-z0-9_-]{0,62}`.\n *\n * @remarks\n * Behaviour:\n *\n * - `GET` / `HEAD` — body is a fresh 25-byte TAI64N\n * label (`HEAD` omits the body). Response headers:\n * Content-Type `application/tai64n`, Content-Length\n * `25`, Cache-Control `no-store`, plus\n * `TAI-Leap-Seconds` carrying the current count.\n * - Any other method — `405 Method Not Allowed` with\n * `Allow: GET, HEAD`.\n * - Request with more than one `TAI-Nonce` header —\n * `400 Bad Request`. Stricter than the spec's\n * \"treat as absent\" rule: a duplicated singleton\n * field is malformed input, so we refuse rather\n * than silently down-ranking it to unsigned.\n * - Request `TAI-Nonce` — the value is echoed verbatim\n * in the response.\n * - Request `TAI-Nonce` *and* `signer` configured *and*\n * the request method is `GET` *and* the nonce field\n * value is between 14 and 174 octets — adds\n * `TAI-Key-Selector` and `TAI-Signature` (sf-binary)\n * over the bytes produced by\n * {@link taistampSignedPayload}. The\n * domain-separation tag means the same key cannot\n * be tricked into producing valid signatures for\n * other protocols. `HEAD` and `405` responses are\n * never signed.\n *\n * The corresponding public key is expected to be\n * published out-of-band as a DNS TXT record at\n * `<selector>._taistamp.<host>` — verifiers fetch the\n * key by selector so the operator can rotate keys by\n * publishing a new selector while the old one is\n * still cached.\n *\n * @see {@link https://cr.yp.to/libtai/tai64.html} for\n * TAI64N format\n */\nexport const newTaistampHandler = (\n config: TaistampHandlerConfig = {},\n): ((request: Request) => Promise<Response>) => {\n const { selector, signer } = config;\n\n if ((signer === undefined) !== (selector === undefined)) {\n throw new TypeError(\n 'newTaistampHandler: signer and selector must be set together',\n );\n }\n if (selector !== undefined && !SELECTOR_PATTERN.test(selector)) {\n throw new TypeError(\n `newTaistampHandler: selector must match ${SELECTOR_PATTERN.source}`,\n );\n }\n\n return async (request) => {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response(undefined, {\n status: 405,\n headers: { allow: 'GET, HEAD' },\n });\n }\n\n const nonce = request.headers.get(TAI64N_HEADER_NONCE);\n\n // `TAI-Nonce` is a singleton sf-binary; a valid value\n // contains no `,` of its own, so a comma in the joined\n // header value means the client sent the field more\n // than once.\n if (nonce !== null && nonce.includes(',')) {\n return new Response(undefined, { status: 400 });\n }\n\n const label = tai64nLabel();\n\n const headers = new Headers({\n 'cache-control': 'no-store',\n 'content-length': String(TAI64N_CONTENT_LENGTH),\n 'content-type': TAI64N_CONTENT_TYPE,\n [TAI64N_HEADER_LEAP_SECONDS]: String(TAI_OFFSET),\n });\n\n if (nonce !== null) {\n headers.set(TAI64N_HEADER_NONCE, nonce);\n\n const nonceLength = textEncoder.encode(nonce).length;\n const inRange =\n nonceLength >= NONCE_MIN_OCTETS && nonceLength <= NONCE_MAX_OCTETS;\n\n if (\n request.method === 'GET' &&\n signer !== undefined &&\n selector !== undefined &&\n inRange\n ) {\n const message = taistampSignedPayload(\n label,\n TAI_OFFSET,\n selector,\n nonce,\n );\n const signature = await signer.sign(message);\n headers.set(TAI64N_HEADER_KEY_SELECTOR, selector);\n headers.set(\n TAI64N_HEADER_SIGNATURE,\n encodeStructuredBinary(signature),\n );\n }\n }\n\n const body = request.method === 'HEAD' ? undefined : label;\n return new Response(body, { status: 200, headers });\n };\n};\n","/**\n * Generic signer abstraction over a private key.\n *\n * @remarks\n * The handler doesn't care which algorithm or key\n * store produced the signature, only that signing\n * succeeded. Pluggable so consumers can wire in\n * HSM-backed, KMS-backed, or in-process WebCrypto\n * signers without touching the handler. Verifiers\n * must agree on the algorithm and the public key\n * out-of-band — typically by pinning the public key\n * to a DNS TXT record.\n */\nexport interface Signer {\n /**\n * Produce a signature over `message`.\n *\n * @param message - bytes to sign; the caller is\n * responsible for any framing or domain separation.\n * Typed as {@link BufferSource} to match WebCrypto's\n * own input shape — any `ArrayBuffer` or typed-array\n * view is accepted.\n * @returns the raw signature bytes (algorithm-defined\n * length and encoding) as an `ArrayBuffer`, matching\n * WebCrypto's native output shape\n */\n sign: (message: BufferSource) => Promise<ArrayBuffer>\n}\n\n/**\n * Build a {@link Signer} backed by a WebCrypto Ed25519\n * private `CryptoKey`.\n *\n * @param key - Ed25519 private key with `'sign'` in\n * `key.usages`. The algorithm `name` must be\n * `'Ed25519'`.\n * @returns a {@link Signer} producing 64-byte raw\n * Ed25519 signatures (R ‖ s, RFC 8032)\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc8032}\n */\nexport const newEd25519Signer = (key: CryptoKey): Signer => ({\n sign: async (message) => crypto.subtle.sign('Ed25519', key, message),\n});\n","import pkg from '../package.json' with { type: 'json' };\n\n/** Package version from package.json. */\nexport const VERSION: string = pkg.version;\n\nexport * from './const';\nexport {\n newTaistampHandler,\n type TaistampHandlerConfig,\n taistampSignedPayload,\n} from './handler';\nexport {\n newEd25519Signer,\n type Signer,\n} from './signer';\nexport {\n fromUTC,\n now,\n tai64nLabel,\n tai64nLabelFromUTC,\n} from './utils';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","sources":["../src/const.ts","../src/cors.ts","../src/leap-seconds.ts","../src/nonce.ts","../src/utils.ts","../src/handler.ts","../src/signer.ts","../src/index.ts"],"sourcesContent":["export const TAI64N_PATH = '/.well-known/taistamp';\n\nexport const TAI64N_CONTENT_TYPE = 'application/tai64n';\nexport const TAI64N_CONTENT_LENGTH = 1 + 16 + 8; // '@' + sec (16 hex chars) + nano (8 hex chars)\n\nexport const TAI64N_HEADER_KEY_SELECTOR = 'TAI-Key-Selector';\nexport const TAI64N_HEADER_LEAP_SECONDS = 'TAI-Leap-Seconds';\nexport const TAI64N_HEADER_NONCE = 'TAI-Nonce';\nexport const TAI64N_HEADER_SIGNATURE = 'TAI-Signature';\n\nexport const TAI64_EPOCH_HI = 0x40_00_00_00;\n","import {\n TAI64N_HEADER_KEY_SELECTOR,\n TAI64N_HEADER_LEAP_SECONDS,\n TAI64N_HEADER_NONCE,\n TAI64N_HEADER_SIGNATURE,\n} from './const';\n\n// `Access-Control-Allow-Methods` (Fetch) is the list\n// of methods JS would ever preflight, so `OPTIONS` is\n// omitted. This is intentionally narrower than the\n// `Allow` header (RFC 9110 §9.3.7 method discovery,\n// `GET, HEAD, OPTIONS`) the handler itself emits.\nconst CORS_ALLOW_METHODS = 'GET, HEAD';\nconst CORS_ALLOW_HEADERS = TAI64N_HEADER_NONCE;\nconst CORS_EXPOSE_HEADERS = [\n TAI64N_HEADER_LEAP_SECONDS,\n TAI64N_HEADER_NONCE,\n TAI64N_HEADER_KEY_SELECTOR,\n TAI64N_HEADER_SIGNATURE,\n].join(', ');\n\n/**\n * The three CORS header maps the handler splices into\n * responses, keyed by response kind.\n *\n * - `preflight` — added to `OPTIONS 200` replies.\n * - `response` — added to successful `GET` / `HEAD`\n * replies; carries `Access-Control-Expose-Headers`\n * so browser JS can read the `TAI-*` headers.\n * - `error` — added to `405` replies; just the origin\n * header (and `Vary` when scoped).\n */\nexport type CORSHeaderSets = {\n error: Record<string, string>\n preflight: Record<string, string>\n response: Record<string, string>\n};\n\n/**\n * Pre-bake the three CORS header maps the handler\n * splices into responses, keyed by response kind.\n * `cors === false` collapses every map to `{}` so the\n * spread is a no-op; missing or empty input falls back\n * to `'*'`; `cors === '*'` skips `Vary: Origin` (a\n * wildcard does not vary by origin); a scoped origin\n * adds `Vary: Origin` so caches can keep per-origin\n * variants distinct.\n */\nexport const buildCORSHeaders = (\n cors: false | string | undefined,\n): CORSHeaderSets => {\n if (cors === false) {\n return { error: {}, preflight: {}, response: {} };\n }\n const origin = cors || '*';\n const vary: Record<string, string> =\n origin === '*' ? {} : { vary: 'Origin' };\n return {\n error: {\n 'access-control-allow-origin': origin,\n ...vary,\n },\n preflight: {\n 'access-control-allow-origin': origin,\n 'access-control-allow-methods': CORS_ALLOW_METHODS,\n 'access-control-allow-headers': CORS_ALLOW_HEADERS,\n 'access-control-expose-headers': CORS_EXPOSE_HEADERS,\n ...vary,\n },\n response: {\n 'access-control-allow-origin': origin,\n 'access-control-expose-headers': CORS_EXPOSE_HEADERS,\n ...vary,\n },\n };\n};\n","// cspell:words IERS\n\nimport { TAI64N_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.1.\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 non-integer, negative,\n * or exceeds {@link TAI_LEAP_SECONDS_MAX}.\n */\nexport const asLeapSeconds = (\n value: number,\n): LeapSeconds | undefined => {\n if (\n !Number.isInteger(value) ||\n value < 0 ||\n value > TAI_LEAP_SECONDS_MAX\n ) 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.1\n * collapsed into one verdict.\n */\nexport const extractLeapSeconds = (\n headers: Headers,\n): LeapSeconds | undefined => {\n const raw = headers.get(TAI64N_HEADER_LEAP_SECONDS);\n if (!raw || !DECIMAL_INTEGER.test(raw)) return undefined;\n return asLeapSeconds(Number(raw));\n};\n","import { TAI64N_HEADER_NONCE } from './const';\n\n/**\n * Lower bound on `TAI-Nonce` field-value octets. A\n * field shorter than this is treated as absent (spec\n * §5.2). sf-binary is ASCII-only — the string length\n * equals the octet count.\n */\nexport const NONCE_MIN_OCTETS = 14;\n\n/**\n * Upper bound on `TAI-Nonce` field-value octets. A\n * field longer than this is treated as absent (spec\n * §5.2).\n */\nexport const NONCE_MAX_OCTETS = 174;\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 excluded — a\n * zero-length nonce is treated as absent per spec\n * §5.2. The alphabet contains no `,`, so a duplicated\n * field (joined by the Web `Headers` API with `,`)\n * fails the same check.\n */\nconst 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\ndeclare const NonceBrand: unique symbol;\n\n/**\n * `string` that has been confirmed to satisfy the\n * sf-binary syntax of RFC 9651 §3.3.5 and the\n * `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]` length range\n * required by spec §5.2. Construct only via\n * {@link asNonce} or {@link extractNonce}; the brand\n * prevents arbitrary strings from reaching the\n * signing path.\n */\nexport type Nonce = string & { readonly [NonceBrand]: never };\n\n/**\n * Brand `value` as a {@link Nonce} when it satisfies\n * sf-binary syntax (RFC 9651 §3.3.5) and falls inside\n * `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]`. Returns\n * `undefined` for anything else — every \"treat as\n * absent\" case in spec §5.2 collapsed into one\n * verdict.\n */\nexport const asNonce = (value: string): Nonce | undefined => {\n if (\n !value ||\n value.length < NONCE_MIN_OCTETS ||\n value.length > NONCE_MAX_OCTETS ||\n !SF_BINARY_PATTERN.test(value)\n ) return undefined;\n return value as Nonce;\n};\n\n/**\n * Extract a usable `TAI-Nonce` from request headers.\n * Returns `undefined` when the field is missing or\n * fails {@link asNonce} validation.\n */\nexport const extractNonce = (headers: Headers): Nonce | undefined => {\n const value = headers.get(TAI64N_HEADER_NONCE);\n return value === null ? undefined : asNonce(value);\n};\n","import { TAI64_EPOCH_HI } from './const';\nimport { TAI_LEAP_SECONDS } from './leap-seconds';\n\ntype timestamp = {\n nano: number\n sec: number\n\n offset?: number\n};\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\nexport const now = (): timestamp => {\n const utc = Date.now();\n return fromUTC(utc);\n};\n\nexport const tai64nLabel = (value?: timestamp): string => {\n const { sec, nano } = value ?? now();\n\n const secHi = Math.trunc(sec / u32Range) + TAI64_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\nexport const tai64nLabelFromUTC = (utc: number): string => tai64nLabel(fromUTC(utc));\n\nconst u32Range = 0x1_00_00_00_00;\n","import type { Signer } from './signer';\nimport {\n TAI64N_CONTENT_LENGTH,\n TAI64N_CONTENT_TYPE,\n TAI64N_HEADER_KEY_SELECTOR,\n TAI64N_HEADER_LEAP_SECONDS,\n TAI64N_HEADER_NONCE,\n TAI64N_HEADER_SIGNATURE,\n} from './const';\nimport { buildCORSHeaders } from './cors';\nimport { type LeapSeconds, TAI_LEAP_SECONDS } from './leap-seconds';\nimport { extractNonce, type Nonce } from './nonce';\nimport { tai64nLabel } from './utils';\n\nconst SELECTOR_PATTERN = /^[A-Za-z][\\dA-Za-z_-]{0,62}$/;\n\nconst ALLOW_HEADER = 'GET, HEAD, OPTIONS';\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Domain-separation tag prepended to every signed\n * payload. Versioned so a v2 protocol can use the same\n * key without colliding with v1 signatures, and\n * NUL-terminated so the boundary between tag and\n * label is unambiguous.\n */\nconst DOMAIN_SEPARATOR = textEncoder.encode('taistamp-v1\\0');\n\nconst asBytes = (source: BufferSource): Uint8Array => {\n if (source instanceof Uint8Array) {\n return source;\n }\n if (ArrayBuffer.isView(source)) {\n return new Uint8Array(\n source.buffer,\n source.byteOffset,\n source.byteLength,\n );\n }\n return new Uint8Array(source);\n};\n\n/**\n * Encode `source` as a Structured Field Value sf-binary\n * item per [RFC 9651 §3.3.5]: standard base64 with `=`\n * padding, wrapped in a leading and trailing colon.\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc9651#name-byte-sequences}\n */\nconst encodeStructuredBinary = (source: BufferSource): string => {\n // Spread is safe for the 64-byte signatures handled\n // here; revisit if larger payloads ever land.\n const bytes = asBytes(source);\n const standard = btoa(String.fromCodePoint(...bytes));\n return `:${standard}:`;\n};\n\n/**\n * Compose the byte sequence covered by a TAI-Signature.\n *\n * @param label - the 25-byte TAI64N label string the\n * server is returning\n * @param leapSeconds - the leap-seconds count the server\n * advertises in `TAI-Leap-Seconds`\n * @param selector - the key selector the server\n * advertises in `TAI-Key-Selector`; verifiers use\n * this to look up the public key in DNS at\n * `<selector>._taistamp.<host>`\n * @param nonce - the client-supplied nonce, echoed\n * verbatim in `TAI-Nonce`; brand a verifier-side\n * string with {@link asNonce} before passing it in\n * @returns the byte sequence verifiers reconstruct\n * from the response and pass to their public-key\n * verify routine. The framing is the\n * domain-separation tag (`taistamp-v1` plus a\n * trailing NUL byte), then the label bytes, then\n * the leap-seconds count as a 4-byte big-endian\n * unsigned integer, then a 1-byte selector length,\n * then the selector bytes, then the nonce bytes.\n *\n * @remarks\n * Binding the selector into the signed payload stops a\n * downgrade attacker from rewriting `TAI-Key-Selector`\n * to point at a compromised or weaker key — the\n * signature would no longer verify under that key.\n * `leapSeconds` is encoded as a 4-byte big-endian\n * unsigned integer; the selector is length-prefixed by\n * a single byte (selectors are ≤ 63 chars per\n * {@link newTaistampHandler}'s validation).\n */\nexport const composeSignaturePayload = (\n label: string,\n leapSeconds: LeapSeconds,\n selector: string,\n nonce: Nonce,\n): ArrayBuffer => {\n const labelBytes = textEncoder.encode(label);\n const selectorBytes = textEncoder.encode(selector);\n const nonceBytes = textEncoder.encode(nonce);\n\n const buffer = new ArrayBuffer(\n DOMAIN_SEPARATOR.length +\n labelBytes.length +\n 4 +\n 1 +\n selectorBytes.length +\n nonceBytes.length,\n );\n const view = new Uint8Array(buffer);\n\n let offset = 0;\n view.set(DOMAIN_SEPARATOR, offset);\n offset += DOMAIN_SEPARATOR.length;\n view.set(labelBytes, offset);\n offset += labelBytes.length;\n new DataView(buffer).setUint32(offset, leapSeconds, false);\n offset += 4;\n view[offset] = selectorBytes.length;\n offset += 1;\n view.set(selectorBytes, offset);\n offset += selectorBytes.length;\n view.set(nonceBytes, offset);\n\n return buffer;\n};\n\n/**\n * Configuration for {@link newTaistampHandler}.\n *\n * @remarks\n * `signer` and `selector` are co-required: pass both\n * to enable authenticated responses, or neither for\n * an unsigned handler. Passing only one is rejected\n * at construction time — without the selector\n * verifiers cannot find the key in DNS, and a\n * selector without a signer is a misconfiguration.\n */\nexport interface TaistampHandlerConfig {\n /**\n * Key selector advertised in the `TAI-Key-Selector`\n * response header and bound into the signed payload.\n * Verifiers look up the public key at\n * `<selector>._taistamp.<host>` in DNS.\n *\n * Must match `[A-Za-z][A-Za-z0-9_-]{0,62}` (a single\n * DNS label starting with a letter, using\n * DKIM-compatible characters and a valid sf-token);\n * rotate by changing the selector and publishing a\n * new TXT record.\n */\n selector?: string\n\n /**\n * {@link Signer} that produces `TAI-Signature` over\n * the framed payload from {@link composeSignaturePayload}.\n * Without a signer the nonce is still echoed but the\n * response is unsigned.\n */\n signer?: Signer\n\n /**\n * CORS origin policy. Defaults to `'*'`; pass `false`\n * to disable CORS entirely, or a specific origin\n * (e.g. `'https://example.com'`) to scope the policy.\n *\n * Every response (`GET` / `HEAD` / `OPTIONS` / `405`)\n * gains `Access-Control-Allow-Origin`; pre-flight\n * `OPTIONS` also carries `-Allow-Methods`,\n * `-Allow-Headers`, and `-Expose-Headers`; success\n * `GET` / `HEAD` carry `-Expose-Headers` so browser\n * JS can read the `TAI-*` response headers. A\n * non-`'*'` value adds `Vary: Origin` so caches can\n * keep per-origin variants distinct.\n *\n * Disabling CORS does not affect method discovery:\n * `OPTIONS` is still answered with `200` and\n * `Allow: GET, HEAD, OPTIONS` per RFC 9110 §9.3.7.\n */\n cors?: false | string\n}\n\n/**\n * Validate a {@link TaistampHandlerConfig} and return\n * it unchanged when every field is well-formed.\n * Throws `TypeError` otherwise so misconfiguration\n * surfaces at handler construction rather than on the\n * first request.\n *\n * @throws TypeError if `signer` and `selector` are not\n * both set or both unset, or if `selector` does not\n * match `[A-Za-z][A-Za-z0-9_-]{0,62}`.\n */\nconst validateHandlerConfig = (\n config: TaistampHandlerConfig,\n): TaistampHandlerConfig => {\n const { cors, selector, signer } = config;\n\n if ((signer === undefined) !== (selector === undefined)) {\n throw new TypeError(\n 'newTaistampHandler: signer and selector must be set together',\n );\n }\n if (cors !== undefined && cors !== false && typeof cors !== 'string') {\n throw new TypeError(\n 'newTaistampHandler: cors must be false or a string origin',\n );\n }\n if (selector !== undefined && !SELECTOR_PATTERN.test(selector)) {\n throw new TypeError(\n `newTaistampHandler: selector must match ${SELECTOR_PATTERN.source}`,\n );\n }\n\n return config;\n};\n\n/**\n * Validate a {@link TaistampHandlerConfig} and derive\n * the construction-time state the handler closure\n * captures: the pre-baked CORS header maps and an\n * `addSignature` helper that mutates a response\n * `Headers` to carry `TAI-Key-Selector` and\n * `TAI-Signature` over the framed payload, present\n * only when both `signer` and `selector` are\n * configured. Validation is delegated to\n * {@link validateHandlerConfig}.\n *\n * @throws TypeError per {@link validateHandlerConfig}.\n */\nconst fromHandlerConfig = (config: TaistampHandlerConfig) => {\n const { cors, selector, signer } = validateHandlerConfig(config);\n\n const corsHeaders = buildCORSHeaders(cors);\n\n const addSignature = selector !== undefined && signer !== undefined ?\n async (\n headers: Headers,\n label: string,\n nonce: Nonce,\n ): Promise<void> => {\n const payload = composeSignaturePayload(\n label, TAI_LEAP_SECONDS, selector, nonce,\n );\n const signature = await signer.sign(payload);\n headers.set(TAI64N_HEADER_KEY_SELECTOR, selector);\n headers.set(\n TAI64N_HEADER_SIGNATURE,\n encodeStructuredBinary(signature),\n );\n } :\n undefined;\n\n return { addSignature, corsHeaders };\n};\n\n/**\n * Build a handler for `/.well-known/taistamp`.\n *\n * @param config - optional {@link TaistampHandlerConfig}\n * @returns an `async (request) => Response` callable\n * directly as a Web `fetch` handler or as a Hono\n * route handler.\n *\n * @throws TypeError if `signer` and `selector` are not\n * both set or both unset, or if `selector` does not\n * match `[A-Za-z][A-Za-z0-9_-]{0,62}`.\n *\n * @remarks\n * Behaviour:\n *\n * - `GET` / `HEAD` — body is a fresh 25-byte TAI64N\n * label (`HEAD` omits the body). Response headers:\n * Content-Type `application/tai64n`, Content-Length\n * `25`, Cache-Control `no-store`, plus\n * `TAI-Leap-Seconds` carrying the current count.\n * - `OPTIONS` — `200` with `Allow: GET, HEAD, OPTIONS`.\n * When CORS is enabled (the default) the response\n * also carries `Access-Control-Allow-*` and\n * `-Expose-Headers` per\n * {@link TaistampHandlerConfig.cors}. `OPTIONS` is\n * never signed.\n * - Any other method — `405 Method Not Allowed` with\n * `Allow: GET, HEAD, OPTIONS`.\n * - Request `TAI-Nonce` — the value is echoed verbatim\n * in the response. A missing, empty, duplicated,\n * structurally malformed, or out-of-range\n * (14..174 octets) field is treated as absent (no\n * echo, no signature) per spec §5.2 — see\n * {@link extractNonce}.\n * - Request `TAI-Nonce` *and* `signer` configured *and*\n * the request method is `GET` — adds\n * `TAI-Key-Selector` and `TAI-Signature` (sf-binary)\n * over the bytes produced by\n * {@link composeSignaturePayload}. The\n * domain-separation tag means the same key cannot\n * be tricked into producing valid signatures for\n * other protocols. `HEAD`, `OPTIONS`, and `405`\n * responses are never signed.\n *\n * The corresponding public key is expected to be\n * published out-of-band as a DNS TXT record at\n * `<selector>._taistamp.<host>` — verifiers fetch the\n * key by selector so the operator can rotate keys by\n * publishing a new selector while the old one is\n * still cached.\n *\n * @see {@link https://cr.yp.to/libtai/tai64.html} for\n * TAI64N format\n */\nexport const newTaistampHandler = (\n config: TaistampHandlerConfig = {},\n): ((request: Request) => Promise<Response>) => {\n const { addSignature, corsHeaders } = fromHandlerConfig(config);\n\n return async (request) => {\n if (request.method === 'OPTIONS') {\n return new Response(undefined, {\n status: 200,\n headers: { allow: ALLOW_HEADER, ...corsHeaders.preflight },\n });\n }\n\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return new Response(undefined, {\n status: 405,\n headers: { allow: ALLOW_HEADER, ...corsHeaders.error },\n });\n }\n\n const nonce = extractNonce(request.headers);\n const label = tai64nLabel();\n\n const headers = new Headers({\n 'cache-control': 'no-store',\n 'content-length': String(TAI64N_CONTENT_LENGTH),\n 'content-type': TAI64N_CONTENT_TYPE,\n [TAI64N_HEADER_LEAP_SECONDS]: String(TAI_LEAP_SECONDS),\n ...corsHeaders.response,\n });\n\n if (nonce) {\n headers.set(TAI64N_HEADER_NONCE, nonce);\n if (request.method === 'GET' && addSignature) {\n await addSignature(headers, label, nonce);\n }\n }\n\n const body = request.method === 'HEAD' ? undefined : label;\n return new Response(body, { status: 200, headers });\n };\n};\n","/**\n * Generic signer abstraction over a private key.\n *\n * @remarks\n * The handler doesn't care which algorithm or key\n * store produced the signature, only that signing\n * succeeded. Pluggable so consumers can wire in\n * HSM-backed, KMS-backed, or in-process WebCrypto\n * signers without touching the handler. Verifiers\n * must agree on the algorithm and the public key\n * out-of-band — typically by pinning the public key\n * to a DNS TXT record.\n */\nexport interface Signer {\n /**\n * Produce a signature over `message`.\n *\n * @param message - bytes to sign; the caller is\n * responsible for any framing or domain separation.\n * Typed as {@link BufferSource} to match WebCrypto's\n * own input shape — any `ArrayBuffer` or typed-array\n * view is accepted.\n * @returns the raw signature bytes (algorithm-defined\n * length and encoding) as an `ArrayBuffer`, matching\n * WebCrypto's native output shape\n */\n sign: (message: BufferSource) => Promise<ArrayBuffer>\n}\n\n/**\n * Build a {@link Signer} backed by a WebCrypto Ed25519\n * private `CryptoKey`.\n *\n * @param key - Ed25519 private key with `'sign'` in\n * `key.usages`. The algorithm `name` must be\n * `'Ed25519'`.\n * @returns a {@link Signer} producing 64-byte raw\n * Ed25519 signatures (R ‖ s, RFC 8032)\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc8032}\n */\nexport const newEd25519Signer = (key: CryptoKey): Signer => ({\n sign: async (message) => crypto.subtle.sign('Ed25519', key, message),\n});\n","import pkg from '../package.json' with { type: 'json' };\n\n/** Package version from package.json. */\nexport const VERSION: string = pkg.version;\n\nexport * from './const';\nexport {\n composeSignaturePayload,\n newTaistampHandler,\n type TaistampHandlerConfig,\n} from './handler';\nexport {\n asLeapSeconds,\n extractLeapSeconds,\n type LeapSeconds,\n TAI_LEAP_SECONDS,\n TAI_LEAP_SECONDS_MAX,\n} from './leap-seconds';\nexport {\n asNonce,\n type Nonce,\n} from './nonce';\nexport {\n newEd25519Signer,\n type Signer,\n} from './signer';\nexport {\n fromUTC,\n now,\n tai64nLabel,\n tai64nLabelFromUTC,\n} from './utils';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kagal/taistamp",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "description": "Signed TAI64N timestamps over HTTP",
6
6
  "author": "Apptly Software Ltd <oss@apptly.co>",
@@ -31,8 +31,11 @@
31
31
  "dist"
32
32
  ],
33
33
  "devDependencies": {
34
+ "@cloudflare/vitest-pool-workers": "^0.13.1",
35
+ "@cloudflare/workers-types": "^4.20260317.1",
34
36
  "@kagal/build-tsdoc": "~0.1.0",
35
37
  "@kagal/cross-test": "~0.1.3",
38
+ "@noble/ed25519": "^2.1.0",
36
39
  "@poupe/eslint-config": "~0.9.1",
37
40
  "@types/node": "^20.19.39",
38
41
  "@vitest/coverage-istanbul": "^4.1.5",
@@ -49,7 +52,8 @@
49
52
  "pnpm": ">= 10.33.2"
50
53
  },
51
54
  "publishConfig": {
52
- "access": "public"
55
+ "access": "public",
56
+ "provenance": true
53
57
  },
54
58
  "scripts": {
55
59
  "build": "unbuild",