@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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/auth.d.ts +139 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +991 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/cache-status.d.ts +15 -0
  8. package/dist/cache-status.d.ts.map +1 -0
  9. package/dist/cache-status.js +152 -0
  10. package/dist/cache-status.js.map +1 -0
  11. package/dist/cache.d.ts +94 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +244 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/client-hints.d.ts +23 -0
  16. package/dist/client-hints.d.ts.map +1 -0
  17. package/dist/client-hints.js +81 -0
  18. package/dist/client-hints.js.map +1 -0
  19. package/dist/conditional.d.ts +97 -0
  20. package/dist/conditional.d.ts.map +1 -0
  21. package/dist/conditional.js +300 -0
  22. package/dist/conditional.js.map +1 -0
  23. package/dist/content-disposition.d.ts +23 -0
  24. package/dist/content-disposition.d.ts.map +1 -0
  25. package/dist/content-disposition.js +122 -0
  26. package/dist/content-disposition.js.map +1 -0
  27. package/dist/cookie.d.ts +43 -0
  28. package/dist/cookie.d.ts.map +1 -0
  29. package/dist/cookie.js +472 -0
  30. package/dist/cookie.js.map +1 -0
  31. package/dist/cors.d.ts +53 -0
  32. package/dist/cors.d.ts.map +1 -0
  33. package/dist/cors.js +170 -0
  34. package/dist/cors.js.map +1 -0
  35. package/dist/datetime.d.ts +53 -0
  36. package/dist/datetime.d.ts.map +1 -0
  37. package/dist/datetime.js +205 -0
  38. package/dist/datetime.js.map +1 -0
  39. package/dist/digest.d.ts +220 -0
  40. package/dist/digest.d.ts.map +1 -0
  41. package/dist/digest.js +355 -0
  42. package/dist/digest.js.map +1 -0
  43. package/dist/encoding.d.ts +14 -0
  44. package/dist/encoding.d.ts.map +1 -0
  45. package/dist/encoding.js +86 -0
  46. package/dist/encoding.js.map +1 -0
  47. package/dist/etag.d.ts +55 -0
  48. package/dist/etag.d.ts.map +1 -0
  49. package/dist/etag.js +182 -0
  50. package/dist/etag.js.map +1 -0
  51. package/dist/ext-value.d.ts +40 -0
  52. package/dist/ext-value.d.ts.map +1 -0
  53. package/dist/ext-value.js +119 -0
  54. package/dist/ext-value.js.map +1 -0
  55. package/dist/forwarded.d.ts +14 -0
  56. package/dist/forwarded.d.ts.map +1 -0
  57. package/dist/forwarded.js +93 -0
  58. package/dist/forwarded.js.map +1 -0
  59. package/dist/header-utils.d.ts +71 -0
  60. package/dist/header-utils.d.ts.map +1 -0
  61. package/dist/header-utils.js +143 -0
  62. package/dist/header-utils.js.map +1 -0
  63. package/dist/headers.d.ts +71 -0
  64. package/dist/headers.d.ts.map +1 -0
  65. package/dist/headers.js +134 -0
  66. package/dist/headers.js.map +1 -0
  67. package/dist/hsts.d.ts +15 -0
  68. package/dist/hsts.d.ts.map +1 -0
  69. package/dist/hsts.js +106 -0
  70. package/dist/hsts.js.map +1 -0
  71. package/dist/http-signatures.d.ts +202 -0
  72. package/dist/http-signatures.d.ts.map +1 -0
  73. package/dist/http-signatures.js +720 -0
  74. package/dist/http-signatures.js.map +1 -0
  75. package/dist/index.d.ts +41 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +125 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/json-pointer.d.ts +97 -0
  80. package/dist/json-pointer.d.ts.map +1 -0
  81. package/dist/json-pointer.js +278 -0
  82. package/dist/json-pointer.js.map +1 -0
  83. package/dist/jsonpath.d.ts +98 -0
  84. package/dist/jsonpath.d.ts.map +1 -0
  85. package/dist/jsonpath.js +1470 -0
  86. package/dist/jsonpath.js.map +1 -0
  87. package/dist/language.d.ts +14 -0
  88. package/dist/language.d.ts.map +1 -0
  89. package/dist/language.js +95 -0
  90. package/dist/language.js.map +1 -0
  91. package/dist/link.d.ts +102 -0
  92. package/dist/link.d.ts.map +1 -0
  93. package/dist/link.js +437 -0
  94. package/dist/link.js.map +1 -0
  95. package/dist/linkset.d.ts +111 -0
  96. package/dist/linkset.d.ts.map +1 -0
  97. package/dist/linkset.js +501 -0
  98. package/dist/linkset.js.map +1 -0
  99. package/dist/negotiate.d.ts +71 -0
  100. package/dist/negotiate.d.ts.map +1 -0
  101. package/dist/negotiate.js +357 -0
  102. package/dist/negotiate.js.map +1 -0
  103. package/dist/pagination.d.ts +80 -0
  104. package/dist/pagination.d.ts.map +1 -0
  105. package/dist/pagination.js +188 -0
  106. package/dist/pagination.js.map +1 -0
  107. package/dist/prefer.d.ts +18 -0
  108. package/dist/prefer.d.ts.map +1 -0
  109. package/dist/prefer.js +93 -0
  110. package/dist/prefer.js.map +1 -0
  111. package/dist/problem.d.ts +54 -0
  112. package/dist/problem.d.ts.map +1 -0
  113. package/dist/problem.js +104 -0
  114. package/dist/problem.js.map +1 -0
  115. package/dist/proxy-status.d.ts +28 -0
  116. package/dist/proxy-status.d.ts.map +1 -0
  117. package/dist/proxy-status.js +220 -0
  118. package/dist/proxy-status.js.map +1 -0
  119. package/dist/range.d.ts +28 -0
  120. package/dist/range.d.ts.map +1 -0
  121. package/dist/range.js +243 -0
  122. package/dist/range.js.map +1 -0
  123. package/dist/response.d.ts +101 -0
  124. package/dist/response.d.ts.map +1 -0
  125. package/dist/response.js +200 -0
  126. package/dist/response.js.map +1 -0
  127. package/dist/sorting.d.ts +66 -0
  128. package/dist/sorting.d.ts.map +1 -0
  129. package/dist/sorting.js +168 -0
  130. package/dist/sorting.js.map +1 -0
  131. package/dist/structured-fields.d.ts +30 -0
  132. package/dist/structured-fields.d.ts.map +1 -0
  133. package/dist/structured-fields.js +468 -0
  134. package/dist/structured-fields.js.map +1 -0
  135. package/dist/types.d.ts +772 -0
  136. package/dist/types.d.ts.map +1 -0
  137. package/dist/types.js +8 -0
  138. package/dist/types.js.map +1 -0
  139. package/dist/uri-template.d.ts +48 -0
  140. package/dist/uri-template.d.ts.map +1 -0
  141. package/dist/uri-template.js +483 -0
  142. package/dist/uri-template.js.map +1 -0
  143. package/dist/uri.d.ts +80 -0
  144. package/dist/uri.d.ts.map +1 -0
  145. package/dist/uri.js +423 -0
  146. package/dist/uri.js.map +1 -0
  147. 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