@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/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { Signer, Signer as Signer$1, newSigner as newEd25519Signer } from "@kagal/ed25519-secret";
1
+ import { LeapSeconds, 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, tai64nLabelFromUTC, tai64nLabelToUTC } from "./_chunks/time.mjs";
2
+ import { Bytes, KeyConfig, KeyRecord, Signer, Signer as Signer$1, Verifier, newSigner as newEd25519Signer, parseRecordToVerifier, parseSecretToKey, parseSecretsToKeys } from "@kagal/ed25519-secret";
2
3
  /**
3
4
  * Read a response body as a 7-bit ASCII string.
4
5
  *
@@ -23,71 +24,12 @@ declare const readASCII: (response: Response, context?: string) => Promise<strin
23
24
  *
24
25
  * Builds on {@link readASCII}, adding the structural
25
26
  * invariant every label satisfies: the body is exactly
26
- * `TAI64N_CONTENT_LENGTH` octets. Throws `TypeError` if the
27
+ * `TAI64N_LABEL_LENGTH` octets. Throws `TypeError` if the
27
28
  * length differs or the body carries a non-ASCII octet;
28
29
  * pass `context` to prefix that error message. Consumes the
29
30
  * response body.
30
31
  */
31
32
  declare const readLabel: (response: Response, context?: string) => Promise<string>;
32
- declare const TAISTAMP_PATH = "/.well-known/taistamp";
33
- /** @deprecated Renamed to {@link TAISTAMP_PATH}. */
34
- declare const TAI64N_PATH = "/.well-known/taistamp";
35
- declare const TAI64N_CONTENT_TYPE = "application/tai64n";
36
- declare const TAI64N_CONTENT_LENGTH: number;
37
- declare const TAI64N_HEADER_KEY_SELECTOR = "TAI-Key-Selector";
38
- declare const TAI64N_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
39
- declare const TAI64N_HEADER_NONCE = "TAI-Nonce";
40
- declare const TAI64N_HEADER_SIGNATURE = "TAI-Signature";
41
- declare const TAI64_EPOCH_HI = 1073741824;
42
- /**
43
- * Upper bound for `leapSeconds` in the taistamp signed
44
- * payload. The framing encodes the value as a 4-byte
45
- * big-endian unsigned integer, so any input outside
46
- * `[0, 2^32-1]` cannot be represented. Verifiers MUST
47
- * treat an out-of-range `TAI-Leap-Seconds` response
48
- * header as unsigned, per spec §5.3.
49
- */
50
- declare const TAI_LEAP_SECONDS_MAX = 4294967295;
51
- declare const LeapSecondsBrand: unique symbol;
52
- /**
53
- * `number` that has been confirmed to fit the
54
- * `[0, TAI_LEAP_SECONDS_MAX]` u32be range required by
55
- * the taistamp signed-payload framing. Construct only
56
- * via {@link extractLeapSeconds} or {@link asLeapSeconds};
57
- * the brand prevents an arbitrary number from reaching
58
- * the signing path.
59
- */
60
- type LeapSeconds = number & {
61
- readonly [LeapSecondsBrand]: never;
62
- };
63
- /**
64
- * Coerce a `number` to a {@link LeapSeconds}. Returns
65
- * `undefined` when `value` is non-integer, negative,
66
- * or exceeds {@link TAI_LEAP_SECONDS_MAX}.
67
- */
68
- declare const asLeapSeconds: (value: number) => LeapSeconds | undefined;
69
- /**
70
- * Current TAI − UTC offset in whole seconds, used by
71
- * `fromUTC()` and emitted in the `TAI-Leap-Seconds`
72
- * response header. The value 37 has been in force
73
- * since 2017-01-01; update on the next IERS leap-second
74
- * announcement.
75
- *
76
- * @remarks
77
- * Stays a single `LeapSeconds` until a leap-seconds
78
- * table is added so the offset can be computed for any
79
- * TAI second; this constant becomes redundant then.
80
- */
81
- declare const TAI_LEAP_SECONDS: LeapSeconds;
82
- /**
83
- * Extract a usable leap-seconds count from response
84
- * headers. Returns `undefined` when the
85
- * `TAI-Leap-Seconds` field is missing, empty,
86
- * non-numeric, non-integer, negative, or out-of-range
87
- * — every "treat as unsigned" case in spec §5.3
88
- * collapsed into one verdict.
89
- */
90
- declare const extractLeapSeconds: (headers: Headers) => LeapSeconds | undefined;
91
33
  declare const NonceBrand: unique symbol;
92
34
  /**
93
35
  * `string` that has been confirmed to satisfy the
@@ -96,9 +38,9 @@ declare const NonceBrand: unique symbol;
96
38
  * `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]` — the
97
39
  * pre-decode form of spec §5.4's normative
98
40
  * decoded-length bound of 7..129 octets. Construct
99
- * only via {@link asNonce} or {@link extractNonce};
100
- * the brand prevents arbitrary strings from reaching
101
- * the signing path.
41
+ * only via {@link asNonce}, {@link extractNonce}, or
42
+ * {@link newNonce}; the brand prevents arbitrary
43
+ * strings from reaching the signing path.
102
44
  */
103
45
  type Nonce = string & {
104
46
  readonly [NonceBrand]: never;
@@ -114,6 +56,25 @@ type Nonce = string & {
114
56
  * verdict.
115
57
  */
116
58
  declare const asNonce: (value: string) => Nonce | undefined;
59
+ /**
60
+ * Extract a usable `TAI-Nonce` from headers — the
61
+ * request on the serving side, the response's nonce
62
+ * echo on the verifying side. Returns `undefined` when
63
+ * the field is missing or fails {@link asNonce}
64
+ * validation.
65
+ */
66
+ declare const extractNonce: (headers: Headers) => Nonce | undefined;
67
+ /**
68
+ * Mint a fresh client `TAI-Nonce`: `byteLength` random
69
+ * bytes framed as an sf-binary item, branded directly —
70
+ * the result is conformant by construction.
71
+ * `byteLength` must be an integer within
72
+ * `[NONCE_MIN_BYTES, NONCE_MAX_BYTES]` —
73
+ * spec §5.4's decoded-length bound; anything else
74
+ * throws `TypeError`. `context` (default `'newNonce'`)
75
+ * prefixes the thrown error.
76
+ */
77
+ declare const newNonce: (byteLength?: number, context?: string) => Nonce;
117
78
  /**
118
79
  * Compose the byte sequence covered by a TAI-Signature.
119
80
  *
@@ -168,11 +129,11 @@ interface TaistampHandlerConfig {
168
129
  * Verifiers look up the public key at
169
130
  * `<selector>._taistamp.<host>` in DNS.
170
131
  *
171
- * Must match `[A-Za-z][A-Za-z0-9_-]{0,62}` (a single
172
- * DNS label starting with a letter, using
173
- * DKIM-compatible characters and a valid sf-token);
174
- * rotate by changing the selector and publishing a
175
- * new TXT record.
132
+ * Must match `[A-Za-z]([A-Za-z0-9_-]{0,61}[A-Za-z0-9])?`
133
+ * a single DNS-safe label that starts with a letter,
134
+ * ends with a letter or digit, and is also a valid
135
+ * Structured Field token; rotate by changing the
136
+ * selector and publishing a new TXT record.
176
137
  */
177
138
  selector?: string;
178
139
  /**
@@ -191,7 +152,8 @@ interface TaistampHandlerConfig {
191
152
  * gains `Access-Control-Allow-Origin`; pre-flight
192
153
  * `OPTIONS` also carries `-Allow-Methods`,
193
154
  * `-Allow-Headers`, `-Expose-Headers`, and
194
- * `-Max-Age: 600` per spec §5.2; success
155
+ * `-Max-Age` (default 600s, see {@link corsMaxAge})
156
+ * per spec §5.2; success
195
157
  * `GET` / `HEAD` carry `-Expose-Headers` so browser
196
158
  * JS can read the `TAI-*` response headers. A
197
159
  * non-`'*'` value adds `Vary: Origin` so caches can
@@ -202,6 +164,15 @@ interface TaistampHandlerConfig {
202
164
  * `Allow: GET, HEAD, OPTIONS` per RFC 9110 §9.3.7.
203
165
  */
204
166
  cors?: false | string;
167
+ /**
168
+ * `Access-Control-Max-Age` for pre-flight `OPTIONS`
169
+ * responses, in seconds. Defaults to 600 (10 minutes,
170
+ * the spec §5.2 floor); a value below 600 clamps up to
171
+ * it so the pre-flight stays spec-compliant. Ignored
172
+ * when `cors` is `false`. Must be a non-negative
173
+ * integer.
174
+ */
175
+ corsMaxAge?: number;
205
176
  }
206
177
  /**
207
178
  * Build a handler for `/.well-known/taistamp`.
@@ -212,15 +183,18 @@ interface TaistampHandlerConfig {
212
183
  * route handler.
213
184
  *
214
185
  * @throws TypeError if `signer` and `selector` are not
215
- * both set or both unset, or if `selector` does not
216
- * match `[A-Za-z][A-Za-z0-9_-]{0,62}`.
186
+ * both set or both unset, if `selector` does not match
187
+ * `[A-Za-z]([A-Za-z0-9_-]{0,61}[A-Za-z0-9])?`, or if
188
+ * `corsMaxAge` is not a non-negative integer.
217
189
  *
218
190
  * @remarks
219
191
  * Behaviour:
220
192
  *
221
193
  * - `GET` / `HEAD` — body is a fresh 25-byte TAI64N
222
194
  * label (`HEAD` omits the body). Response headers:
223
- * Content-Type `application/tai64n`, Content-Length
195
+ * Content-Type `application/tai64n`, Content-Disposition
196
+ * `inline` (so a browser renders the label in place
197
+ * rather than offering it as a download), Content-Length
224
198
  * `25`, Cache-Control `no-store`, plus
225
199
  * `TAI-Leap-Seconds` carrying the current count.
226
200
  * - `OPTIONS` — `200` with `Allow: GET, HEAD, OPTIONS`.
@@ -259,16 +233,25 @@ interface TaistampHandlerConfig {
259
233
  * TAI64N format
260
234
  */
261
235
  declare const newTaistampHandler: (config?: TaistampHandlerConfig) => ((request: Request) => Promise<Response>);
262
- type timestamp = {
263
- nano: number;
264
- sec: number;
265
- offset?: number;
266
- };
267
- declare const fromUTC: (utc: number) => timestamp;
268
- declare const now: () => timestamp;
269
- declare const tai64nLabel: (value?: timestamp) => string;
270
- declare const tai64nLabelFromUTC: (utc: number) => string;
236
+ /**
237
+ * Decode a `TAI-Signature` wire value into the raw
238
+ * Ed25519 signature bytes. Returns `undefined` when
239
+ * `value` fails sf-binary syntax (RFC 9651 §3.3.5) or
240
+ * does not decode to exactly `SIGNATURE_BYTES`
241
+ * octets a malformed field is equivalent to a
242
+ * missing one. This validates form only; semantics
243
+ * involving other fields (nonce echo, selector,
244
+ * verification) stay with the caller.
245
+ */
246
+ declare const asSignature: (value: string) => Bytes | undefined;
247
+ /**
248
+ * Extract the raw Ed25519 signature from response
249
+ * headers. Returns `undefined` when the
250
+ * `TAI-Signature` field is missing or fails
251
+ * {@link asSignature} validation.
252
+ */
253
+ declare const extractSignature: (headers: Headers) => Bytes | undefined;
271
254
  /** Package version from package.json. */
272
255
  declare const VERSION: string;
273
- export { type LeapSeconds, type Nonce, type Signer, 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, TAISTAMP_PATH, TAI_LEAP_SECONDS, TAI_LEAP_SECONDS_MAX, type TaistampHandlerConfig, VERSION, asLeapSeconds, asNonce, composeSignaturePayload, extractLeapSeconds, fromUTC, newEd25519Signer, newTaistampHandler, now, readASCII, readLabel, tai64nLabel, tai64nLabelFromUTC };
256
+ export { type KeyConfig, type KeyRecord, type LeapSeconds, type Nonce, type Signer, 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, type TaistampHandlerConfig, VERSION, type Verifier, asLeapSeconds, asNonce, asSignature, composeSignaturePayload, extractLeapSeconds, extractNonce, extractSignature, newEd25519Signer, newNonce, newTaistampHandler, parseRecordToVerifier, parseSecretToKey, parseSecretsToKeys, readASCII, readLabel, tai64nLabelFromUTC, tai64nLabelToUTC };
274
257
  //# sourceMappingURL=index.d.mts.map
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Signer, Signer as Signer$1, newSigner as newEd25519Signer } from "@kagal/ed25519-secret";
1
+ import { LeapSeconds, 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, tai64nLabelFromUTC, tai64nLabelToUTC } from "./_chunks/time.mjs";
2
+ import { Bytes, KeyConfig, KeyRecord, Signer, Signer as Signer$1, Verifier, newSigner as newEd25519Signer, parseRecordToVerifier, parseSecretToKey, parseSecretsToKeys } from "@kagal/ed25519-secret";
2
3
  /**
3
4
  * Read a response body as a 7-bit ASCII string.
4
5
  *
@@ -23,71 +24,12 @@ declare const readASCII: (response: Response, context?: string) => Promise<strin
23
24
  *
24
25
  * Builds on {@link readASCII}, adding the structural
25
26
  * invariant every label satisfies: the body is exactly
26
- * `TAI64N_CONTENT_LENGTH` octets. Throws `TypeError` if the
27
+ * `TAI64N_LABEL_LENGTH` octets. Throws `TypeError` if the
27
28
  * length differs or the body carries a non-ASCII octet;
28
29
  * pass `context` to prefix that error message. Consumes the
29
30
  * response body.
30
31
  */
31
32
  declare const readLabel: (response: Response, context?: string) => Promise<string>;
32
- declare const TAISTAMP_PATH = "/.well-known/taistamp";
33
- /** @deprecated Renamed to {@link TAISTAMP_PATH}. */
34
- declare const TAI64N_PATH = "/.well-known/taistamp";
35
- declare const TAI64N_CONTENT_TYPE = "application/tai64n";
36
- declare const TAI64N_CONTENT_LENGTH: number;
37
- declare const TAI64N_HEADER_KEY_SELECTOR = "TAI-Key-Selector";
38
- declare const TAI64N_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
39
- declare const TAI64N_HEADER_NONCE = "TAI-Nonce";
40
- declare const TAI64N_HEADER_SIGNATURE = "TAI-Signature";
41
- declare const TAI64_EPOCH_HI = 1073741824;
42
- /**
43
- * Upper bound for `leapSeconds` in the taistamp signed
44
- * payload. The framing encodes the value as a 4-byte
45
- * big-endian unsigned integer, so any input outside
46
- * `[0, 2^32-1]` cannot be represented. Verifiers MUST
47
- * treat an out-of-range `TAI-Leap-Seconds` response
48
- * header as unsigned, per spec §5.3.
49
- */
50
- declare const TAI_LEAP_SECONDS_MAX = 4294967295;
51
- declare const LeapSecondsBrand: unique symbol;
52
- /**
53
- * `number` that has been confirmed to fit the
54
- * `[0, TAI_LEAP_SECONDS_MAX]` u32be range required by
55
- * the taistamp signed-payload framing. Construct only
56
- * via {@link extractLeapSeconds} or {@link asLeapSeconds};
57
- * the brand prevents an arbitrary number from reaching
58
- * the signing path.
59
- */
60
- type LeapSeconds = number & {
61
- readonly [LeapSecondsBrand]: never;
62
- };
63
- /**
64
- * Coerce a `number` to a {@link LeapSeconds}. Returns
65
- * `undefined` when `value` is non-integer, negative,
66
- * or exceeds {@link TAI_LEAP_SECONDS_MAX}.
67
- */
68
- declare const asLeapSeconds: (value: number) => LeapSeconds | undefined;
69
- /**
70
- * Current TAI − UTC offset in whole seconds, used by
71
- * `fromUTC()` and emitted in the `TAI-Leap-Seconds`
72
- * response header. The value 37 has been in force
73
- * since 2017-01-01; update on the next IERS leap-second
74
- * announcement.
75
- *
76
- * @remarks
77
- * Stays a single `LeapSeconds` until a leap-seconds
78
- * table is added so the offset can be computed for any
79
- * TAI second; this constant becomes redundant then.
80
- */
81
- declare const TAI_LEAP_SECONDS: LeapSeconds;
82
- /**
83
- * Extract a usable leap-seconds count from response
84
- * headers. Returns `undefined` when the
85
- * `TAI-Leap-Seconds` field is missing, empty,
86
- * non-numeric, non-integer, negative, or out-of-range
87
- * — every "treat as unsigned" case in spec §5.3
88
- * collapsed into one verdict.
89
- */
90
- declare const extractLeapSeconds: (headers: Headers) => LeapSeconds | undefined;
91
33
  declare const NonceBrand: unique symbol;
92
34
  /**
93
35
  * `string` that has been confirmed to satisfy the
@@ -96,9 +38,9 @@ declare const NonceBrand: unique symbol;
96
38
  * `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]` — the
97
39
  * pre-decode form of spec §5.4's normative
98
40
  * decoded-length bound of 7..129 octets. Construct
99
- * only via {@link asNonce} or {@link extractNonce};
100
- * the brand prevents arbitrary strings from reaching
101
- * the signing path.
41
+ * only via {@link asNonce}, {@link extractNonce}, or
42
+ * {@link newNonce}; the brand prevents arbitrary
43
+ * strings from reaching the signing path.
102
44
  */
103
45
  type Nonce = string & {
104
46
  readonly [NonceBrand]: never;
@@ -114,6 +56,25 @@ type Nonce = string & {
114
56
  * verdict.
115
57
  */
116
58
  declare const asNonce: (value: string) => Nonce | undefined;
59
+ /**
60
+ * Extract a usable `TAI-Nonce` from headers — the
61
+ * request on the serving side, the response's nonce
62
+ * echo on the verifying side. Returns `undefined` when
63
+ * the field is missing or fails {@link asNonce}
64
+ * validation.
65
+ */
66
+ declare const extractNonce: (headers: Headers) => Nonce | undefined;
67
+ /**
68
+ * Mint a fresh client `TAI-Nonce`: `byteLength` random
69
+ * bytes framed as an sf-binary item, branded directly —
70
+ * the result is conformant by construction.
71
+ * `byteLength` must be an integer within
72
+ * `[NONCE_MIN_BYTES, NONCE_MAX_BYTES]` —
73
+ * spec §5.4's decoded-length bound; anything else
74
+ * throws `TypeError`. `context` (default `'newNonce'`)
75
+ * prefixes the thrown error.
76
+ */
77
+ declare const newNonce: (byteLength?: number, context?: string) => Nonce;
117
78
  /**
118
79
  * Compose the byte sequence covered by a TAI-Signature.
119
80
  *
@@ -168,11 +129,11 @@ interface TaistampHandlerConfig {
168
129
  * Verifiers look up the public key at
169
130
  * `<selector>._taistamp.<host>` in DNS.
170
131
  *
171
- * Must match `[A-Za-z][A-Za-z0-9_-]{0,62}` (a single
172
- * DNS label starting with a letter, using
173
- * DKIM-compatible characters and a valid sf-token);
174
- * rotate by changing the selector and publishing a
175
- * new TXT record.
132
+ * Must match `[A-Za-z]([A-Za-z0-9_-]{0,61}[A-Za-z0-9])?`
133
+ * a single DNS-safe label that starts with a letter,
134
+ * ends with a letter or digit, and is also a valid
135
+ * Structured Field token; rotate by changing the
136
+ * selector and publishing a new TXT record.
176
137
  */
177
138
  selector?: string;
178
139
  /**
@@ -191,7 +152,8 @@ interface TaistampHandlerConfig {
191
152
  * gains `Access-Control-Allow-Origin`; pre-flight
192
153
  * `OPTIONS` also carries `-Allow-Methods`,
193
154
  * `-Allow-Headers`, `-Expose-Headers`, and
194
- * `-Max-Age: 600` per spec §5.2; success
155
+ * `-Max-Age` (default 600s, see {@link corsMaxAge})
156
+ * per spec §5.2; success
195
157
  * `GET` / `HEAD` carry `-Expose-Headers` so browser
196
158
  * JS can read the `TAI-*` response headers. A
197
159
  * non-`'*'` value adds `Vary: Origin` so caches can
@@ -202,6 +164,15 @@ interface TaistampHandlerConfig {
202
164
  * `Allow: GET, HEAD, OPTIONS` per RFC 9110 §9.3.7.
203
165
  */
204
166
  cors?: false | string;
167
+ /**
168
+ * `Access-Control-Max-Age` for pre-flight `OPTIONS`
169
+ * responses, in seconds. Defaults to 600 (10 minutes,
170
+ * the spec §5.2 floor); a value below 600 clamps up to
171
+ * it so the pre-flight stays spec-compliant. Ignored
172
+ * when `cors` is `false`. Must be a non-negative
173
+ * integer.
174
+ */
175
+ corsMaxAge?: number;
205
176
  }
206
177
  /**
207
178
  * Build a handler for `/.well-known/taistamp`.
@@ -212,15 +183,18 @@ interface TaistampHandlerConfig {
212
183
  * route handler.
213
184
  *
214
185
  * @throws TypeError if `signer` and `selector` are not
215
- * both set or both unset, or if `selector` does not
216
- * match `[A-Za-z][A-Za-z0-9_-]{0,62}`.
186
+ * both set or both unset, if `selector` does not match
187
+ * `[A-Za-z]([A-Za-z0-9_-]{0,61}[A-Za-z0-9])?`, or if
188
+ * `corsMaxAge` is not a non-negative integer.
217
189
  *
218
190
  * @remarks
219
191
  * Behaviour:
220
192
  *
221
193
  * - `GET` / `HEAD` — body is a fresh 25-byte TAI64N
222
194
  * label (`HEAD` omits the body). Response headers:
223
- * Content-Type `application/tai64n`, Content-Length
195
+ * Content-Type `application/tai64n`, Content-Disposition
196
+ * `inline` (so a browser renders the label in place
197
+ * rather than offering it as a download), Content-Length
224
198
  * `25`, Cache-Control `no-store`, plus
225
199
  * `TAI-Leap-Seconds` carrying the current count.
226
200
  * - `OPTIONS` — `200` with `Allow: GET, HEAD, OPTIONS`.
@@ -259,16 +233,25 @@ interface TaistampHandlerConfig {
259
233
  * TAI64N format
260
234
  */
261
235
  declare const newTaistampHandler: (config?: TaistampHandlerConfig) => ((request: Request) => Promise<Response>);
262
- type timestamp = {
263
- nano: number;
264
- sec: number;
265
- offset?: number;
266
- };
267
- declare const fromUTC: (utc: number) => timestamp;
268
- declare const now: () => timestamp;
269
- declare const tai64nLabel: (value?: timestamp) => string;
270
- declare const tai64nLabelFromUTC: (utc: number) => string;
236
+ /**
237
+ * Decode a `TAI-Signature` wire value into the raw
238
+ * Ed25519 signature bytes. Returns `undefined` when
239
+ * `value` fails sf-binary syntax (RFC 9651 §3.3.5) or
240
+ * does not decode to exactly `SIGNATURE_BYTES`
241
+ * octets a malformed field is equivalent to a
242
+ * missing one. This validates form only; semantics
243
+ * involving other fields (nonce echo, selector,
244
+ * verification) stay with the caller.
245
+ */
246
+ declare const asSignature: (value: string) => Bytes | undefined;
247
+ /**
248
+ * Extract the raw Ed25519 signature from response
249
+ * headers. Returns `undefined` when the
250
+ * `TAI-Signature` field is missing or fails
251
+ * {@link asSignature} validation.
252
+ */
253
+ declare const extractSignature: (headers: Headers) => Bytes | undefined;
271
254
  /** Package version from package.json. */
272
255
  declare const VERSION: string;
273
- export { type LeapSeconds, type Nonce, type Signer, 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, TAISTAMP_PATH, TAI_LEAP_SECONDS, TAI_LEAP_SECONDS_MAX, type TaistampHandlerConfig, VERSION, asLeapSeconds, asNonce, composeSignaturePayload, extractLeapSeconds, fromUTC, newEd25519Signer, newTaistampHandler, now, readASCII, readLabel, tai64nLabel, tai64nLabelFromUTC };
256
+ export { type KeyConfig, type KeyRecord, type LeapSeconds, type Nonce, type Signer, 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, type TaistampHandlerConfig, VERSION, type Verifier, asLeapSeconds, asNonce, asSignature, composeSignaturePayload, extractLeapSeconds, extractNonce, extractSignature, newEd25519Signer, newNonce, newTaistampHandler, parseRecordToVerifier, parseSecretToKey, parseSecretsToKeys, readASCII, readLabel, tai64nLabelFromUTC, tai64nLabelToUTC };
274
257
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,14 +1,6 @@
1
- import { assertValidSelector, decodeASCII, decodeBase64, encodeBase64, newSigner as newEd25519Signer } from "@kagal/ed25519-secret";
2
- var version = "0.1.1";
3
- const TAISTAMP_PATH = "/.well-known/taistamp";
4
- const TAI64N_PATH = TAISTAMP_PATH;
5
- const TAI64N_CONTENT_TYPE = "application/tai64n";
6
- const TAI64N_CONTENT_LENGTH = 25;
7
- const TAI64N_HEADER_KEY_SELECTOR = "TAI-Key-Selector";
8
- const TAI64N_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
9
- const TAI64N_HEADER_NONCE = "TAI-Nonce";
10
- const TAI64N_HEADER_SIGNATURE = "TAI-Signature";
11
- const TAI64_EPOCH_HI = 1073741824;
1
+ import { SF_BINARY_PATTERN, 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, tai64nLabel, tai64nLabelFromUTC, tai64nLabelToUTC } from "./_chunks/time.mjs";
2
+ import { assertValidSelector, atLeast, decodeASCII, getRandom, isInRange, newSigner as newEd25519Signer, parseRecordToVerifier, parseSecretToKey, parseSecretsToKeys } from "@kagal/ed25519-secret";
3
+ var version = "0.2.0";
12
4
  const readASCII = async (response, context) => decodeASCII(new Uint8Array(await response.arrayBuffer()), context);
13
5
  const readLabel = async (response, context) => {
14
6
  const label = await readASCII(response, context);
@@ -19,88 +11,65 @@ const readLabel = async (response, context) => {
19
11
  return label;
20
12
  };
21
13
  const CORS_ALLOW_METHODS = "GET, HEAD";
22
- const CORS_ALLOW_HEADERS = TAI64N_HEADER_NONCE;
14
+ const CORS_ALLOW_HEADERS = TAISTAMP_HEADER_NONCE;
23
15
  const CORS_EXPOSE_HEADERS = [
24
- TAI64N_HEADER_LEAP_SECONDS,
25
- TAI64N_HEADER_NONCE,
26
- TAI64N_HEADER_KEY_SELECTOR,
27
- TAI64N_HEADER_SIGNATURE
16
+ TAISTAMP_HEADER_LEAP_SECONDS,
17
+ TAISTAMP_HEADER_NONCE,
18
+ TAISTAMP_HEADER_KEY_SELECTOR,
19
+ TAISTAMP_HEADER_SIGNATURE
28
20
  ].join(", ");
29
- const CORS_MAX_AGE = "600";
30
- const buildCORSHeaders = (cors) => {
21
+ const CORS_MAX_AGE_MIN = 600;
22
+ const buildCORSHeaders = (cors, corsMaxAge) => {
31
23
  if (cors === false) return {
32
24
  error: {},
33
25
  preflight: {},
34
26
  response: {}
35
27
  };
36
28
  const origin = cors || "*";
37
- const vary = origin === "*" ? {} : { vary: "Origin" };
29
+ const maxAge = atLeast(CORS_MAX_AGE_MIN, corsMaxAge);
30
+ const vary = origin === "*" ? {} : { Vary: "Origin" };
38
31
  return {
39
32
  error: {
40
- "access-control-allow-origin": origin,
33
+ "Access-Control-Allow-Origin": origin,
41
34
  ...vary
42
35
  },
43
36
  preflight: {
44
- "access-control-allow-origin": origin,
45
- "access-control-allow-methods": CORS_ALLOW_METHODS,
46
- "access-control-allow-headers": CORS_ALLOW_HEADERS,
47
- "access-control-expose-headers": CORS_EXPOSE_HEADERS,
48
- "access-control-max-age": CORS_MAX_AGE,
37
+ "Access-Control-Allow-Origin": origin,
38
+ "Access-Control-Allow-Methods": CORS_ALLOW_METHODS,
39
+ "Access-Control-Allow-Headers": CORS_ALLOW_HEADERS,
40
+ "Access-Control-Expose-Headers": CORS_EXPOSE_HEADERS,
41
+ "Access-Control-Max-Age": String(maxAge),
49
42
  ...vary
50
43
  },
51
44
  response: {
52
- "access-control-allow-origin": origin,
53
- "access-control-expose-headers": CORS_EXPOSE_HEADERS,
45
+ "Access-Control-Allow-Origin": origin,
46
+ "Access-Control-Expose-Headers": CORS_EXPOSE_HEADERS,
54
47
  ...vary
55
48
  }
56
49
  };
57
50
  };
58
- const TAI_LEAP_SECONDS_MAX = 4294967295;
59
- const asLeapSeconds = (value) => {
60
- if (!Number.isInteger(value) || value < 0 || value > 4294967295) return void 0;
61
- return value;
62
- };
63
- const TAI_LEAP_SECONDS = 37;
64
- const DECIMAL_INTEGER = /^(?:0|[1-9]\d*)$/;
65
- const extractLeapSeconds = (headers) => {
66
- const raw = headers.get(TAI64N_HEADER_LEAP_SECONDS);
67
- if (!raw || !DECIMAL_INTEGER.test(raw)) return void 0;
68
- return asLeapSeconds(Number(raw));
69
- };
70
- const SF_BINARY_PATTERN = /^:(?:[\d+/A-Za-z]{4})*(?:[\d+/A-Za-z]{4}|[\d+/A-Za-z]{3}=|[\d+/A-Za-z]{2}==):$/;
71
51
  const asNonce = (value) => {
72
52
  if (!value || value.length < 14 || value.length > 174 || !SF_BINARY_PATTERN.test(value)) return void 0;
73
53
  return value;
74
54
  };
75
55
  const extractNonce = (headers) => {
76
- const value = headers.get(TAI64N_HEADER_NONCE);
56
+ const value = headers.get(TAISTAMP_HEADER_NONCE);
77
57
  return value === null ? void 0 : asNonce(value);
78
58
  };
79
- const fromUTC = (utc) => {
80
- return {
81
- sec: Math.floor(utc / 1e3) + 37,
82
- nano: utc % 1e3 * 1e6,
83
- offset: 37
84
- };
85
- };
86
- const now = () => {
87
- return fromUTC(Date.now());
88
- };
89
- const tai64nLabel = (value) => {
90
- const { sec, nano } = value ?? now();
91
- const secHi = Math.trunc(sec / u32Range) + TAI64_EPOCH_HI;
92
- const secLo = sec % u32Range;
93
- return `@${secHi.toString(16).padStart(8, "0")}${secLo.toString(16).padStart(8, "0")}${nano.toString(16).padStart(8, "0")}`;
59
+ const newNonce = (byteLength = 16, context = "newNonce") => {
60
+ if (!isInRange(byteLength, 7, 129)) {
61
+ const prefix = context ? `${context}: ` : "";
62
+ throw new TypeError(`${prefix}expected integer byte length within 7..129, got ${byteLength}`);
63
+ }
64
+ return encodeSFBinary(getRandom(byteLength, context));
94
65
  };
95
- const tai64nLabelFromUTC = (utc) => tai64nLabel(fromUTC(utc));
96
- const u32Range = 4294967296;
97
66
  const ALLOW_HEADER = "GET, HEAD, OPTIONS";
98
67
  const textEncoder = new TextEncoder();
99
68
  const DOMAIN_SEPARATOR = textEncoder.encode("taistamp-v1\0");
100
69
  const composeSignaturePayload = (label, leapSeconds, selector, nonce) => {
101
70
  const labelBytes = textEncoder.encode(label);
102
71
  const selectorBytes = textEncoder.encode(selector);
103
- const nonceBytes = decodeBase64(nonce.slice(1, -1));
72
+ const nonceBytes = decodeSFBinary(nonce, "composeSignaturePayload");
104
73
  const buffer = new ArrayBuffer(DOMAIN_SEPARATOR.length + labelBytes.length + 4 + 1 + selectorBytes.length + nonceBytes.length);
105
74
  const view = new Uint8Array(buffer);
106
75
  let offset = 0;
@@ -118,21 +87,22 @@ const composeSignaturePayload = (label, leapSeconds, selector, nonce) => {
118
87
  return buffer;
119
88
  };
120
89
  const validateHandlerConfig = (config) => {
121
- const { cors, selector, signer } = config;
90
+ const { cors, corsMaxAge, selector, signer } = config;
122
91
  if (signer === void 0 !== (selector === void 0)) throw new TypeError("newTaistampHandler: signer and selector must be set together");
123
92
  if (cors !== void 0 && cors !== false && typeof cors !== "string") throw new TypeError("newTaistampHandler: cors must be false or a string origin");
93
+ if (corsMaxAge !== void 0 && !isInRange(corsMaxAge, 0)) throw new TypeError("newTaistampHandler: corsMaxAge must be a non-negative integer");
124
94
  if (selector !== void 0) assertValidSelector(selector, "newTaistampHandler");
125
95
  return config;
126
96
  };
127
97
  const fromHandlerConfig = (config) => {
128
- const { cors, selector, signer } = validateHandlerConfig(config);
129
- const corsHeaders = buildCORSHeaders(cors);
98
+ const { cors, corsMaxAge, selector, signer } = validateHandlerConfig(config);
99
+ const corsHeaders = buildCORSHeaders(cors, corsMaxAge);
130
100
  return {
131
101
  addSignature: selector !== void 0 && signer !== void 0 ? async (headers, label, nonce) => {
132
102
  const payload = composeSignaturePayload(label, 37, selector, nonce);
133
103
  const signature = await signer.sign(payload);
134
- headers.set(TAI64N_HEADER_KEY_SELECTOR, selector);
135
- headers.set(TAI64N_HEADER_SIGNATURE, `:${encodeBase64(new Uint8Array(signature))}:`);
104
+ headers.set(TAISTAMP_HEADER_KEY_SELECTOR, selector);
105
+ headers.set(TAISTAMP_HEADER_SIGNATURE, encodeSFBinary(new Uint8Array(signature)));
136
106
  } : void 0,
137
107
  corsHeaders
138
108
  };
@@ -143,28 +113,29 @@ const newTaistampHandler = (config = {}) => {
143
113
  if (request.method === "OPTIONS") return new Response(void 0, {
144
114
  status: 200,
145
115
  headers: {
146
- allow: ALLOW_HEADER,
116
+ Allow: ALLOW_HEADER,
147
117
  ...corsHeaders.preflight
148
118
  }
149
119
  });
150
120
  if (request.method !== "GET" && request.method !== "HEAD") return new Response(void 0, {
151
121
  status: 405,
152
122
  headers: {
153
- allow: ALLOW_HEADER,
123
+ Allow: ALLOW_HEADER,
154
124
  ...corsHeaders.error
155
125
  }
156
126
  });
157
127
  const nonce = extractNonce(request.headers);
158
128
  const label = tai64nLabel();
159
129
  const headers = new Headers({
160
- "cache-control": "no-store",
161
- "content-length": String(25),
162
- "content-type": TAI64N_CONTENT_TYPE,
163
- [TAI64N_HEADER_LEAP_SECONDS]: String(37),
130
+ "Cache-Control": "no-store",
131
+ "Content-Disposition": "inline",
132
+ "Content-Length": String(25),
133
+ "Content-Type": TAISTAMP_CONTENT_TYPE,
134
+ [TAISTAMP_HEADER_LEAP_SECONDS]: String(37),
164
135
  ...corsHeaders.response
165
136
  });
166
137
  if (nonce && request.method === "GET") {
167
- headers.set(TAI64N_HEADER_NONCE, nonce);
138
+ headers.set(TAISTAMP_HEADER_NONCE, nonce);
168
139
  if (addSignature) await addSignature(headers, label, nonce);
169
140
  }
170
141
  const body = request.method === "HEAD" ? void 0 : label;
@@ -174,7 +145,16 @@ const newTaistampHandler = (config = {}) => {
174
145
  });
175
146
  };
176
147
  };
148
+ const asSignature = (value) => {
149
+ if (!SF_BINARY_PATTERN.test(value)) return void 0;
150
+ const bytes = decodeSFBinary(value);
151
+ return bytes.length === 64 ? bytes : void 0;
152
+ };
153
+ const extractSignature = (headers) => {
154
+ const value = headers.get(TAISTAMP_HEADER_SIGNATURE);
155
+ return value === null ? void 0 : asSignature(value);
156
+ };
177
157
  const VERSION = version;
178
- 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, TAISTAMP_PATH, TAI_LEAP_SECONDS, TAI_LEAP_SECONDS_MAX, VERSION, asLeapSeconds, asNonce, composeSignaturePayload, extractLeapSeconds, fromUTC, newEd25519Signer, newTaistampHandler, now, readASCII, readLabel, tai64nLabel, tai64nLabelFromUTC };
158
+ export { 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, VERSION, asLeapSeconds, asNonce, asSignature, composeSignaturePayload, extractLeapSeconds, extractNonce, extractSignature, newEd25519Signer, newNonce, newTaistampHandler, parseRecordToVerifier, parseSecretToKey, parseSecretsToKeys, readASCII, readLabel, tai64nLabelFromUTC, tai64nLabelToUTC };
179
159
 
180
160
  //# sourceMappingURL=index.mjs.map