@irvinebroque/http-rfc-utils 0.1.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/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/auth.d.ts +139 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +991 -0
- package/dist/auth.js.map +1 -0
- package/dist/cache-status.d.ts +15 -0
- package/dist/cache-status.d.ts.map +1 -0
- package/dist/cache-status.js +152 -0
- package/dist/cache-status.js.map +1 -0
- package/dist/cache.d.ts +94 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +244 -0
- package/dist/cache.js.map +1 -0
- package/dist/client-hints.d.ts +23 -0
- package/dist/client-hints.d.ts.map +1 -0
- package/dist/client-hints.js +81 -0
- package/dist/client-hints.js.map +1 -0
- package/dist/conditional.d.ts +97 -0
- package/dist/conditional.d.ts.map +1 -0
- package/dist/conditional.js +300 -0
- package/dist/conditional.js.map +1 -0
- package/dist/content-disposition.d.ts +23 -0
- package/dist/content-disposition.d.ts.map +1 -0
- package/dist/content-disposition.js +122 -0
- package/dist/content-disposition.js.map +1 -0
- package/dist/cookie.d.ts +43 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +472 -0
- package/dist/cookie.js.map +1 -0
- package/dist/cors.d.ts +53 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +170 -0
- package/dist/cors.js.map +1 -0
- package/dist/datetime.d.ts +53 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +205 -0
- package/dist/datetime.js.map +1 -0
- package/dist/digest.d.ts +220 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +355 -0
- package/dist/digest.js.map +1 -0
- package/dist/encoding.d.ts +14 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +86 -0
- package/dist/encoding.js.map +1 -0
- package/dist/etag.d.ts +55 -0
- package/dist/etag.d.ts.map +1 -0
- package/dist/etag.js +182 -0
- package/dist/etag.js.map +1 -0
- package/dist/ext-value.d.ts +40 -0
- package/dist/ext-value.d.ts.map +1 -0
- package/dist/ext-value.js +119 -0
- package/dist/ext-value.js.map +1 -0
- package/dist/forwarded.d.ts +14 -0
- package/dist/forwarded.d.ts.map +1 -0
- package/dist/forwarded.js +93 -0
- package/dist/forwarded.js.map +1 -0
- package/dist/header-utils.d.ts +71 -0
- package/dist/header-utils.d.ts.map +1 -0
- package/dist/header-utils.js +143 -0
- package/dist/header-utils.js.map +1 -0
- package/dist/headers.d.ts +71 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +134 -0
- package/dist/headers.js.map +1 -0
- package/dist/hsts.d.ts +15 -0
- package/dist/hsts.d.ts.map +1 -0
- package/dist/hsts.js +106 -0
- package/dist/hsts.js.map +1 -0
- package/dist/http-signatures.d.ts +202 -0
- package/dist/http-signatures.d.ts.map +1 -0
- package/dist/http-signatures.js +720 -0
- package/dist/http-signatures.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/json-pointer.d.ts +97 -0
- package/dist/json-pointer.d.ts.map +1 -0
- package/dist/json-pointer.js +278 -0
- package/dist/json-pointer.js.map +1 -0
- package/dist/jsonpath.d.ts +98 -0
- package/dist/jsonpath.d.ts.map +1 -0
- package/dist/jsonpath.js +1470 -0
- package/dist/jsonpath.js.map +1 -0
- package/dist/language.d.ts +14 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +95 -0
- package/dist/language.js.map +1 -0
- package/dist/link.d.ts +102 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +437 -0
- package/dist/link.js.map +1 -0
- package/dist/linkset.d.ts +111 -0
- package/dist/linkset.d.ts.map +1 -0
- package/dist/linkset.js +501 -0
- package/dist/linkset.js.map +1 -0
- package/dist/negotiate.d.ts +71 -0
- package/dist/negotiate.d.ts.map +1 -0
- package/dist/negotiate.js +357 -0
- package/dist/negotiate.js.map +1 -0
- package/dist/pagination.d.ts +80 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +188 -0
- package/dist/pagination.js.map +1 -0
- package/dist/prefer.d.ts +18 -0
- package/dist/prefer.d.ts.map +1 -0
- package/dist/prefer.js +93 -0
- package/dist/prefer.js.map +1 -0
- package/dist/problem.d.ts +54 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +104 -0
- package/dist/problem.js.map +1 -0
- package/dist/proxy-status.d.ts +28 -0
- package/dist/proxy-status.d.ts.map +1 -0
- package/dist/proxy-status.js +220 -0
- package/dist/proxy-status.js.map +1 -0
- package/dist/range.d.ts +28 -0
- package/dist/range.d.ts.map +1 -0
- package/dist/range.js +243 -0
- package/dist/range.js.map +1 -0
- package/dist/response.d.ts +101 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +200 -0
- package/dist/response.js.map +1 -0
- package/dist/sorting.d.ts +66 -0
- package/dist/sorting.d.ts.map +1 -0
- package/dist/sorting.js +168 -0
- package/dist/sorting.js.map +1 -0
- package/dist/structured-fields.d.ts +30 -0
- package/dist/structured-fields.d.ts.map +1 -0
- package/dist/structured-fields.js +468 -0
- package/dist/structured-fields.js.map +1 -0
- package/dist/types.d.ts +772 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/uri-template.d.ts +48 -0
- package/dist/uri-template.d.ts.map +1 -0
- package/dist/uri-template.js +483 -0
- package/dist/uri-template.js.map +1 -0
- package/dist/uri.d.ts +80 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +423 -0
- package/dist/uri.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Message Signatures per RFC 9421.
|
|
3
|
+
* RFC 9421 §2-3.
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9421.html
|
|
5
|
+
*
|
|
6
|
+
* This module provides primitives for creating signature bases and parsing/formatting
|
|
7
|
+
* Signature-Input and Signature fields. Actual cryptographic signing/verification
|
|
8
|
+
* is out of scope - this provides the HTTP-layer primitives.
|
|
9
|
+
*/
|
|
10
|
+
import { Buffer } from 'node:buffer';
|
|
11
|
+
import { parseSfDict, serializeSfDict } from './structured-fields.js';
|
|
12
|
+
/**
|
|
13
|
+
* Derived component names per RFC 9421 §2.2.
|
|
14
|
+
* These are special component identifiers that start with '@'.
|
|
15
|
+
*/
|
|
16
|
+
export const DERIVED_COMPONENTS = [
|
|
17
|
+
'@method',
|
|
18
|
+
'@target-uri',
|
|
19
|
+
'@authority',
|
|
20
|
+
'@scheme',
|
|
21
|
+
'@request-target',
|
|
22
|
+
'@path',
|
|
23
|
+
'@query',
|
|
24
|
+
'@query-param',
|
|
25
|
+
'@status',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Check if a component name is a derived component.
|
|
29
|
+
* RFC 9421 §2.2.
|
|
30
|
+
*/
|
|
31
|
+
export function isDerivedComponent(name) {
|
|
32
|
+
return DERIVED_COMPONENTS.includes(name);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a Signature-Input field value.
|
|
36
|
+
* RFC 9421 §4.1.
|
|
37
|
+
*
|
|
38
|
+
* The Signature-Input field is a Dictionary Structured Field containing the
|
|
39
|
+
* metadata for one or more message signatures.
|
|
40
|
+
*
|
|
41
|
+
* @param value - The Signature-Input header field value
|
|
42
|
+
* @returns Array of parsed SignatureInput objects, or null if parsing fails
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const inputs = parseSignatureInput(
|
|
47
|
+
* 'sig1=("@method" "@authority" "content-type");created=1618884473;keyid="test-key"'
|
|
48
|
+
* );
|
|
49
|
+
* // Returns: [{ label: 'sig1', components: [...], params: { created: 1618884473, keyid: 'test-key' } }]
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function parseSignatureInput(value) {
|
|
53
|
+
const dict = parseSfDict(value);
|
|
54
|
+
if (!dict) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const [label, entry] of Object.entries(dict)) {
|
|
59
|
+
// Each entry MUST be an inner list
|
|
60
|
+
if (!('items' in entry)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const innerList = entry;
|
|
64
|
+
const components = [];
|
|
65
|
+
// Parse component identifiers from inner list items
|
|
66
|
+
for (const item of innerList.items) {
|
|
67
|
+
// Each item MUST be a string (component identifier)
|
|
68
|
+
if (typeof item.value !== 'string') {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const component = parseComponentIdentifierFromItem(item);
|
|
72
|
+
if (!component) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
components.push(component);
|
|
76
|
+
}
|
|
77
|
+
// Parse signature parameters from inner list params
|
|
78
|
+
const params = parseSignatureParamsFromSf(innerList.params);
|
|
79
|
+
results.push({ label, components, params });
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse a component identifier from a structured field item.
|
|
85
|
+
* RFC 9421 §2.
|
|
86
|
+
*/
|
|
87
|
+
function parseComponentIdentifierFromItem(item) {
|
|
88
|
+
if (typeof item.value !== 'string') {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const name = item.value;
|
|
92
|
+
const params = {};
|
|
93
|
+
if (item.params) {
|
|
94
|
+
if (item.params.sf === true) {
|
|
95
|
+
params.sf = true;
|
|
96
|
+
}
|
|
97
|
+
if (typeof item.params.key === 'string') {
|
|
98
|
+
params.key = item.params.key;
|
|
99
|
+
}
|
|
100
|
+
if (item.params.bs === true) {
|
|
101
|
+
params.bs = true;
|
|
102
|
+
}
|
|
103
|
+
if (item.params.req === true) {
|
|
104
|
+
params.req = true;
|
|
105
|
+
}
|
|
106
|
+
if (item.params.tr === true) {
|
|
107
|
+
params.tr = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Object.keys(params).length > 0 ? { name, params } : { name };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Parse signature parameters from structured field params.
|
|
114
|
+
* RFC 9421 §2.3.
|
|
115
|
+
*/
|
|
116
|
+
function parseSignatureParamsFromSf(sfParams) {
|
|
117
|
+
if (!sfParams) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const params = {};
|
|
121
|
+
if (typeof sfParams.created === 'number') {
|
|
122
|
+
params.created = sfParams.created;
|
|
123
|
+
}
|
|
124
|
+
if (typeof sfParams.expires === 'number') {
|
|
125
|
+
params.expires = sfParams.expires;
|
|
126
|
+
}
|
|
127
|
+
if (typeof sfParams.nonce === 'string') {
|
|
128
|
+
params.nonce = sfParams.nonce;
|
|
129
|
+
}
|
|
130
|
+
if (typeof sfParams.alg === 'string') {
|
|
131
|
+
params.alg = sfParams.alg;
|
|
132
|
+
}
|
|
133
|
+
if (typeof sfParams.keyid === 'string') {
|
|
134
|
+
params.keyid = sfParams.keyid;
|
|
135
|
+
}
|
|
136
|
+
if (typeof sfParams.tag === 'string') {
|
|
137
|
+
params.tag = sfParams.tag;
|
|
138
|
+
}
|
|
139
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Format SignatureInput objects to a Signature-Input field value.
|
|
143
|
+
* RFC 9421 §4.1.
|
|
144
|
+
*
|
|
145
|
+
* @param inputs - Array of SignatureInput objects to format
|
|
146
|
+
* @returns The formatted Signature-Input header field value
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* const value = formatSignatureInput([{
|
|
151
|
+
* label: 'sig1',
|
|
152
|
+
* components: [{ name: '@method' }, { name: 'content-type' }],
|
|
153
|
+
* params: { created: 1618884473, keyid: 'test-key' }
|
|
154
|
+
* }]);
|
|
155
|
+
* // Returns: 'sig1=("@method" "content-type");created=1618884473;keyid="test-key"'
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export function formatSignatureInput(inputs) {
|
|
159
|
+
const dict = {};
|
|
160
|
+
for (const input of inputs) {
|
|
161
|
+
const items = input.components.map(component => {
|
|
162
|
+
const item = { value: component.name };
|
|
163
|
+
if (component.params) {
|
|
164
|
+
const params = {};
|
|
165
|
+
if (component.params.sf) {
|
|
166
|
+
params.sf = true;
|
|
167
|
+
}
|
|
168
|
+
if (component.params.key !== undefined) {
|
|
169
|
+
params.key = component.params.key;
|
|
170
|
+
}
|
|
171
|
+
if (component.params.bs) {
|
|
172
|
+
params.bs = true;
|
|
173
|
+
}
|
|
174
|
+
if (component.params.req) {
|
|
175
|
+
params.req = true;
|
|
176
|
+
}
|
|
177
|
+
if (component.params.tr) {
|
|
178
|
+
params.tr = true;
|
|
179
|
+
}
|
|
180
|
+
if (Object.keys(params).length > 0) {
|
|
181
|
+
item.params = params;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return item;
|
|
185
|
+
});
|
|
186
|
+
const innerList = { items };
|
|
187
|
+
if (input.params) {
|
|
188
|
+
const params = {};
|
|
189
|
+
if (input.params.created !== undefined) {
|
|
190
|
+
params.created = input.params.created;
|
|
191
|
+
}
|
|
192
|
+
if (input.params.expires !== undefined) {
|
|
193
|
+
params.expires = input.params.expires;
|
|
194
|
+
}
|
|
195
|
+
if (input.params.nonce !== undefined) {
|
|
196
|
+
params.nonce = input.params.nonce;
|
|
197
|
+
}
|
|
198
|
+
if (input.params.alg !== undefined) {
|
|
199
|
+
params.alg = input.params.alg;
|
|
200
|
+
}
|
|
201
|
+
if (input.params.keyid !== undefined) {
|
|
202
|
+
params.keyid = input.params.keyid;
|
|
203
|
+
}
|
|
204
|
+
if (input.params.tag !== undefined) {
|
|
205
|
+
params.tag = input.params.tag;
|
|
206
|
+
}
|
|
207
|
+
if (Object.keys(params).length > 0) {
|
|
208
|
+
innerList.params = params;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
dict[input.label] = innerList;
|
|
212
|
+
}
|
|
213
|
+
return serializeSfDict(dict);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Parse a Signature field value.
|
|
217
|
+
* RFC 9421 §4.2.
|
|
218
|
+
*
|
|
219
|
+
* The Signature field is a Dictionary Structured Field containing signature
|
|
220
|
+
* values as byte sequences.
|
|
221
|
+
*
|
|
222
|
+
* @param value - The Signature header field value
|
|
223
|
+
* @returns Array of parsed Signature objects, or null if parsing fails
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* const sigs = parseSignature('sig1=:YmFzZTY0ZW5jb2RlZHNpZw==:');
|
|
228
|
+
* // Returns: [{ label: 'sig1', value: Uint8Array([...]) }]
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
export function parseSignature(value) {
|
|
232
|
+
const dict = parseSfDict(value);
|
|
233
|
+
if (!dict) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const results = [];
|
|
237
|
+
for (const [label, entry] of Object.entries(dict)) {
|
|
238
|
+
// Each entry MUST be a byte sequence (item)
|
|
239
|
+
if ('items' in entry) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const item = entry;
|
|
243
|
+
if (!(item.value instanceof Uint8Array)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
results.push({ label, value: item.value });
|
|
247
|
+
}
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Format Signature objects to a Signature field value.
|
|
252
|
+
* RFC 9421 §4.2.
|
|
253
|
+
*
|
|
254
|
+
* @param signatures - Array of Signature objects to format
|
|
255
|
+
* @returns The formatted Signature header field value
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* const value = formatSignature([{
|
|
260
|
+
* label: 'sig1',
|
|
261
|
+
* value: new Uint8Array([98, 97, 115, 101, 54, 52])
|
|
262
|
+
* }]);
|
|
263
|
+
* // Returns: 'sig1=:YmFzZTY0:' (base64 encoded)
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export function formatSignature(signatures) {
|
|
267
|
+
const dict = {};
|
|
268
|
+
for (const sig of signatures) {
|
|
269
|
+
dict[sig.label] = { value: sig.value };
|
|
270
|
+
}
|
|
271
|
+
return serializeSfDict(dict);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Parse a component identifier string.
|
|
275
|
+
* RFC 9421 §2.
|
|
276
|
+
*
|
|
277
|
+
* Component identifiers are strings that identify HTTP message components
|
|
278
|
+
* to be included in the signature base.
|
|
279
|
+
*
|
|
280
|
+
* @param value - The component identifier string (e.g., '"content-type"' or '"cache-control";sf')
|
|
281
|
+
* @returns Parsed SignatureComponent, or null if parsing fails
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* const component = parseComponentIdentifier('"content-type"');
|
|
286
|
+
* // Returns: { name: 'content-type' }
|
|
287
|
+
*
|
|
288
|
+
* const componentWithParams = parseComponentIdentifier('"example-dict";key="member"');
|
|
289
|
+
* // Returns: { name: 'example-dict', params: { key: 'member' } }
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
export function parseComponentIdentifier(value) {
|
|
293
|
+
// A component identifier is represented as a string item with optional parameters
|
|
294
|
+
// Format: "name" or "name";param1;param2=value
|
|
295
|
+
const trimmed = value.trim();
|
|
296
|
+
// Must start with a quote
|
|
297
|
+
if (!trimmed.startsWith('"')) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
// Find the end of the quoted string
|
|
301
|
+
let i = 1;
|
|
302
|
+
let name = '';
|
|
303
|
+
while (i < trimmed.length) {
|
|
304
|
+
const char = trimmed[i];
|
|
305
|
+
if (char === '"') {
|
|
306
|
+
i++;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (char === '\\' && i + 1 < trimmed.length) {
|
|
310
|
+
// Escape sequence
|
|
311
|
+
i++;
|
|
312
|
+
name += trimmed[i];
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
name += char;
|
|
316
|
+
}
|
|
317
|
+
i++;
|
|
318
|
+
}
|
|
319
|
+
if (i === trimmed.length && trimmed[i - 1] !== '"') {
|
|
320
|
+
return null; // Unterminated string
|
|
321
|
+
}
|
|
322
|
+
const params = {};
|
|
323
|
+
// Parse parameters after the quoted string
|
|
324
|
+
while (i < trimmed.length) {
|
|
325
|
+
// Skip whitespace
|
|
326
|
+
while (i < trimmed.length && (trimmed[i] === ' ' || trimmed[i] === '\t')) {
|
|
327
|
+
i++;
|
|
328
|
+
}
|
|
329
|
+
if (i >= trimmed.length) {
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
// Expect semicolon
|
|
333
|
+
if (trimmed[i] !== ';') {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
i++;
|
|
337
|
+
// Skip whitespace
|
|
338
|
+
while (i < trimmed.length && (trimmed[i] === ' ' || trimmed[i] === '\t')) {
|
|
339
|
+
i++;
|
|
340
|
+
}
|
|
341
|
+
// Parse parameter name
|
|
342
|
+
let paramName = '';
|
|
343
|
+
while (i < trimmed.length) {
|
|
344
|
+
const ch = trimmed[i];
|
|
345
|
+
if (ch === undefined || !/[a-z0-9_\-\.\*]/.test(ch)) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
paramName += ch;
|
|
349
|
+
i++;
|
|
350
|
+
}
|
|
351
|
+
if (!paramName) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
// Check for parameter value
|
|
355
|
+
// Skip whitespace
|
|
356
|
+
while (i < trimmed.length && (trimmed[i] === ' ' || trimmed[i] === '\t')) {
|
|
357
|
+
i++;
|
|
358
|
+
}
|
|
359
|
+
if (i < trimmed.length && trimmed[i] === '=') {
|
|
360
|
+
i++;
|
|
361
|
+
// Skip whitespace
|
|
362
|
+
while (i < trimmed.length && (trimmed[i] === ' ' || trimmed[i] === '\t')) {
|
|
363
|
+
i++;
|
|
364
|
+
}
|
|
365
|
+
// Parse parameter value (quoted string or token)
|
|
366
|
+
if (trimmed[i] === '"') {
|
|
367
|
+
i++;
|
|
368
|
+
let paramValue = '';
|
|
369
|
+
while (i < trimmed.length && trimmed[i] !== '"') {
|
|
370
|
+
if (trimmed[i] === '\\' && i + 1 < trimmed.length) {
|
|
371
|
+
i++;
|
|
372
|
+
paramValue += trimmed[i];
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
paramValue += trimmed[i];
|
|
376
|
+
}
|
|
377
|
+
i++;
|
|
378
|
+
}
|
|
379
|
+
if (trimmed[i] !== '"') {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
i++;
|
|
383
|
+
if (paramName === 'key') {
|
|
384
|
+
params.key = paramValue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// Token value
|
|
389
|
+
let paramValue = '';
|
|
390
|
+
while (i < trimmed.length) {
|
|
391
|
+
const ch = trimmed[i];
|
|
392
|
+
if (ch === undefined || !/[A-Za-z0-9!#$%&'*+\-.^_`|~:\/]/.test(ch)) {
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
paramValue += ch;
|
|
396
|
+
i++;
|
|
397
|
+
}
|
|
398
|
+
if (paramName === 'key') {
|
|
399
|
+
params.key = paramValue;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
// Boolean parameter
|
|
405
|
+
if (paramName === 'sf') {
|
|
406
|
+
params.sf = true;
|
|
407
|
+
}
|
|
408
|
+
else if (paramName === 'bs') {
|
|
409
|
+
params.bs = true;
|
|
410
|
+
}
|
|
411
|
+
else if (paramName === 'req') {
|
|
412
|
+
params.req = true;
|
|
413
|
+
}
|
|
414
|
+
else if (paramName === 'tr') {
|
|
415
|
+
params.tr = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return Object.keys(params).length > 0 ? { name, params } : { name };
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Format a component identifier to string.
|
|
423
|
+
* RFC 9421 §2.
|
|
424
|
+
*
|
|
425
|
+
* @param component - The SignatureComponent to format
|
|
426
|
+
* @returns The formatted component identifier string
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```ts
|
|
430
|
+
* formatComponentIdentifier({ name: 'content-type' });
|
|
431
|
+
* // Returns: '"content-type"'
|
|
432
|
+
*
|
|
433
|
+
* formatComponentIdentifier({ name: 'example-dict', params: { key: 'member' } });
|
|
434
|
+
* // Returns: '"example-dict";key="member"'
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
export function formatComponentIdentifier(component) {
|
|
438
|
+
// Escape special characters in name
|
|
439
|
+
const escapedName = component.name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
440
|
+
let result = `"${escapedName}"`;
|
|
441
|
+
if (component.params) {
|
|
442
|
+
if (component.params.sf) {
|
|
443
|
+
result += ';sf';
|
|
444
|
+
}
|
|
445
|
+
if (component.params.key !== undefined) {
|
|
446
|
+
const escapedKey = component.params.key.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
447
|
+
result += `;key="${escapedKey}"`;
|
|
448
|
+
}
|
|
449
|
+
if (component.params.bs) {
|
|
450
|
+
result += ';bs';
|
|
451
|
+
}
|
|
452
|
+
if (component.params.req) {
|
|
453
|
+
result += ';req';
|
|
454
|
+
}
|
|
455
|
+
if (component.params.tr) {
|
|
456
|
+
result += ';tr';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Canonicalize field values per RFC 9421 §2.1.
|
|
463
|
+
*
|
|
464
|
+
* Multiple field values are combined with ", " (comma + space).
|
|
465
|
+
* Leading/trailing whitespace is trimmed from each value.
|
|
466
|
+
* Obsolete line folding is replaced with a single space.
|
|
467
|
+
*
|
|
468
|
+
* @param values - Array of field values
|
|
469
|
+
* @returns The canonicalized field value
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```ts
|
|
473
|
+
* canonicalizeFieldValue([' value1 ', ' value2 ']);
|
|
474
|
+
* // Returns: 'value1, value2'
|
|
475
|
+
* ```
|
|
476
|
+
*/
|
|
477
|
+
export function canonicalizeFieldValue(values) {
|
|
478
|
+
return values
|
|
479
|
+
.map(v => {
|
|
480
|
+
// RFC 9421 §2.1: Replace obsolete line folding (CRLF + WSP) with single space
|
|
481
|
+
const unfolded = v.replace(/\r?\n[ \t]+/g, ' ');
|
|
482
|
+
// Trim leading and trailing whitespace
|
|
483
|
+
return unfolded.trim();
|
|
484
|
+
})
|
|
485
|
+
.join(', ');
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Binary-wrap field values per RFC 9421 §2.1.4.
|
|
489
|
+
*
|
|
490
|
+
* Each field value is base64-encoded individually, and the results are
|
|
491
|
+
* combined with ", " (comma + space) and then re-encoded as a byte sequence.
|
|
492
|
+
*
|
|
493
|
+
* @param values - Array of field values
|
|
494
|
+
* @returns The binary-wrapped field value as a byte sequence
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* ```ts
|
|
498
|
+
* binaryWrapFieldValues(['value1', 'value2']);
|
|
499
|
+
* // Returns base64 of each value concatenated
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
export function binaryWrapFieldValues(values) {
|
|
503
|
+
// RFC 9421 §2.1.4: For binary-wrapped fields, each field line value is
|
|
504
|
+
// base64-encoded and the results are concatenated with ":"
|
|
505
|
+
const encoded = values.map(v => {
|
|
506
|
+
// Encode each value as UTF-8 bytes, then base64
|
|
507
|
+
const bytes = new TextEncoder().encode(v.trim());
|
|
508
|
+
return `:${Buffer.from(bytes).toString('base64')}:`;
|
|
509
|
+
});
|
|
510
|
+
// Join with ", " and return as bytes
|
|
511
|
+
const combined = encoded.join(', ');
|
|
512
|
+
return new TextEncoder().encode(combined);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Derive a component value from a message context.
|
|
516
|
+
* RFC 9421 §2.
|
|
517
|
+
*
|
|
518
|
+
* @param message - The message context
|
|
519
|
+
* @param component - The component to derive
|
|
520
|
+
* @returns The derived value, or null if the component cannot be derived
|
|
521
|
+
*/
|
|
522
|
+
export function deriveComponentValue(message, component) {
|
|
523
|
+
const name = component.name.toLowerCase();
|
|
524
|
+
// RFC 9421 §2.1: Field names MUST be lowercased
|
|
525
|
+
// RFC 9421 §2.2: Derived components start with '@'
|
|
526
|
+
if (isDerivedComponent(name)) {
|
|
527
|
+
return deriveDerivedComponentValue(message, component);
|
|
528
|
+
}
|
|
529
|
+
// Regular header field
|
|
530
|
+
return deriveFieldValue(message, component);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Derive a derived component value.
|
|
534
|
+
* RFC 9421 §2.2.
|
|
535
|
+
*/
|
|
536
|
+
function deriveDerivedComponentValue(message, component) {
|
|
537
|
+
const name = component.name;
|
|
538
|
+
// RFC 9421 §2.2.9: If req parameter is set, derive from request context
|
|
539
|
+
const ctx = component.params?.req ? message.request : message;
|
|
540
|
+
if (component.params?.req && !message.request) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
switch (name) {
|
|
544
|
+
case '@method':
|
|
545
|
+
// RFC 9421 §2.2.1: Method MUST be uppercase
|
|
546
|
+
return ctx?.method?.toUpperCase() ?? null;
|
|
547
|
+
case '@target-uri':
|
|
548
|
+
// RFC 9421 §2.2.2: Full target URI
|
|
549
|
+
return ctx?.targetUri ?? null;
|
|
550
|
+
case '@authority':
|
|
551
|
+
// RFC 9421 §2.2.3: Host + optional port
|
|
552
|
+
return ctx?.authority ?? null;
|
|
553
|
+
case '@scheme':
|
|
554
|
+
// RFC 9421 §2.2.4: Scheme (lowercase)
|
|
555
|
+
return ctx?.scheme?.toLowerCase() ?? null;
|
|
556
|
+
case '@request-target':
|
|
557
|
+
// RFC 9421 §2.2.5: Request target (path + query, HTTP/1.1 style)
|
|
558
|
+
if (ctx?.path === undefined) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
return ctx.query ? `${ctx.path}?${ctx.query.slice(1)}` : ctx.path;
|
|
562
|
+
case '@path':
|
|
563
|
+
// RFC 9421 §2.2.6: Absolute path (normalized)
|
|
564
|
+
return ctx?.path ?? null;
|
|
565
|
+
case '@query':
|
|
566
|
+
// RFC 9421 §2.2.7: Query string with leading '?', or '?' if empty
|
|
567
|
+
if (ctx?.query === undefined) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
// Query should include leading '?'
|
|
571
|
+
return ctx.query.startsWith('?') ? ctx.query : `?${ctx.query}`;
|
|
572
|
+
case '@query-param':
|
|
573
|
+
// RFC 9421 §2.2.8: Individual query parameter
|
|
574
|
+
return deriveQueryParam(ctx, component);
|
|
575
|
+
case '@status':
|
|
576
|
+
// RFC 9421 §2.2.10: Status code (3 digits, response only)
|
|
577
|
+
if (message.status === undefined) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
return String(message.status).padStart(3, '0');
|
|
581
|
+
default:
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Derive a query parameter value.
|
|
587
|
+
* RFC 9421 §2.2.8.
|
|
588
|
+
*/
|
|
589
|
+
function deriveQueryParam(ctx, component) {
|
|
590
|
+
if (!ctx?.query || !component.params?.key) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
// Parse query string
|
|
594
|
+
const query = ctx.query.startsWith('?') ? ctx.query.slice(1) : ctx.query;
|
|
595
|
+
const params = new URLSearchParams(query);
|
|
596
|
+
const value = params.get(component.params.key);
|
|
597
|
+
return value;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Derive a field value from headers.
|
|
601
|
+
* RFC 9421 §2.1.
|
|
602
|
+
*/
|
|
603
|
+
function deriveFieldValue(message, component) {
|
|
604
|
+
const name = component.name.toLowerCase();
|
|
605
|
+
// RFC 9421 §2.2.9: If req parameter is set, derive from request context
|
|
606
|
+
const ctx = component.params?.req ? message.request : message;
|
|
607
|
+
if (component.params?.req && !message.request) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
// RFC 9421 §2.1.3: If tr parameter is set, derive from trailers
|
|
611
|
+
const headers = component.params?.tr ? ctx?.trailers : ctx?.headers;
|
|
612
|
+
if (!headers) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
const values = headers.get(name);
|
|
616
|
+
if (!values || values.length === 0) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
// RFC 9421 §2.1.4: Binary-wrapped fields
|
|
620
|
+
if (component.params?.bs) {
|
|
621
|
+
const wrapped = binaryWrapFieldValues(values);
|
|
622
|
+
return `:${Buffer.from(wrapped).toString('base64')}:`;
|
|
623
|
+
}
|
|
624
|
+
// RFC 9421 §2.1: Canonicalize field value
|
|
625
|
+
return canonicalizeFieldValue(values);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Create the signature base string.
|
|
629
|
+
* RFC 9421 §2.5.
|
|
630
|
+
*
|
|
631
|
+
* The signature base is the string that will be signed. It contains one line
|
|
632
|
+
* per covered component, plus the signature parameters line at the end.
|
|
633
|
+
*
|
|
634
|
+
* @param message - The message context
|
|
635
|
+
* @param components - The components to include in the signature
|
|
636
|
+
* @param params - The signature parameters
|
|
637
|
+
* @returns The signature base and formatted signature-params, or null if creation fails
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```ts
|
|
641
|
+
* const result = createSignatureBase(
|
|
642
|
+
* {
|
|
643
|
+
* method: 'POST',
|
|
644
|
+
* authority: 'example.com',
|
|
645
|
+
* headers: new Map([['content-type', ['application/json']]])
|
|
646
|
+
* },
|
|
647
|
+
* [{ name: '@method' }, { name: '@authority' }, { name: 'content-type' }],
|
|
648
|
+
* { created: 1618884473, keyid: 'test-key' }
|
|
649
|
+
* );
|
|
650
|
+
* // Returns:
|
|
651
|
+
* // {
|
|
652
|
+
* // base: '"@method": POST\n"@authority": example.com\n"content-type": application/json\n"@signature-params": ("@method" "@authority" "content-type");created=1618884473;keyid="test-key"',
|
|
653
|
+
* // signatureParams: '("@method" "@authority" "content-type");created=1618884473;keyid="test-key"'
|
|
654
|
+
* // }
|
|
655
|
+
* ```
|
|
656
|
+
*/
|
|
657
|
+
export function createSignatureBase(message, components, params) {
|
|
658
|
+
const lines = [];
|
|
659
|
+
// RFC 9421 §2.5: Each component identifier MUST occur only once
|
|
660
|
+
const seen = new Set();
|
|
661
|
+
for (const component of components) {
|
|
662
|
+
const identifier = formatComponentIdentifier(component);
|
|
663
|
+
if (seen.has(identifier)) {
|
|
664
|
+
return null; // Duplicate component
|
|
665
|
+
}
|
|
666
|
+
seen.add(identifier);
|
|
667
|
+
const value = deriveComponentValue(message, component);
|
|
668
|
+
if (value === null) {
|
|
669
|
+
return null; // Required component missing
|
|
670
|
+
}
|
|
671
|
+
// RFC 9421 §2.5: Component values MUST NOT contain newline characters
|
|
672
|
+
if (value.includes('\n') || value.includes('\r')) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
// RFC 9421 §2.5: Each line is "identifier": value
|
|
676
|
+
lines.push(`${identifier}: ${value}`);
|
|
677
|
+
}
|
|
678
|
+
// RFC 9421 §3.1: Build @signature-params as the final line
|
|
679
|
+
const signatureParams = buildSignatureParamsValue(components, params);
|
|
680
|
+
lines.push(`"@signature-params": ${signatureParams}`);
|
|
681
|
+
// RFC 9421 §2.5: Lines separated by single LF (no trailing LF)
|
|
682
|
+
const base = lines.join('\n');
|
|
683
|
+
return { base, signatureParams };
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Build the @signature-params value.
|
|
687
|
+
* RFC 9421 §2.3.
|
|
688
|
+
*/
|
|
689
|
+
function buildSignatureParamsValue(components, params) {
|
|
690
|
+
// Build the inner list representation
|
|
691
|
+
const items = components.map(formatComponentIdentifier);
|
|
692
|
+
let result = `(${items.join(' ')})`;
|
|
693
|
+
// Add parameters in the defined order
|
|
694
|
+
if (params) {
|
|
695
|
+
if (params.created !== undefined) {
|
|
696
|
+
result += `;created=${params.created}`;
|
|
697
|
+
}
|
|
698
|
+
if (params.expires !== undefined) {
|
|
699
|
+
result += `;expires=${params.expires}`;
|
|
700
|
+
}
|
|
701
|
+
if (params.nonce !== undefined) {
|
|
702
|
+
const escapedNonce = params.nonce.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
703
|
+
result += `;nonce="${escapedNonce}"`;
|
|
704
|
+
}
|
|
705
|
+
if (params.alg !== undefined) {
|
|
706
|
+
const escapedAlg = params.alg.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
707
|
+
result += `;alg="${escapedAlg}"`;
|
|
708
|
+
}
|
|
709
|
+
if (params.keyid !== undefined) {
|
|
710
|
+
const escapedKeyid = params.keyid.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
711
|
+
result += `;keyid="${escapedKeyid}"`;
|
|
712
|
+
}
|
|
713
|
+
if (params.tag !== undefined) {
|
|
714
|
+
const escapedTag = params.tag.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
715
|
+
result += `;tag="${escapedTag}"`;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=http-signatures.js.map
|