@kagal/taistamp 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -53
- package/dist/_chunks/time.d.mts +144 -0
- package/dist/_chunks/time.mjs +68 -0
- package/dist/_chunks/time.mjs.map +1 -0
- package/dist/index.api.json +2094 -1546
- package/dist/index.d.mts +66 -83
- package/dist/index.d.ts +66 -83
- package/dist/index.mjs +52 -72
- package/dist/index.mjs.map +1 -1
- package/dist/utils.api.json +825 -0
- package/dist/utils.d.mts +29 -0
- package/dist/utils.d.ts +29 -0
- package/dist/utils.mjs +2 -0
- package/package.json +9 -5
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
-
* `
|
|
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}
|
|
100
|
-
* the brand prevents arbitrary
|
|
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,
|
|
172
|
-
* DNS label
|
|
173
|
-
*
|
|
174
|
-
* rotate by changing the
|
|
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
|
|
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,
|
|
216
|
-
*
|
|
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-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
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 {
|
|
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
|
-
* `
|
|
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}
|
|
100
|
-
* the brand prevents arbitrary
|
|
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,
|
|
172
|
-
* DNS label
|
|
173
|
-
*
|
|
174
|
-
* rotate by changing the
|
|
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
|
|
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,
|
|
216
|
-
*
|
|
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-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
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 =
|
|
14
|
+
const CORS_ALLOW_HEADERS = TAISTAMP_HEADER_NONCE;
|
|
23
15
|
const CORS_EXPOSE_HEADERS = [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
TAISTAMP_HEADER_LEAP_SECONDS,
|
|
17
|
+
TAISTAMP_HEADER_NONCE,
|
|
18
|
+
TAISTAMP_HEADER_KEY_SELECTOR,
|
|
19
|
+
TAISTAMP_HEADER_SIGNATURE
|
|
28
20
|
].join(", ");
|
|
29
|
-
const
|
|
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
|
|
29
|
+
const maxAge = atLeast(CORS_MAX_AGE_MIN, corsMaxAge);
|
|
30
|
+
const vary = origin === "*" ? {} : { Vary: "Origin" };
|
|
38
31
|
return {
|
|
39
32
|
error: {
|
|
40
|
-
"
|
|
33
|
+
"Access-Control-Allow-Origin": origin,
|
|
41
34
|
...vary
|
|
42
35
|
},
|
|
43
36
|
preflight: {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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
|
-
"
|
|
53
|
-
"
|
|
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(
|
|
56
|
+
const value = headers.get(TAISTAMP_HEADER_NONCE);
|
|
77
57
|
return value === null ? void 0 : asNonce(value);
|
|
78
58
|
};
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 =
|
|
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(
|
|
135
|
-
headers.set(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
|
|
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(
|
|
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 {
|
|
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
|