@kagal/taistamp 0.0.4 → 0.0.5
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 +4 -4
- package/dist/index.d.mts +50 -56
- package/dist/index.mjs +131 -167
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -15
- package/dist/index.d.ts +0 -244
package/README.md
CHANGED
|
@@ -15,14 +15,14 @@ pnpm add @kagal/taistamp
|
|
|
15
15
|
## Handler
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
|
-
import { newTaistampHandler,
|
|
18
|
+
import { newTaistampHandler, TAISTAMP_PATH } from '@kagal/taistamp';
|
|
19
19
|
|
|
20
20
|
const taistamp = newTaistampHandler();
|
|
21
21
|
|
|
22
22
|
// Worker fetch handler
|
|
23
23
|
export default {
|
|
24
24
|
async fetch(request: Request): Promise<Response> {
|
|
25
|
-
if (new URL(request.url).pathname ===
|
|
25
|
+
if (new URL(request.url).pathname === TAISTAMP_PATH) {
|
|
26
26
|
return taistamp(request);
|
|
27
27
|
}
|
|
28
28
|
// ...
|
|
@@ -30,7 +30,7 @@ export default {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
// Hono route
|
|
33
|
-
app.get(
|
|
33
|
+
app.get(TAISTAMP_PATH, (c) => taistamp(c.req.raw));
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`newTaistampHandler()` returns an
|
|
@@ -305,7 +305,7 @@ history.
|
|
|
305
305
|
|
|
306
306
|
| Name | Value |
|
|
307
307
|
|------|-------|
|
|
308
|
-
| `
|
|
308
|
+
| `TAISTAMP_PATH` | `/.well-known/taistamp` |
|
|
309
309
|
| `TAI64N_CONTENT_TYPE` | `application/tai64n` |
|
|
310
310
|
| `TAI64N_CONTENT_LENGTH` | `25` |
|
|
311
311
|
| `TAI64N_HEADER_KEY_SELECTOR` | `TAI-Key-Selector` |
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Signer } from
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Signer, Signer as Signer$1, newSigner as newEd25519Signer } from "@kagal/ed25519-secret";
|
|
2
|
+
declare const TAISTAMP_PATH = "/.well-known/taistamp";
|
|
3
|
+
/** @deprecated Renamed to {@link TAISTAMP_PATH}. */
|
|
4
4
|
declare const TAI64N_PATH = "/.well-known/taistamp";
|
|
5
5
|
declare const TAI64N_CONTENT_TYPE = "application/tai64n";
|
|
6
6
|
declare const TAI64N_CONTENT_LENGTH: number;
|
|
@@ -9,7 +9,6 @@ declare const TAI64N_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
|
|
|
9
9
|
declare const TAI64N_HEADER_NONCE = "TAI-Nonce";
|
|
10
10
|
declare const TAI64N_HEADER_SIGNATURE = "TAI-Signature";
|
|
11
11
|
declare const TAI64_EPOCH_HI = 1073741824;
|
|
12
|
-
|
|
13
12
|
/**
|
|
14
13
|
* Upper bound for `leapSeconds` in the taistamp signed
|
|
15
14
|
* payload. The framing encodes the value as a 4-byte
|
|
@@ -29,7 +28,7 @@ declare const LeapSecondsBrand: unique symbol;
|
|
|
29
28
|
* the signing path.
|
|
30
29
|
*/
|
|
31
30
|
type LeapSeconds = number & {
|
|
32
|
-
|
|
31
|
+
readonly [LeapSecondsBrand]: never;
|
|
33
32
|
};
|
|
34
33
|
/**
|
|
35
34
|
* Coerce a `number` to a {@link LeapSeconds}. Returns
|
|
@@ -59,7 +58,6 @@ declare const TAI_LEAP_SECONDS: LeapSeconds;
|
|
|
59
58
|
* collapsed into one verdict.
|
|
60
59
|
*/
|
|
61
60
|
declare const extractLeapSeconds: (headers: Headers) => LeapSeconds | undefined;
|
|
62
|
-
|
|
63
61
|
declare const NonceBrand: unique symbol;
|
|
64
62
|
/**
|
|
65
63
|
* `string` that has been confirmed to satisfy the
|
|
@@ -71,7 +69,7 @@ declare const NonceBrand: unique symbol;
|
|
|
71
69
|
* signing path.
|
|
72
70
|
*/
|
|
73
71
|
type Nonce = string & {
|
|
74
|
-
|
|
72
|
+
readonly [NonceBrand]: never;
|
|
75
73
|
};
|
|
76
74
|
/**
|
|
77
75
|
* Brand `value` as a {@link Nonce} when it satisfies
|
|
@@ -82,7 +80,6 @@ type Nonce = string & {
|
|
|
82
80
|
* verdict.
|
|
83
81
|
*/
|
|
84
82
|
declare const asNonce: (value: string) => Nonce | undefined;
|
|
85
|
-
|
|
86
83
|
/**
|
|
87
84
|
* Compose the byte sequence covered by a TAI-Signature.
|
|
88
85
|
*
|
|
@@ -129,46 +126,46 @@ declare const composeSignaturePayload: (label: string, leapSeconds: LeapSeconds,
|
|
|
129
126
|
* selector without a signer is a misconfiguration.
|
|
130
127
|
*/
|
|
131
128
|
interface TaistampHandlerConfig {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Key selector advertised in the `TAI-Key-Selector`
|
|
131
|
+
* response header and bound into the signed payload.
|
|
132
|
+
* Verifiers look up the public key at
|
|
133
|
+
* `<selector>._taistamp.<host>` in DNS.
|
|
134
|
+
*
|
|
135
|
+
* Must match `[A-Za-z][A-Za-z0-9_-]{0,62}` (a single
|
|
136
|
+
* DNS label starting with a letter, using
|
|
137
|
+
* DKIM-compatible characters and a valid sf-token);
|
|
138
|
+
* rotate by changing the selector and publishing a
|
|
139
|
+
* new TXT record.
|
|
140
|
+
*/
|
|
141
|
+
selector?: string;
|
|
142
|
+
/**
|
|
143
|
+
* {@link Signer} that produces `TAI-Signature` over
|
|
144
|
+
* the framed payload from {@link composeSignaturePayload}.
|
|
145
|
+
* Without a signer the nonce is still echoed but the
|
|
146
|
+
* response is unsigned.
|
|
147
|
+
*/
|
|
148
|
+
signer?: Signer$1;
|
|
149
|
+
/**
|
|
150
|
+
* CORS origin policy. Defaults to `'*'`; pass `false`
|
|
151
|
+
* to disable CORS entirely, or a specific origin
|
|
152
|
+
* (e.g. `'https://example.com'`) to scope the policy.
|
|
153
|
+
*
|
|
154
|
+
* Every response (`GET` / `HEAD` / `OPTIONS` / `405`)
|
|
155
|
+
* gains `Access-Control-Allow-Origin`; pre-flight
|
|
156
|
+
* `OPTIONS` also carries `-Allow-Methods`,
|
|
157
|
+
* `-Allow-Headers`, `-Expose-Headers`, and
|
|
158
|
+
* `-Max-Age: 600` per spec §4.2; success
|
|
159
|
+
* `GET` / `HEAD` carry `-Expose-Headers` so browser
|
|
160
|
+
* JS can read the `TAI-*` response headers. A
|
|
161
|
+
* non-`'*'` value adds `Vary: Origin` so caches can
|
|
162
|
+
* keep per-origin variants distinct.
|
|
163
|
+
*
|
|
164
|
+
* Disabling CORS does not affect method discovery:
|
|
165
|
+
* `OPTIONS` is still answered with `200` and
|
|
166
|
+
* `Allow: GET, HEAD, OPTIONS` per RFC 9110 §9.3.7.
|
|
167
|
+
*/
|
|
168
|
+
cors?: false | string;
|
|
172
169
|
}
|
|
173
170
|
/**
|
|
174
171
|
* Build a handler for `/.well-known/taistamp`.
|
|
@@ -226,19 +223,16 @@ interface TaistampHandlerConfig {
|
|
|
226
223
|
* TAI64N format
|
|
227
224
|
*/
|
|
228
225
|
declare const newTaistampHandler: (config?: TaistampHandlerConfig) => ((request: Request) => Promise<Response>);
|
|
229
|
-
|
|
230
226
|
type timestamp = {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
nano: number;
|
|
228
|
+
sec: number;
|
|
229
|
+
offset?: number;
|
|
234
230
|
};
|
|
235
231
|
declare const fromUTC: (utc: number) => timestamp;
|
|
236
232
|
declare const now: () => timestamp;
|
|
237
233
|
declare const tai64nLabel: (value?: timestamp) => string;
|
|
238
234
|
declare const tai64nLabelFromUTC: (utc: number) => string;
|
|
239
|
-
|
|
240
235
|
/** Package version from package.json. */
|
|
241
236
|
declare const VERSION: string;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
export type { LeapSeconds, Nonce, TaistampHandlerConfig };
|
|
237
|
+
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, tai64nLabel, tai64nLabelFromUTC };
|
|
238
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,216 +1,180 @@
|
|
|
1
|
-
import { assertValidSelector } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const pkg = {
|
|
6
|
-
version: version};
|
|
7
|
-
|
|
8
|
-
const TAI64N_PATH = "/.well-known/taistamp";
|
|
1
|
+
import { assertValidSelector, newSigner as newEd25519Signer } from "@kagal/ed25519-secret";
|
|
2
|
+
var version = "0.0.5";
|
|
3
|
+
const TAISTAMP_PATH = "/.well-known/taistamp";
|
|
4
|
+
const TAI64N_PATH = TAISTAMP_PATH;
|
|
9
5
|
const TAI64N_CONTENT_TYPE = "application/tai64n";
|
|
10
|
-
const TAI64N_CONTENT_LENGTH =
|
|
6
|
+
const TAI64N_CONTENT_LENGTH = 25;
|
|
11
7
|
const TAI64N_HEADER_KEY_SELECTOR = "TAI-Key-Selector";
|
|
12
8
|
const TAI64N_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
|
|
13
9
|
const TAI64N_HEADER_NONCE = "TAI-Nonce";
|
|
14
10
|
const TAI64N_HEADER_SIGNATURE = "TAI-Signature";
|
|
15
11
|
const TAI64_EPOCH_HI = 1073741824;
|
|
16
|
-
|
|
17
12
|
const CORS_ALLOW_METHODS = "GET, HEAD";
|
|
18
13
|
const CORS_ALLOW_HEADERS = TAI64N_HEADER_NONCE;
|
|
19
14
|
const CORS_EXPOSE_HEADERS = [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
TAI64N_HEADER_LEAP_SECONDS,
|
|
16
|
+
TAI64N_HEADER_NONCE,
|
|
17
|
+
TAI64N_HEADER_KEY_SELECTOR,
|
|
18
|
+
TAI64N_HEADER_SIGNATURE
|
|
24
19
|
].join(", ");
|
|
25
20
|
const CORS_MAX_AGE = "600";
|
|
26
21
|
const buildCORSHeaders = (cors) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
22
|
+
if (cors === false) return {
|
|
23
|
+
error: {},
|
|
24
|
+
preflight: {},
|
|
25
|
+
response: {}
|
|
26
|
+
};
|
|
27
|
+
const origin = cors || "*";
|
|
28
|
+
const vary = origin === "*" ? {} : { vary: "Origin" };
|
|
29
|
+
return {
|
|
30
|
+
error: {
|
|
31
|
+
"access-control-allow-origin": origin,
|
|
32
|
+
...vary
|
|
33
|
+
},
|
|
34
|
+
preflight: {
|
|
35
|
+
"access-control-allow-origin": origin,
|
|
36
|
+
"access-control-allow-methods": CORS_ALLOW_METHODS,
|
|
37
|
+
"access-control-allow-headers": CORS_ALLOW_HEADERS,
|
|
38
|
+
"access-control-expose-headers": CORS_EXPOSE_HEADERS,
|
|
39
|
+
"access-control-max-age": CORS_MAX_AGE,
|
|
40
|
+
...vary
|
|
41
|
+
},
|
|
42
|
+
response: {
|
|
43
|
+
"access-control-allow-origin": origin,
|
|
44
|
+
"access-control-expose-headers": CORS_EXPOSE_HEADERS,
|
|
45
|
+
...vary
|
|
46
|
+
}
|
|
47
|
+
};
|
|
51
48
|
};
|
|
52
|
-
|
|
53
49
|
const TAI_LEAP_SECONDS_MAX = 4294967295;
|
|
54
50
|
const asLeapSeconds = (value) => {
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
if (!Number.isInteger(value) || value < 0 || value > 4294967295) return void 0;
|
|
52
|
+
return value;
|
|
57
53
|
};
|
|
58
54
|
const TAI_LEAP_SECONDS = 37;
|
|
59
55
|
const DECIMAL_INTEGER = /^(?:0|[1-9]\d*)$/;
|
|
60
56
|
const extractLeapSeconds = (headers) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
const raw = headers.get(TAI64N_HEADER_LEAP_SECONDS);
|
|
58
|
+
if (!raw || !DECIMAL_INTEGER.test(raw)) return void 0;
|
|
59
|
+
return asLeapSeconds(Number(raw));
|
|
64
60
|
};
|
|
65
|
-
|
|
66
|
-
const NONCE_MIN_OCTETS = 14;
|
|
67
|
-
const NONCE_MAX_OCTETS = 174;
|
|
68
61
|
const SF_BINARY_PATTERN = /^:(?:[\d+/A-Za-z]{4})*(?:[\d+/A-Za-z]{4}|[\d+/A-Za-z]{3}=|[\d+/A-Za-z]{2}==):$/;
|
|
69
62
|
const asNonce = (value) => {
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
if (!value || value.length < 14 || value.length > 174 || !SF_BINARY_PATTERN.test(value)) return void 0;
|
|
64
|
+
return value;
|
|
72
65
|
};
|
|
73
66
|
const extractNonce = (headers) => {
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
const value = headers.get(TAI64N_HEADER_NONCE);
|
|
68
|
+
return value === null ? void 0 : asNonce(value);
|
|
76
69
|
};
|
|
77
|
-
|
|
78
70
|
const fromUTC = (utc) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
return {
|
|
72
|
+
sec: Math.floor(utc / 1e3) + 37,
|
|
73
|
+
nano: utc % 1e3 * 1e6,
|
|
74
|
+
offset: 37
|
|
75
|
+
};
|
|
82
76
|
};
|
|
83
77
|
const now = () => {
|
|
84
|
-
|
|
85
|
-
return fromUTC(utc);
|
|
78
|
+
return fromUTC(Date.now());
|
|
86
79
|
};
|
|
87
80
|
const tai64nLabel = (value) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const secLoHex = secLo.toString(16).padStart(8, "0");
|
|
93
|
-
const nanoHex = nano.toString(16).padStart(8, "0");
|
|
94
|
-
return `@${secHiHex}${secLoHex}${nanoHex}`;
|
|
81
|
+
const { sec, nano } = value ?? now();
|
|
82
|
+
const secHi = Math.trunc(sec / u32Range) + TAI64_EPOCH_HI;
|
|
83
|
+
const secLo = sec % u32Range;
|
|
84
|
+
return `@${secHi.toString(16).padStart(8, "0")}${secLo.toString(16).padStart(8, "0")}${nano.toString(16).padStart(8, "0")}`;
|
|
95
85
|
};
|
|
96
86
|
const tai64nLabelFromUTC = (utc) => tai64nLabel(fromUTC(utc));
|
|
97
87
|
const u32Range = 4294967296;
|
|
98
|
-
|
|
99
88
|
const ALLOW_HEADER = "GET, HEAD, OPTIONS";
|
|
100
89
|
const textEncoder = new TextEncoder();
|
|
101
90
|
const DOMAIN_SEPARATOR = textEncoder.encode("taistamp-v1\0");
|
|
102
91
|
const asBytes = (source) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (ArrayBuffer.isView(source)) {
|
|
107
|
-
return new Uint8Array(
|
|
108
|
-
source.buffer,
|
|
109
|
-
source.byteOffset,
|
|
110
|
-
source.byteLength
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
return new Uint8Array(source);
|
|
92
|
+
if (source instanceof Uint8Array) return source;
|
|
93
|
+
if (ArrayBuffer.isView(source)) return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
|
94
|
+
return new Uint8Array(source);
|
|
114
95
|
};
|
|
115
96
|
const encodeStructuredBinary = (source) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return `:${standard}:`;
|
|
97
|
+
const bytes = asBytes(source);
|
|
98
|
+
return `:${btoa(String.fromCodePoint(...bytes))}:`;
|
|
119
99
|
};
|
|
120
100
|
const composeSignaturePayload = (label, leapSeconds, selector, nonce) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
view.set(nonceBytes, offset);
|
|
140
|
-
return buffer;
|
|
101
|
+
const labelBytes = textEncoder.encode(label);
|
|
102
|
+
const selectorBytes = textEncoder.encode(selector);
|
|
103
|
+
const nonceBytes = textEncoder.encode(nonce);
|
|
104
|
+
const buffer = new ArrayBuffer(DOMAIN_SEPARATOR.length + labelBytes.length + 4 + 1 + selectorBytes.length + nonceBytes.length);
|
|
105
|
+
const view = new Uint8Array(buffer);
|
|
106
|
+
let offset = 0;
|
|
107
|
+
view.set(DOMAIN_SEPARATOR, offset);
|
|
108
|
+
offset += DOMAIN_SEPARATOR.length;
|
|
109
|
+
view.set(labelBytes, offset);
|
|
110
|
+
offset += labelBytes.length;
|
|
111
|
+
new DataView(buffer).setUint32(offset, leapSeconds, false);
|
|
112
|
+
offset += 4;
|
|
113
|
+
view[offset] = selectorBytes.length;
|
|
114
|
+
offset += 1;
|
|
115
|
+
view.set(selectorBytes, offset);
|
|
116
|
+
offset += selectorBytes.length;
|
|
117
|
+
view.set(nonceBytes, offset);
|
|
118
|
+
return buffer;
|
|
141
119
|
};
|
|
142
120
|
const validateHandlerConfig = (config) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
if (cors !== void 0 && cors !== false && typeof cors !== "string") {
|
|
150
|
-
throw new TypeError(
|
|
151
|
-
"newTaistampHandler: cors must be false or a string origin"
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
if (selector !== void 0) {
|
|
155
|
-
assertValidSelector(selector, "newTaistampHandler");
|
|
156
|
-
}
|
|
157
|
-
return config;
|
|
121
|
+
const { cors, selector, signer } = config;
|
|
122
|
+
if (signer === void 0 !== (selector === void 0)) throw new TypeError("newTaistampHandler: signer and selector must be set together");
|
|
123
|
+
if (cors !== void 0 && cors !== false && typeof cors !== "string") throw new TypeError("newTaistampHandler: cors must be false or a string origin");
|
|
124
|
+
if (selector !== void 0) assertValidSelector(selector, "newTaistampHandler");
|
|
125
|
+
return config;
|
|
158
126
|
};
|
|
159
127
|
const fromHandlerConfig = (config) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
headers.set(
|
|
172
|
-
TAI64N_HEADER_SIGNATURE,
|
|
173
|
-
encodeStructuredBinary(signature)
|
|
174
|
-
);
|
|
175
|
-
} : void 0;
|
|
176
|
-
return { addSignature, corsHeaders };
|
|
128
|
+
const { cors, selector, signer } = validateHandlerConfig(config);
|
|
129
|
+
const corsHeaders = buildCORSHeaders(cors);
|
|
130
|
+
return {
|
|
131
|
+
addSignature: selector !== void 0 && signer !== void 0 ? async (headers, label, nonce) => {
|
|
132
|
+
const payload = composeSignaturePayload(label, 37, selector, nonce);
|
|
133
|
+
const signature = await signer.sign(payload);
|
|
134
|
+
headers.set(TAI64N_HEADER_KEY_SELECTOR, selector);
|
|
135
|
+
headers.set(TAI64N_HEADER_SIGNATURE, encodeStructuredBinary(signature));
|
|
136
|
+
} : void 0,
|
|
137
|
+
corsHeaders
|
|
138
|
+
};
|
|
177
139
|
};
|
|
178
140
|
const newTaistampHandler = (config = {}) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
141
|
+
const { addSignature, corsHeaders } = fromHandlerConfig(config);
|
|
142
|
+
return async (request) => {
|
|
143
|
+
if (request.method === "OPTIONS") return new Response(void 0, {
|
|
144
|
+
status: 200,
|
|
145
|
+
headers: {
|
|
146
|
+
allow: ALLOW_HEADER,
|
|
147
|
+
...corsHeaders.preflight
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
if (request.method !== "GET" && request.method !== "HEAD") return new Response(void 0, {
|
|
151
|
+
status: 405,
|
|
152
|
+
headers: {
|
|
153
|
+
allow: ALLOW_HEADER,
|
|
154
|
+
...corsHeaders.error
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
const nonce = extractNonce(request.headers);
|
|
158
|
+
const label = tai64nLabel();
|
|
159
|
+
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),
|
|
164
|
+
...corsHeaders.response
|
|
165
|
+
});
|
|
166
|
+
if (nonce && request.method === "GET") {
|
|
167
|
+
headers.set(TAI64N_HEADER_NONCE, nonce);
|
|
168
|
+
if (addSignature) await addSignature(headers, label, nonce);
|
|
169
|
+
}
|
|
170
|
+
const body = request.method === "HEAD" ? void 0 : label;
|
|
171
|
+
return new Response(body, {
|
|
172
|
+
status: 200,
|
|
173
|
+
headers
|
|
174
|
+
});
|
|
175
|
+
};
|
|
211
176
|
};
|
|
177
|
+
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, tai64nLabel, tai64nLabelFromUTC };
|
|
212
179
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
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, newTaistampHandler, now, tai64nLabel, tai64nLabelFromUTC };
|
|
216
|
-
//# sourceMappingURL=index.mjs.map
|
|
180
|
+
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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/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// Spec §4.2 SHOULDs at least 600s; 10 minutes is the\n// floor the spec example uses and keeps high-traffic\n// cross-origin clients off a pre-flight per fetch.\nconst CORS_MAX_AGE = '600';\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 'access-control-max-age': CORS_MAX_AGE,\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 { assertValidSelector, type Signer } from '@kagal/ed25519-secret';\n\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 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`, `-Expose-Headers`, and\n * `-Max-Age: 600` per spec §4.2; 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) {\n assertValidSelector(selector, 'newTaistampHandler');\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` — on `GET`, the value is echoed\n * verbatim in the response. A missing, empty,\n * duplicated, 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}. `HEAD`, `OPTIONS`, and `405`\n * responses never carry `TAI-Nonce` per spec §4.1.\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 && request.method === 'GET') {\n headers.set(TAI64N_HEADER_NONCE, nonce);\n if (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","import pkg from '../package.json' with { type: 'json' };\n\n/** Package version from package.json. */\nexport const VERSION: string = pkg.version;\n\nexport {\n newSigner as newEd25519Signer,\n type Signer,\n} from '@kagal/ed25519-secret';\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 fromUTC,\n now,\n tai64nLabel,\n tai64nLabelFromUTC,\n} from './utils';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["pkg.version"],"sources":["../package.json","../src/const.ts","../src/cors.ts","../src/leap-seconds.ts","../src/nonce.ts","../src/utils.ts","../src/handler.ts","../src/index.ts"],"sourcesContent":["","export const TAISTAMP_PATH = '/.well-known/taistamp';\n\n/** @deprecated Renamed to {@link TAISTAMP_PATH}. */\nexport const TAI64N_PATH = TAISTAMP_PATH;\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// Spec §4.2 SHOULDs at least 600s; 10 minutes is the\n// floor the spec example uses and keeps high-traffic\n// cross-origin clients off a pre-flight per fetch.\nconst CORS_MAX_AGE = '600';\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 'access-control-max-age': CORS_MAX_AGE,\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 { assertValidSelector, type Signer } from '@kagal/ed25519-secret';\n\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 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`, `-Expose-Headers`, and\n * `-Max-Age: 600` per spec §4.2; 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) {\n assertValidSelector(selector, 'newTaistampHandler');\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` — on `GET`, the value is echoed\n * verbatim in the response. A missing, empty,\n * duplicated, 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}. `HEAD`, `OPTIONS`, and `405`\n * responses never carry `TAI-Nonce` per spec §4.1.\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 && request.method === 'GET') {\n headers.set(TAI64N_HEADER_NONCE, nonce);\n if (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","import pkg from '../package.json' with { type: 'json' };\n\n/** Package version from package.json. */\nexport const VERSION: string = pkg.version;\n\nexport {\n newSigner as newEd25519Signer,\n type Signer,\n} from '@kagal/ed25519-secret';\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 fromUTC,\n now,\n tai64nLabel,\n tai64nLabelFromUTC,\n} from './utils';\n"],"mappings":";;ACAA,MAAa,gBAAgB;AAG7B,MAAa,cAAc;AAE3B,MAAa,sBAAsB;AACnC,MAAa,wBAAwB;AAErC,MAAa,6BAA6B;AAC1C,MAAa,6BAA6B;AAC1C,MAAa,sBAAsB;AACnC,MAAa,0BAA0B;AAEvC,MAAa,iBAAiB;ACD9B,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;CAC1B;CACA;CACA;CACA;AACF,EAAE,KAAK,IAAI;AAIX,MAAM,eAAe;;;;;;;;;;EA6BrB,OAAa;GAGX,+BACS;GAAE,GAAA;EAAW;EAAe,WAAW;GAAE,+BAAA;GAElD,gCAAuB;GACvB,gCACoB;GACpB,iCAAO;GACL,0BAAO;GACL,GAAA;;EAEF,UAAA;GACA,+BAAW;GACT,iCAA+B;GAC/B,GAAA;;;;MAMF,uBAAU;MAER,iBAAA,UAAA;KACA,CAAG,OAAA,UAAA,KAAA,KAAA,QAAA,KAAA,QAAA,YAAA,OAAA,KAAA;QACL;;;;;;;;;AC/CJ,MAAa,oBACX;MAOA,WAAO,UAAA;CACT,IAAA,CAAA,SAAA,MAAA,SAAA,MAAA,MAAA,SAAA,OAAA,CAAA,kBAAA,KAAA,KAAA,GAAA,OAAA,KAAA;;;;;;;;;;;EAcA,QAAa;;;;;;;;;CAUb,MAAM,QAAA,MAAA;;;;;;;AAUN,MAAa,mBAAA,YACX,OAC4B,eAAA;MAC5B,WAAY,WAAY;CACxB,IAAI,kBAAS,YAAqB,OAAM;CACxC,IAAA,YAAO,OAAc,MAAO,GAAI,OAAA,IAAA,WAAA,OAAA,QAAA,OAAA,YAAA,OAAA,UAAA;CAClC,OAAA,IAAA,WAAA,MAAA;;;;;;ACxEA,MAAa,2BAAmB,OAAA,aAAA,UAAA,UAAA;;;;;;CAOhC,IAAA,SAAa;;;;;;;;;;CAWb,UAAM,cAAA;;;;;;;;CAwBN,IAAA,aAAwB,KAAA,GAAA,oBAAqC,UAAA,oBAAA;CAC3D,OACG;;;;;;;GAaL,MAAa,UAAA,wBAAwD,OAAA,IAAA,UAAA,KAAA;GACnE,MAAM,YAAQ,MAAY,OAAA,KAAA,OAAmB;GAC7C,QAAO,IAAA,4BAAqC,QAAK;GACnD,QAAA,IAAA,yBAAA,uBAAA,SAAA,CAAA;;EC1DA;CAIE;;MAAoB,sBAAA,SAAA,CAAA,MAAA;CAAyB,MAAA,EAAA,cAAA,gBAAA,kBAAA,MAAA;CAC/C,OAAA,OAAA,YAAA;EAEA,IAAa,QAAA,WAAuB,WAAA,OAAA,IAAA,SAAA,KAAA,GAAA;GAElC,QAAO;GACT,SAAA;IAEA,OAAa;IACX,GAAM,YAAO;GAEb;EACA,CAAA;EAMA,IAAA,QAJiB,WAAM,SAAa,QAAS,WAC5B,QAAM,OAAW,IAAE,SAAY,KAGnB,GAFb;GAGlB,QAAA;GAEA,SAAa;IAEb,OAAM;;GCtBN;EAEA,CAAA;;;;;;;;GASA,GAAM,YAAA;EAEN,CAAA;EACE,IAAI,SAAA,QAAkB,WAAA,OACpB;GAEF,QAAI,IAAA,qBACF,KAAO;GAMT,IAAA,cAAW,MAAW,aAAM,SAAA,OAAA,KAAA;EAC9B;;;;;;;;MAcE,UADiB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kagal/taistamp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Signed TAI64N timestamps over HTTP",
|
|
6
6
|
"author": "Apptly Software Ltd <oss@apptly.co>",
|
|
@@ -31,37 +31,37 @@
|
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@kagal/ed25519-secret": "^0.1.
|
|
34
|
+
"@kagal/ed25519-secret": "^0.1.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@cloudflare/vitest-pool-workers": "^0.13.1",
|
|
38
|
-
"@cloudflare/workers-types": "^4.
|
|
39
|
-
"@kagal/
|
|
40
|
-
"@kagal/cross-test": "~0.1.3",
|
|
38
|
+
"@cloudflare/workers-types": "^4.20260514.1",
|
|
39
|
+
"@kagal/cross-test": "^0.1.3",
|
|
41
40
|
"@noble/ed25519": "^3.1.0",
|
|
42
|
-
"@poupe/eslint-config": "
|
|
43
|
-
"@types/node": "^20.19.
|
|
44
|
-
"@vitest/coverage-istanbul": "^4.1.
|
|
41
|
+
"@poupe/eslint-config": "^0.9.1",
|
|
42
|
+
"@types/node": "^20.19.41",
|
|
43
|
+
"@vitest/coverage-istanbul": "^4.1.6",
|
|
45
44
|
"eslint": "^9.39.4",
|
|
46
45
|
"npm-run-all2": "^8.0.4",
|
|
47
|
-
"
|
|
46
|
+
"obuild": "^0.4.34",
|
|
47
|
+
"publint": "^0.3.21",
|
|
48
48
|
"rimraf": "^6.1.3",
|
|
49
|
-
"typescript": "
|
|
50
|
-
"
|
|
51
|
-
"vitest": "^4.1.5"
|
|
49
|
+
"typescript": "^6.0.3",
|
|
50
|
+
"vitest": "^4.1.6"
|
|
52
51
|
},
|
|
53
52
|
"engines": {
|
|
54
53
|
"node": ">= 20.20.1",
|
|
55
54
|
"pnpm": ">= 10.33.2"
|
|
56
55
|
},
|
|
57
56
|
"publishConfig": {
|
|
58
|
-
"access": "public"
|
|
57
|
+
"access": "public",
|
|
58
|
+
"provenance": true
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
|
-
"build": "
|
|
61
|
+
"build": "obuild",
|
|
62
62
|
"clean": "rimraf dist node_modules",
|
|
63
63
|
"dev": "vitest",
|
|
64
|
-
"dev:prepare": "
|
|
64
|
+
"dev:prepare": "obuild --stub",
|
|
65
65
|
"lint": "eslint --fix .",
|
|
66
66
|
"lint:_pkg-pr-new": "eslint --fix package.json",
|
|
67
67
|
"lint:check": "eslint .",
|
package/dist/index.d.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { Signer } from '@kagal/ed25519-secret';
|
|
2
|
-
export { Signer, newSigner as newEd25519Signer } from '@kagal/ed25519-secret';
|
|
3
|
-
|
|
4
|
-
declare const TAI64N_PATH = "/.well-known/taistamp";
|
|
5
|
-
declare const TAI64N_CONTENT_TYPE = "application/tai64n";
|
|
6
|
-
declare const TAI64N_CONTENT_LENGTH: number;
|
|
7
|
-
declare const TAI64N_HEADER_KEY_SELECTOR = "TAI-Key-Selector";
|
|
8
|
-
declare const TAI64N_HEADER_LEAP_SECONDS = "TAI-Leap-Seconds";
|
|
9
|
-
declare const TAI64N_HEADER_NONCE = "TAI-Nonce";
|
|
10
|
-
declare const TAI64N_HEADER_SIGNATURE = "TAI-Signature";
|
|
11
|
-
declare const TAI64_EPOCH_HI = 1073741824;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Upper bound for `leapSeconds` in the taistamp signed
|
|
15
|
-
* payload. The framing encodes the value as a 4-byte
|
|
16
|
-
* big-endian unsigned integer, so any input outside
|
|
17
|
-
* `[0, 2^32-1]` cannot be represented. Verifiers MUST
|
|
18
|
-
* treat an out-of-range `TAI-Leap-Seconds` response
|
|
19
|
-
* header as unsigned, per spec §5.1.
|
|
20
|
-
*/
|
|
21
|
-
declare const TAI_LEAP_SECONDS_MAX = 4294967295;
|
|
22
|
-
declare const LeapSecondsBrand: unique symbol;
|
|
23
|
-
/**
|
|
24
|
-
* `number` that has been confirmed to fit the
|
|
25
|
-
* `[0, TAI_LEAP_SECONDS_MAX]` u32be range required by
|
|
26
|
-
* the taistamp signed-payload framing. Construct only
|
|
27
|
-
* via {@link extractLeapSeconds} or {@link asLeapSeconds};
|
|
28
|
-
* the brand prevents an arbitrary number from reaching
|
|
29
|
-
* the signing path.
|
|
30
|
-
*/
|
|
31
|
-
type LeapSeconds = number & {
|
|
32
|
-
readonly [LeapSecondsBrand]: never;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Coerce a `number` to a {@link LeapSeconds}. Returns
|
|
36
|
-
* `undefined` when `value` is non-integer, negative,
|
|
37
|
-
* or exceeds {@link TAI_LEAP_SECONDS_MAX}.
|
|
38
|
-
*/
|
|
39
|
-
declare const asLeapSeconds: (value: number) => LeapSeconds | undefined;
|
|
40
|
-
/**
|
|
41
|
-
* Current TAI − UTC offset in whole seconds, used by
|
|
42
|
-
* `fromUTC()` and emitted in the `TAI-Leap-Seconds`
|
|
43
|
-
* response header. The value 37 has been in force
|
|
44
|
-
* since 2017-01-01; update on the next IERS leap-second
|
|
45
|
-
* announcement.
|
|
46
|
-
*
|
|
47
|
-
* @remarks
|
|
48
|
-
* Stays a single `LeapSeconds` until a leap-seconds
|
|
49
|
-
* table is added so the offset can be computed for any
|
|
50
|
-
* TAI second; this constant becomes redundant then.
|
|
51
|
-
*/
|
|
52
|
-
declare const TAI_LEAP_SECONDS: LeapSeconds;
|
|
53
|
-
/**
|
|
54
|
-
* Extract a usable leap-seconds count from response
|
|
55
|
-
* headers. Returns `undefined` when the
|
|
56
|
-
* `TAI-Leap-Seconds` field is missing, empty,
|
|
57
|
-
* non-numeric, non-integer, negative, or out-of-range
|
|
58
|
-
* — every "treat as unsigned" case in spec §5.1
|
|
59
|
-
* collapsed into one verdict.
|
|
60
|
-
*/
|
|
61
|
-
declare const extractLeapSeconds: (headers: Headers) => LeapSeconds | undefined;
|
|
62
|
-
|
|
63
|
-
declare const NonceBrand: unique symbol;
|
|
64
|
-
/**
|
|
65
|
-
* `string` that has been confirmed to satisfy the
|
|
66
|
-
* sf-binary syntax of RFC 9651 §3.3.5 and the
|
|
67
|
-
* `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]` length range
|
|
68
|
-
* required by spec §5.2. Construct only via
|
|
69
|
-
* {@link asNonce} or {@link extractNonce}; the brand
|
|
70
|
-
* prevents arbitrary strings from reaching the
|
|
71
|
-
* signing path.
|
|
72
|
-
*/
|
|
73
|
-
type Nonce = string & {
|
|
74
|
-
readonly [NonceBrand]: never;
|
|
75
|
-
};
|
|
76
|
-
/**
|
|
77
|
-
* Brand `value` as a {@link Nonce} when it satisfies
|
|
78
|
-
* sf-binary syntax (RFC 9651 §3.3.5) and falls inside
|
|
79
|
-
* `[NONCE_MIN_OCTETS, NONCE_MAX_OCTETS]`. Returns
|
|
80
|
-
* `undefined` for anything else — every "treat as
|
|
81
|
-
* absent" case in spec §5.2 collapsed into one
|
|
82
|
-
* verdict.
|
|
83
|
-
*/
|
|
84
|
-
declare const asNonce: (value: string) => Nonce | undefined;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Compose the byte sequence covered by a TAI-Signature.
|
|
88
|
-
*
|
|
89
|
-
* @param label - the 25-byte TAI64N label string the
|
|
90
|
-
* server is returning
|
|
91
|
-
* @param leapSeconds - the leap-seconds count the server
|
|
92
|
-
* advertises in `TAI-Leap-Seconds`
|
|
93
|
-
* @param selector - the key selector the server
|
|
94
|
-
* advertises in `TAI-Key-Selector`; verifiers use
|
|
95
|
-
* this to look up the public key in DNS at
|
|
96
|
-
* `<selector>._taistamp.<host>`
|
|
97
|
-
* @param nonce - the client-supplied nonce, echoed
|
|
98
|
-
* verbatim in `TAI-Nonce`; brand a verifier-side
|
|
99
|
-
* string with {@link asNonce} before passing it in
|
|
100
|
-
* @returns the byte sequence verifiers reconstruct
|
|
101
|
-
* from the response and pass to their public-key
|
|
102
|
-
* verify routine. The framing is the
|
|
103
|
-
* domain-separation tag (`taistamp-v1` plus a
|
|
104
|
-
* trailing NUL byte), then the label bytes, then
|
|
105
|
-
* the leap-seconds count as a 4-byte big-endian
|
|
106
|
-
* unsigned integer, then a 1-byte selector length,
|
|
107
|
-
* then the selector bytes, then the nonce bytes.
|
|
108
|
-
*
|
|
109
|
-
* @remarks
|
|
110
|
-
* Binding the selector into the signed payload stops a
|
|
111
|
-
* downgrade attacker from rewriting `TAI-Key-Selector`
|
|
112
|
-
* to point at a compromised or weaker key — the
|
|
113
|
-
* signature would no longer verify under that key.
|
|
114
|
-
* `leapSeconds` is encoded as a 4-byte big-endian
|
|
115
|
-
* unsigned integer; the selector is length-prefixed by
|
|
116
|
-
* a single byte (selectors are ≤ 63 chars per
|
|
117
|
-
* {@link newTaistampHandler}'s validation).
|
|
118
|
-
*/
|
|
119
|
-
declare const composeSignaturePayload: (label: string, leapSeconds: LeapSeconds, selector: string, nonce: Nonce) => ArrayBuffer;
|
|
120
|
-
/**
|
|
121
|
-
* Configuration for {@link newTaistampHandler}.
|
|
122
|
-
*
|
|
123
|
-
* @remarks
|
|
124
|
-
* `signer` and `selector` are co-required: pass both
|
|
125
|
-
* to enable authenticated responses, or neither for
|
|
126
|
-
* an unsigned handler. Passing only one is rejected
|
|
127
|
-
* at construction time — without the selector
|
|
128
|
-
* verifiers cannot find the key in DNS, and a
|
|
129
|
-
* selector without a signer is a misconfiguration.
|
|
130
|
-
*/
|
|
131
|
-
interface TaistampHandlerConfig {
|
|
132
|
-
/**
|
|
133
|
-
* Key selector advertised in the `TAI-Key-Selector`
|
|
134
|
-
* response header and bound into the signed payload.
|
|
135
|
-
* Verifiers look up the public key at
|
|
136
|
-
* `<selector>._taistamp.<host>` in DNS.
|
|
137
|
-
*
|
|
138
|
-
* Must match `[A-Za-z][A-Za-z0-9_-]{0,62}` (a single
|
|
139
|
-
* DNS label starting with a letter, using
|
|
140
|
-
* DKIM-compatible characters and a valid sf-token);
|
|
141
|
-
* rotate by changing the selector and publishing a
|
|
142
|
-
* new TXT record.
|
|
143
|
-
*/
|
|
144
|
-
selector?: string;
|
|
145
|
-
/**
|
|
146
|
-
* {@link Signer} that produces `TAI-Signature` over
|
|
147
|
-
* the framed payload from {@link composeSignaturePayload}.
|
|
148
|
-
* Without a signer the nonce is still echoed but the
|
|
149
|
-
* response is unsigned.
|
|
150
|
-
*/
|
|
151
|
-
signer?: Signer;
|
|
152
|
-
/**
|
|
153
|
-
* CORS origin policy. Defaults to `'*'`; pass `false`
|
|
154
|
-
* to disable CORS entirely, or a specific origin
|
|
155
|
-
* (e.g. `'https://example.com'`) to scope the policy.
|
|
156
|
-
*
|
|
157
|
-
* Every response (`GET` / `HEAD` / `OPTIONS` / `405`)
|
|
158
|
-
* gains `Access-Control-Allow-Origin`; pre-flight
|
|
159
|
-
* `OPTIONS` also carries `-Allow-Methods`,
|
|
160
|
-
* `-Allow-Headers`, `-Expose-Headers`, and
|
|
161
|
-
* `-Max-Age: 600` per spec §4.2; success
|
|
162
|
-
* `GET` / `HEAD` carry `-Expose-Headers` so browser
|
|
163
|
-
* JS can read the `TAI-*` response headers. A
|
|
164
|
-
* non-`'*'` value adds `Vary: Origin` so caches can
|
|
165
|
-
* keep per-origin variants distinct.
|
|
166
|
-
*
|
|
167
|
-
* Disabling CORS does not affect method discovery:
|
|
168
|
-
* `OPTIONS` is still answered with `200` and
|
|
169
|
-
* `Allow: GET, HEAD, OPTIONS` per RFC 9110 §9.3.7.
|
|
170
|
-
*/
|
|
171
|
-
cors?: false | string;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Build a handler for `/.well-known/taistamp`.
|
|
175
|
-
*
|
|
176
|
-
* @param config - optional {@link TaistampHandlerConfig}
|
|
177
|
-
* @returns an `async (request) => Response` callable
|
|
178
|
-
* directly as a Web `fetch` handler or as a Hono
|
|
179
|
-
* route handler.
|
|
180
|
-
*
|
|
181
|
-
* @throws TypeError if `signer` and `selector` are not
|
|
182
|
-
* both set or both unset, or if `selector` does not
|
|
183
|
-
* match `[A-Za-z][A-Za-z0-9_-]{0,62}`.
|
|
184
|
-
*
|
|
185
|
-
* @remarks
|
|
186
|
-
* Behaviour:
|
|
187
|
-
*
|
|
188
|
-
* - `GET` / `HEAD` — body is a fresh 25-byte TAI64N
|
|
189
|
-
* label (`HEAD` omits the body). Response headers:
|
|
190
|
-
* Content-Type `application/tai64n`, Content-Length
|
|
191
|
-
* `25`, Cache-Control `no-store`, plus
|
|
192
|
-
* `TAI-Leap-Seconds` carrying the current count.
|
|
193
|
-
* - `OPTIONS` — `200` with `Allow: GET, HEAD, OPTIONS`.
|
|
194
|
-
* When CORS is enabled (the default) the response
|
|
195
|
-
* also carries `Access-Control-Allow-*` and
|
|
196
|
-
* `-Expose-Headers` per
|
|
197
|
-
* {@link TaistampHandlerConfig.cors}. `OPTIONS` is
|
|
198
|
-
* never signed.
|
|
199
|
-
* - Any other method — `405 Method Not Allowed` with
|
|
200
|
-
* `Allow: GET, HEAD, OPTIONS`.
|
|
201
|
-
* - Request `TAI-Nonce` — on `GET`, the value is echoed
|
|
202
|
-
* verbatim in the response. A missing, empty,
|
|
203
|
-
* duplicated, structurally malformed, or out-of-range
|
|
204
|
-
* (14..174 octets) field is treated as absent (no
|
|
205
|
-
* echo, no signature) per spec §5.2 — see
|
|
206
|
-
* {@link extractNonce}. `HEAD`, `OPTIONS`, and `405`
|
|
207
|
-
* responses never carry `TAI-Nonce` per spec §4.1.
|
|
208
|
-
* - Request `TAI-Nonce` *and* `signer` configured *and*
|
|
209
|
-
* the request method is `GET` — adds
|
|
210
|
-
* `TAI-Key-Selector` and `TAI-Signature` (sf-binary)
|
|
211
|
-
* over the bytes produced by
|
|
212
|
-
* {@link composeSignaturePayload}. The
|
|
213
|
-
* domain-separation tag means the same key cannot
|
|
214
|
-
* be tricked into producing valid signatures for
|
|
215
|
-
* other protocols. `HEAD`, `OPTIONS`, and `405`
|
|
216
|
-
* responses are never signed.
|
|
217
|
-
*
|
|
218
|
-
* The corresponding public key is expected to be
|
|
219
|
-
* published out-of-band as a DNS TXT record at
|
|
220
|
-
* `<selector>._taistamp.<host>` — verifiers fetch the
|
|
221
|
-
* key by selector so the operator can rotate keys by
|
|
222
|
-
* publishing a new selector while the old one is
|
|
223
|
-
* still cached.
|
|
224
|
-
*
|
|
225
|
-
* @see {@link https://cr.yp.to/libtai/tai64.html} for
|
|
226
|
-
* TAI64N format
|
|
227
|
-
*/
|
|
228
|
-
declare const newTaistampHandler: (config?: TaistampHandlerConfig) => ((request: Request) => Promise<Response>);
|
|
229
|
-
|
|
230
|
-
type timestamp = {
|
|
231
|
-
nano: number;
|
|
232
|
-
sec: number;
|
|
233
|
-
offset?: number;
|
|
234
|
-
};
|
|
235
|
-
declare const fromUTC: (utc: number) => timestamp;
|
|
236
|
-
declare const now: () => timestamp;
|
|
237
|
-
declare const tai64nLabel: (value?: timestamp) => string;
|
|
238
|
-
declare const tai64nLabelFromUTC: (utc: number) => string;
|
|
239
|
-
|
|
240
|
-
/** Package version from package.json. */
|
|
241
|
-
declare const VERSION: string;
|
|
242
|
-
|
|
243
|
-
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, newTaistampHandler, now, tai64nLabel, tai64nLabelFromUTC };
|
|
244
|
-
export type { LeapSeconds, Nonce, TaistampHandlerConfig };
|