@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
package/dist/auth.js ADDED
@@ -0,0 +1,991 @@
1
+ /**
2
+ * Authorization and WWW-Authenticate utilities for Basic, Bearer, and Digest.
3
+ * RFC 7617 §2, §2.1; RFC 6750 §2.1, §3; RFC 7616 §3.3-3.5.
4
+ * @see https://www.rfc-editor.org/rfc/rfc7617.html#section-2
5
+ * @see https://www.rfc-editor.org/rfc/rfc7616.html
6
+ */
7
+ import { Buffer } from 'node:buffer';
8
+ import { createHash } from 'node:crypto';
9
+ import { decodeExtValue, encodeExtValue } from './ext-value.js';
10
+ import { TOKEN_CHARS } from './header-utils.js';
11
+ const TOKEN68_RE = /^[A-Za-z0-9\-._~+\/]+={0,}$/;
12
+ const B64TOKEN_RE = /^[A-Za-z0-9\-._~+\/]+={0,}$/;
13
+ const BEARER_ERRORS = ['invalid_request', 'invalid_token', 'insufficient_scope'];
14
+ function isToken(value) {
15
+ return TOKEN_CHARS.test(value);
16
+ }
17
+ function isToken68(value) {
18
+ return TOKEN68_RE.test(value);
19
+ }
20
+ function isB64Token(value) {
21
+ return B64TOKEN_RE.test(value);
22
+ }
23
+ function skipOWS(input, index) {
24
+ let i = index;
25
+ while (i < input.length) {
26
+ const char = input[i];
27
+ if (char === ' ' || char === '\t') {
28
+ i++;
29
+ }
30
+ else {
31
+ break;
32
+ }
33
+ }
34
+ return i;
35
+ }
36
+ function parseToken(input, index) {
37
+ let i = index;
38
+ let token = '';
39
+ while (i < input.length) {
40
+ const char = input[i];
41
+ if (!char || !isToken(char)) {
42
+ break;
43
+ }
44
+ token += char;
45
+ i++;
46
+ }
47
+ if (!token) {
48
+ return null;
49
+ }
50
+ return { token, index: i };
51
+ }
52
+ function parseQuotedString(input, index) {
53
+ if (input[index] !== '"') {
54
+ return null;
55
+ }
56
+ let i = index + 1;
57
+ let value = '';
58
+ while (i < input.length) {
59
+ const char = input[i];
60
+ if (char === '"') {
61
+ return { value, index: i + 1 };
62
+ }
63
+ if (char === '\\' && i + 1 < input.length) {
64
+ value += input[i + 1];
65
+ i += 2;
66
+ continue;
67
+ }
68
+ value += char;
69
+ i++;
70
+ }
71
+ return null;
72
+ }
73
+ function parseTokenOrQuoted(input, index) {
74
+ if (input[index] === '"') {
75
+ return parseQuotedString(input, index);
76
+ }
77
+ const tokenResult = parseToken(input, index);
78
+ if (!tokenResult) {
79
+ return null;
80
+ }
81
+ return { value: tokenResult.token, index: tokenResult.index };
82
+ }
83
+ function parseToken68(input, index) {
84
+ let i = index;
85
+ let token = '';
86
+ while (i < input.length) {
87
+ const char = input[i];
88
+ if (char === ' ' || char === '\t' || char === ',') {
89
+ break;
90
+ }
91
+ token += char;
92
+ i++;
93
+ }
94
+ if (!token || !isToken68(token)) {
95
+ return null;
96
+ }
97
+ return { token, index: i };
98
+ }
99
+ function isNextParam(input, index) {
100
+ let i = skipOWS(input, index);
101
+ const tokenResult = parseToken(input, i);
102
+ if (!tokenResult) {
103
+ return false;
104
+ }
105
+ i = skipOWS(input, tokenResult.index);
106
+ return input[i] === '=';
107
+ }
108
+ function parseAuthParamsList(input) {
109
+ const params = [];
110
+ let index = 0;
111
+ while (index < input.length) {
112
+ index = skipOWS(input, index);
113
+ if (index >= input.length) {
114
+ break;
115
+ }
116
+ const nameResult = parseToken(input, index);
117
+ if (!nameResult) {
118
+ return null;
119
+ }
120
+ const name = nameResult.token.toLowerCase();
121
+ index = skipOWS(input, nameResult.index);
122
+ if (input[index] !== '=') {
123
+ return null;
124
+ }
125
+ index++;
126
+ index = skipOWS(input, index);
127
+ const valueResult = parseTokenOrQuoted(input, index);
128
+ if (!valueResult) {
129
+ return null;
130
+ }
131
+ params.push({ name, value: valueResult.value });
132
+ index = skipOWS(input, valueResult.index);
133
+ if (input[index] === ',') {
134
+ index++;
135
+ continue;
136
+ }
137
+ if (index < input.length) {
138
+ return null;
139
+ }
140
+ }
141
+ return params;
142
+ }
143
+ function parseChallenges(header) {
144
+ const challenges = [];
145
+ let index = 0;
146
+ while (index < header.length) {
147
+ index = skipOWS(header, index);
148
+ while (header[index] === ',') {
149
+ index++;
150
+ index = skipOWS(header, index);
151
+ }
152
+ if (index >= header.length) {
153
+ break;
154
+ }
155
+ const schemeResult = parseToken(header, index);
156
+ if (!schemeResult) {
157
+ return null;
158
+ }
159
+ const scheme = schemeResult.token;
160
+ index = skipOWS(header, schemeResult.index);
161
+ if (index >= header.length || header[index] === ',') {
162
+ challenges.push({ scheme });
163
+ if (header[index] === ',') {
164
+ index++;
165
+ }
166
+ continue;
167
+ }
168
+ const token68Result = parseToken68(header, index);
169
+ if (token68Result) {
170
+ challenges.push({ scheme, token68: token68Result.token });
171
+ index = skipOWS(header, token68Result.index);
172
+ if (header[index] === ',') {
173
+ index++;
174
+ }
175
+ continue;
176
+ }
177
+ const params = [];
178
+ while (index < header.length) {
179
+ index = skipOWS(header, index);
180
+ const nameResult = parseToken(header, index);
181
+ if (!nameResult) {
182
+ break;
183
+ }
184
+ const name = nameResult.token.toLowerCase();
185
+ index = skipOWS(header, nameResult.index);
186
+ if (header[index] !== '=') {
187
+ return null;
188
+ }
189
+ index++;
190
+ index = skipOWS(header, index);
191
+ const valueResult = parseTokenOrQuoted(header, index);
192
+ if (!valueResult) {
193
+ return null;
194
+ }
195
+ params.push({ name, value: valueResult.value });
196
+ index = skipOWS(header, valueResult.index);
197
+ if (header[index] === ',') {
198
+ const commaIndex = index;
199
+ index++;
200
+ if (isNextParam(header, index)) {
201
+ continue;
202
+ }
203
+ index = commaIndex;
204
+ break;
205
+ }
206
+ break;
207
+ }
208
+ challenges.push(params.length > 0 ? { scheme, params } : { scheme });
209
+ index = skipOWS(header, index);
210
+ if (header[index] === ',') {
211
+ index++;
212
+ }
213
+ }
214
+ return challenges;
215
+ }
216
+ function quoteAuthParamValue(value) {
217
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
218
+ return `"${escaped}"`;
219
+ }
220
+ function formatAuthParams(params) {
221
+ return params.map(param => `${param.name}=${quoteAuthParamValue(param.value)}`).join(', ');
222
+ }
223
+ function hasCtl(value) {
224
+ for (const char of value) {
225
+ const code = char.charCodeAt(0);
226
+ if (code <= 0x1f || code === 0x7f) {
227
+ return true;
228
+ }
229
+ }
230
+ return false;
231
+ }
232
+ /**
233
+ * Parse Authorization header into scheme + token68 or auth-params.
234
+ */
235
+ // RFC 7235 §2.1: credentials are token68 or auth-params.
236
+ export function parseAuthorization(header) {
237
+ if (!header || !header.trim()) {
238
+ return null;
239
+ }
240
+ const trimmed = header.trim();
241
+ const schemeResult = parseToken(trimmed, 0);
242
+ if (!schemeResult) {
243
+ return null;
244
+ }
245
+ const scheme = schemeResult.token;
246
+ let index = skipOWS(trimmed, schemeResult.index);
247
+ if (index >= trimmed.length) {
248
+ return { scheme };
249
+ }
250
+ const token68Result = parseToken68(trimmed, index);
251
+ if (token68Result && skipOWS(trimmed, token68Result.index) >= trimmed.length) {
252
+ return { scheme, token68: token68Result.token };
253
+ }
254
+ const params = parseAuthParamsList(trimmed.slice(index));
255
+ if (!params) {
256
+ return null;
257
+ }
258
+ return { scheme, params };
259
+ }
260
+ /**
261
+ * Format Authorization header from credentials.
262
+ */
263
+ // RFC 7235 §2.1: Authorization credentials formatting.
264
+ export function formatAuthorization(credentials) {
265
+ if (credentials.token68) {
266
+ return `${credentials.scheme} ${credentials.token68}`;
267
+ }
268
+ if (credentials.params && credentials.params.length > 0) {
269
+ return `${credentials.scheme} ${formatAuthParams(credentials.params)}`;
270
+ }
271
+ return credentials.scheme;
272
+ }
273
+ /**
274
+ * Parse WWW-Authenticate header into challenges.
275
+ */
276
+ // RFC 7235 §2.1: WWW-Authenticate challenge parsing.
277
+ export function parseWWWAuthenticate(header) {
278
+ if (!header || !header.trim()) {
279
+ return [];
280
+ }
281
+ const challenges = parseChallenges(header);
282
+ return challenges ?? [];
283
+ }
284
+ /**
285
+ * Format WWW-Authenticate header from challenges.
286
+ */
287
+ // RFC 7235 §2.1: WWW-Authenticate challenge formatting.
288
+ export function formatWWWAuthenticate(challenges) {
289
+ return challenges.map((challenge) => {
290
+ if (challenge.token68) {
291
+ return `${challenge.scheme} ${challenge.token68}`;
292
+ }
293
+ if (challenge.params && challenge.params.length > 0) {
294
+ return `${challenge.scheme} ${formatAuthParams(challenge.params)}`;
295
+ }
296
+ return challenge.scheme;
297
+ }).join(', ');
298
+ }
299
+ /**
300
+ * Parse Basic Authorization header.
301
+ */
302
+ // RFC 7617 §2: Basic credentials parsing.
303
+ export function parseBasicAuthorization(header, options = {}) {
304
+ const parsed = parseAuthorization(header);
305
+ if (!parsed || parsed.scheme.toLowerCase() !== 'basic' || !parsed.token68) {
306
+ return null;
307
+ }
308
+ if (!isToken68(parsed.token68)) {
309
+ return null;
310
+ }
311
+ const encoding = options.encoding ?? 'utf-8';
312
+ let decoded;
313
+ try {
314
+ decoded = Buffer.from(parsed.token68, 'base64').toString(encoding === 'latin1' ? 'latin1' : 'utf8');
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ const colonIndex = decoded.indexOf(':');
320
+ if (colonIndex === -1) {
321
+ return null;
322
+ }
323
+ const username = decoded.slice(0, colonIndex);
324
+ const password = decoded.slice(colonIndex + 1);
325
+ if (hasCtl(username) || hasCtl(password)) {
326
+ return null;
327
+ }
328
+ return { username, password, encoding };
329
+ }
330
+ /**
331
+ * Format Basic Authorization header.
332
+ */
333
+ // RFC 7617 §2: Basic credentials formatting.
334
+ export function formatBasicAuthorization(username, password, options = {}) {
335
+ if (username.includes(':') || hasCtl(username) || hasCtl(password)) {
336
+ return null;
337
+ }
338
+ const encoding = options.encoding ?? 'utf-8';
339
+ const token = Buffer
340
+ .from(`${username}:${password}`, encoding === 'latin1' ? 'latin1' : 'utf8')
341
+ .toString('base64');
342
+ return `Basic ${token}`;
343
+ }
344
+ /**
345
+ * Parse Basic WWW-Authenticate challenge.
346
+ */
347
+ // RFC 7617 §2, §2.1: Basic challenge parsing.
348
+ export function parseBasicChallenge(header) {
349
+ const challenges = parseWWWAuthenticate(header);
350
+ const challenge = challenges.find(entry => entry.scheme.toLowerCase() === 'basic');
351
+ if (!challenge || !challenge.params) {
352
+ return null;
353
+ }
354
+ const seen = new Set();
355
+ let realm;
356
+ let charset;
357
+ for (const param of challenge.params) {
358
+ const name = param.name.toLowerCase();
359
+ if (seen.has(name)) {
360
+ continue;
361
+ }
362
+ seen.add(name);
363
+ if (name === 'realm') {
364
+ realm = param.value;
365
+ }
366
+ else if (name === 'charset') {
367
+ if (param.value.toLowerCase() === 'utf-8') {
368
+ charset = 'UTF-8';
369
+ }
370
+ }
371
+ }
372
+ if (!realm) {
373
+ return null;
374
+ }
375
+ return { scheme: 'Basic', realm, charset };
376
+ }
377
+ /**
378
+ * Format Basic WWW-Authenticate challenge.
379
+ */
380
+ // RFC 7617 §2, §2.1: Basic challenge formatting.
381
+ export function formatBasicChallenge(realm, options = {}) {
382
+ const params = [{ name: 'realm', value: realm }];
383
+ if (options.charset === 'UTF-8') {
384
+ params.push({ name: 'charset', value: 'UTF-8' });
385
+ }
386
+ return `Basic ${formatAuthParams(params)}`;
387
+ }
388
+ /**
389
+ * Parse Bearer Authorization header.
390
+ */
391
+ // RFC 6750 §2.1: Bearer Authorization parsing.
392
+ export function parseBearerAuthorization(header) {
393
+ const parsed = parseAuthorization(header);
394
+ if (!parsed || parsed.scheme.toLowerCase() !== 'bearer' || !parsed.token68) {
395
+ return null;
396
+ }
397
+ if (!isB64Token(parsed.token68)) {
398
+ return null;
399
+ }
400
+ return parsed.token68;
401
+ }
402
+ /**
403
+ * Format Bearer Authorization header.
404
+ */
405
+ // RFC 6750 §2.1: Bearer Authorization formatting.
406
+ export function formatBearerAuthorization(token) {
407
+ if (!isB64Token(token)) {
408
+ return null;
409
+ }
410
+ return `Bearer ${token}`;
411
+ }
412
+ /**
413
+ * Parse Bearer WWW-Authenticate challenge.
414
+ */
415
+ // RFC 6750 §3: Bearer challenge parsing.
416
+ export function parseBearerChallenge(header) {
417
+ const challenges = parseWWWAuthenticate(header);
418
+ const challenge = challenges.find(entry => entry.scheme.toLowerCase() === 'bearer');
419
+ if (!challenge || !challenge.params || challenge.params.length === 0) {
420
+ return null;
421
+ }
422
+ const seen = new Set();
423
+ const extensions = {};
424
+ const result = {};
425
+ for (const param of challenge.params) {
426
+ const name = param.name.toLowerCase();
427
+ if (seen.has(name)) {
428
+ return null;
429
+ }
430
+ seen.add(name);
431
+ switch (name) {
432
+ case 'realm':
433
+ result.realm = param.value;
434
+ break;
435
+ case 'scope':
436
+ result.scope = param.value;
437
+ break;
438
+ case 'error':
439
+ if (BEARER_ERRORS.includes(param.value)) {
440
+ result.error = param.value;
441
+ }
442
+ break;
443
+ case 'error_description':
444
+ result.errorDescription = param.value;
445
+ break;
446
+ case 'error_uri':
447
+ result.errorUri = param.value;
448
+ break;
449
+ default:
450
+ extensions[name] = param.value;
451
+ break;
452
+ }
453
+ }
454
+ if (Object.keys(extensions).length > 0) {
455
+ result.params = extensions;
456
+ }
457
+ return result;
458
+ }
459
+ /**
460
+ * Format Bearer WWW-Authenticate challenge.
461
+ */
462
+ // RFC 6750 §3: Bearer challenge formatting.
463
+ export function formatBearerChallenge(params) {
464
+ const parts = [];
465
+ if (params.realm) {
466
+ parts.push({ name: 'realm', value: params.realm });
467
+ }
468
+ if (params.scope) {
469
+ parts.push({ name: 'scope', value: params.scope });
470
+ }
471
+ if (params.error) {
472
+ parts.push({ name: 'error', value: params.error });
473
+ }
474
+ if (params.errorDescription) {
475
+ parts.push({ name: 'error_description', value: params.errorDescription });
476
+ }
477
+ if (params.errorUri) {
478
+ parts.push({ name: 'error_uri', value: params.errorUri });
479
+ }
480
+ if (params.params) {
481
+ for (const [name, value] of Object.entries(params.params)) {
482
+ parts.push({ name, value });
483
+ }
484
+ }
485
+ if (parts.length === 0) {
486
+ return 'Bearer';
487
+ }
488
+ return `Bearer ${formatAuthParams(parts)}`;
489
+ }
490
+ // =============================================================================
491
+ // Digest Authentication (RFC 7616)
492
+ // =============================================================================
493
+ /**
494
+ * Supported Digest authentication algorithms.
495
+ * RFC 7616 §3.3: SHA-256 MUST be supported; MD5 for backward compatibility.
496
+ */
497
+ export const DIGEST_AUTH_ALGORITHMS = [
498
+ 'MD5',
499
+ 'MD5-sess',
500
+ 'SHA-256',
501
+ 'SHA-256-sess',
502
+ 'SHA-512-256',
503
+ 'SHA-512-256-sess',
504
+ ];
505
+ const NC_REGEX = /^[0-9a-fA-F]{8}$/;
506
+ /**
507
+ * Check if value is a valid Digest algorithm.
508
+ */
509
+ function isDigestAlgorithm(value) {
510
+ return DIGEST_AUTH_ALGORITHMS.includes(value);
511
+ }
512
+ /**
513
+ * Check if value is a valid qop.
514
+ */
515
+ function isDigestQop(value) {
516
+ return value === 'auth' || value === 'auth-int';
517
+ }
518
+ /**
519
+ * Get the Web Crypto algorithm name for a Digest algorithm.
520
+ * RFC 7616 §3.3.
521
+ */
522
+ function getHashAlgorithm(algorithm) {
523
+ const base = algorithm.replace(/-sess$/, '');
524
+ switch (base) {
525
+ case 'MD5':
526
+ return 'MD5';
527
+ case 'SHA-256':
528
+ return 'SHA-256';
529
+ case 'SHA-512-256':
530
+ return 'SHA-512';
531
+ default:
532
+ return 'SHA-256';
533
+ }
534
+ }
535
+ /**
536
+ * Compute a hash using Web Crypto.
537
+ * Returns lowercase hex string.
538
+ */
539
+ async function computeHash(algorithm, data) {
540
+ const encoder = new TextEncoder();
541
+ const bytes = typeof data === 'string' ? encoder.encode(data) : data;
542
+ // MD5 is not available in Web Crypto, use a simple implementation
543
+ if (algorithm === 'MD5') {
544
+ return computeMD5(bytes);
545
+ }
546
+ // Create a copy to ensure a plain ArrayBuffer
547
+ const buffer = new Uint8Array(bytes).buffer;
548
+ const hashBuffer = await crypto.subtle.digest(algorithm, buffer);
549
+ const hashArray = new Uint8Array(hashBuffer);
550
+ // For SHA-512-256, truncate to 256 bits (32 bytes)
551
+ const truncated = algorithm === 'SHA-512' ? hashArray.slice(0, 32) : hashArray;
552
+ return Array.from(truncated)
553
+ .map(b => b.toString(16).padStart(2, '0'))
554
+ .join('');
555
+ }
556
+ /**
557
+ * Simple MD5 implementation using Node.js crypto.
558
+ * Note: MD5 is deprecated and only included for backward compatibility.
559
+ */
560
+ function computeMD5(data) {
561
+ return createHash('md5').update(data).digest('hex');
562
+ }
563
+ /**
564
+ * Parse Digest WWW-Authenticate challenge.
565
+ * RFC 7616 §3.3.
566
+ */
567
+ // RFC 7616 §3.3: Digest challenge parsing.
568
+ export function parseDigestChallenge(challenge) {
569
+ if (challenge.scheme.toLowerCase() !== 'digest' || !challenge.params) {
570
+ return null;
571
+ }
572
+ const seen = new Map();
573
+ for (const param of challenge.params) {
574
+ const name = param.name.toLowerCase();
575
+ if (!seen.has(name)) {
576
+ seen.set(name, param.value);
577
+ }
578
+ }
579
+ const realm = seen.get('realm');
580
+ const nonce = seen.get('nonce');
581
+ // RFC 7616 §3.3: realm and nonce are required
582
+ if (!realm || !nonce) {
583
+ return null;
584
+ }
585
+ const result = {
586
+ scheme: 'Digest',
587
+ realm,
588
+ nonce,
589
+ };
590
+ // domain: space-separated list of URIs
591
+ const domain = seen.get('domain');
592
+ if (domain) {
593
+ result.domain = domain.split(/\s+/).filter(Boolean);
594
+ }
595
+ const opaque = seen.get('opaque');
596
+ if (opaque) {
597
+ result.opaque = opaque;
598
+ }
599
+ // RFC 7616 §3.3: stale is a flag (not quoted-string)
600
+ const stale = seen.get('stale');
601
+ if (stale && stale.toLowerCase() === 'true') {
602
+ result.stale = true;
603
+ }
604
+ // RFC 7616 §3.3: algorithm is a token (not quoted-string)
605
+ const algorithm = seen.get('algorithm');
606
+ if (algorithm && isDigestAlgorithm(algorithm)) {
607
+ result.algorithm = algorithm;
608
+ }
609
+ // RFC 7616 §3.3: qop-options is quoted-string with comma-separated values
610
+ const qop = seen.get('qop');
611
+ if (qop) {
612
+ const qopValues = qop.split(',').map(v => v.trim()).filter(isDigestQop);
613
+ if (qopValues.length > 0) {
614
+ result.qop = qopValues;
615
+ }
616
+ }
617
+ // RFC 7616 §3.3: charset is "UTF-8" (case-insensitive)
618
+ const charset = seen.get('charset');
619
+ if (charset && charset.toLowerCase() === 'utf-8') {
620
+ result.charset = 'UTF-8';
621
+ }
622
+ // RFC 7616 §3.3: userhash is a flag
623
+ const userhash = seen.get('userhash');
624
+ if (userhash && userhash.toLowerCase() === 'true') {
625
+ result.userhash = true;
626
+ }
627
+ return result;
628
+ }
629
+ /**
630
+ * Format Digest WWW-Authenticate challenge.
631
+ * RFC 7616 §3.3.
632
+ */
633
+ // RFC 7616 §3.3: Digest challenge formatting.
634
+ export function formatDigestChallenge(challenge) {
635
+ const parts = [];
636
+ // RFC 7616 §3.3: realm MUST be quoted-string
637
+ parts.push(`realm=${quoteAuthParamValue(challenge.realm)}`);
638
+ // RFC 7616 §3.3: domain is quoted-string with space-separated URIs
639
+ if (challenge.domain && challenge.domain.length > 0) {
640
+ parts.push(`domain=${quoteAuthParamValue(challenge.domain.join(' '))}`);
641
+ }
642
+ // RFC 7616 §3.3: nonce MUST be quoted-string
643
+ parts.push(`nonce=${quoteAuthParamValue(challenge.nonce)}`);
644
+ // RFC 7616 §3.3: opaque is quoted-string
645
+ if (challenge.opaque) {
646
+ parts.push(`opaque=${quoteAuthParamValue(challenge.opaque)}`);
647
+ }
648
+ // RFC 7616 §3.3: stale is token (not quoted)
649
+ if (challenge.stale) {
650
+ parts.push('stale=true');
651
+ }
652
+ // RFC 7616 §3.3: algorithm is token (not quoted)
653
+ if (challenge.algorithm) {
654
+ parts.push(`algorithm=${challenge.algorithm}`);
655
+ }
656
+ // RFC 7616 §3.3: qop-options is quoted-string
657
+ if (challenge.qop && challenge.qop.length > 0) {
658
+ parts.push(`qop=${quoteAuthParamValue(challenge.qop.join(', '))}`);
659
+ }
660
+ // RFC 7616 §3.3: charset is token
661
+ if (challenge.charset) {
662
+ parts.push(`charset=${challenge.charset}`);
663
+ }
664
+ // RFC 7616 §3.3: userhash is token
665
+ if (challenge.userhash) {
666
+ parts.push('userhash=true');
667
+ }
668
+ return `Digest ${parts.join(', ')}`;
669
+ }
670
+ /**
671
+ * Parse Digest Authorization credentials.
672
+ * RFC 7616 §3.4.
673
+ */
674
+ // RFC 7616 §3.4: Digest credentials parsing.
675
+ export function parseDigestAuthorization(credentials) {
676
+ if (credentials.scheme.toLowerCase() !== 'digest' || !credentials.params) {
677
+ return null;
678
+ }
679
+ const seen = new Map();
680
+ let hasUsername = false;
681
+ let hasUsernameStar = false;
682
+ for (const param of credentials.params) {
683
+ const name = param.name.toLowerCase();
684
+ if (name === 'username') {
685
+ hasUsername = true;
686
+ }
687
+ else if (name === 'username*') {
688
+ hasUsernameStar = true;
689
+ }
690
+ if (!seen.has(name)) {
691
+ seen.set(name, param.value);
692
+ }
693
+ }
694
+ // RFC 7616 §3.4: MUST NOT have both username and username*
695
+ if (hasUsername && hasUsernameStar) {
696
+ return null;
697
+ }
698
+ let username;
699
+ let usernameEncoded = false;
700
+ if (hasUsernameStar) {
701
+ // RFC 7616 §3.4: username* uses RFC 8187 encoding
702
+ const encoded = seen.get('username*');
703
+ if (!encoded) {
704
+ return null;
705
+ }
706
+ const decoded = decodeExtValue(encoded);
707
+ if (!decoded) {
708
+ return null;
709
+ }
710
+ username = decoded.value;
711
+ usernameEncoded = true;
712
+ }
713
+ else {
714
+ const u = seen.get('username');
715
+ if (!u) {
716
+ return null;
717
+ }
718
+ username = u;
719
+ }
720
+ const realm = seen.get('realm');
721
+ const uri = seen.get('uri');
722
+ const response = seen.get('response');
723
+ // RFC 7616 §3.4: realm, uri, and response are required
724
+ if (!realm || !uri || !response) {
725
+ return null;
726
+ }
727
+ const result = {
728
+ scheme: 'Digest',
729
+ username,
730
+ realm,
731
+ uri,
732
+ response,
733
+ };
734
+ if (usernameEncoded) {
735
+ result.usernameEncoded = true;
736
+ }
737
+ // RFC 7616 §3.4: algorithm is token (not quoted)
738
+ const algorithm = seen.get('algorithm');
739
+ if (algorithm && isDigestAlgorithm(algorithm)) {
740
+ result.algorithm = algorithm;
741
+ }
742
+ const cnonce = seen.get('cnonce');
743
+ if (cnonce) {
744
+ result.cnonce = cnonce;
745
+ }
746
+ const opaque = seen.get('opaque');
747
+ if (opaque) {
748
+ result.opaque = opaque;
749
+ }
750
+ // RFC 7616 §3.4: qop is token (not quoted)
751
+ const qop = seen.get('qop');
752
+ if (qop && isDigestQop(qop)) {
753
+ result.qop = qop;
754
+ }
755
+ // RFC 7616 §3.4: nc is exactly 8 hex digits
756
+ const nc = seen.get('nc');
757
+ if (nc) {
758
+ if (!NC_REGEX.test(nc)) {
759
+ return null;
760
+ }
761
+ result.nc = nc;
762
+ }
763
+ // RFC 7616 §3.4: userhash is token
764
+ const userhash = seen.get('userhash');
765
+ if (userhash && userhash.toLowerCase() === 'true') {
766
+ result.userhash = true;
767
+ }
768
+ return result;
769
+ }
770
+ /**
771
+ * Format Digest Authorization credentials.
772
+ * RFC 7616 §3.4.
773
+ */
774
+ // RFC 7616 §3.4: Digest credentials formatting.
775
+ export function formatDigestAuthorization(credentials) {
776
+ const parts = [];
777
+ // RFC 7616 §3.4: username or username* (quoted-string)
778
+ if (credentials.usernameEncoded) {
779
+ // RFC 8187 encoding
780
+ const encoded = encodeExtValue(credentials.username);
781
+ parts.push(`username*=${encoded}`);
782
+ }
783
+ else {
784
+ parts.push(`username=${quoteAuthParamValue(credentials.username)}`);
785
+ }
786
+ // RFC 7616 §3.4: realm is quoted-string
787
+ parts.push(`realm=${quoteAuthParamValue(credentials.realm)}`);
788
+ // RFC 7616 §3.4: uri is quoted-string
789
+ parts.push(`uri=${quoteAuthParamValue(credentials.uri)}`);
790
+ // RFC 7616 §3.4: response is quoted-string
791
+ parts.push(`response=${quoteAuthParamValue(credentials.response)}`);
792
+ // RFC 7616 §3.4: algorithm is token (not quoted)
793
+ if (credentials.algorithm) {
794
+ parts.push(`algorithm=${credentials.algorithm}`);
795
+ }
796
+ // RFC 7616 §3.4: cnonce is quoted-string
797
+ if (credentials.cnonce) {
798
+ parts.push(`cnonce=${quoteAuthParamValue(credentials.cnonce)}`);
799
+ }
800
+ // RFC 7616 §3.4: opaque is quoted-string
801
+ if (credentials.opaque) {
802
+ parts.push(`opaque=${quoteAuthParamValue(credentials.opaque)}`);
803
+ }
804
+ // RFC 7616 §3.4: qop is token (not quoted)
805
+ if (credentials.qop) {
806
+ parts.push(`qop=${credentials.qop}`);
807
+ }
808
+ // RFC 7616 §3.4: nc is exactly 8 hex digits (not quoted)
809
+ if (credentials.nc) {
810
+ parts.push(`nc=${credentials.nc}`);
811
+ }
812
+ // RFC 7616 §3.4: userhash is token
813
+ if (credentials.userhash) {
814
+ parts.push('userhash=true');
815
+ }
816
+ return `Digest ${parts.join(', ')}`;
817
+ }
818
+ /**
819
+ * Parse Authentication-Info header.
820
+ * RFC 7616 §3.5.
821
+ */
822
+ // RFC 7616 §3.5: Authentication-Info parsing.
823
+ export function parseDigestAuthenticationInfo(value) {
824
+ if (!value || !value.trim()) {
825
+ return null;
826
+ }
827
+ const params = parseAuthParamsList(value.trim());
828
+ if (!params) {
829
+ return null;
830
+ }
831
+ const result = {};
832
+ const seen = new Set();
833
+ for (const param of params) {
834
+ const name = param.name.toLowerCase();
835
+ if (seen.has(name)) {
836
+ continue;
837
+ }
838
+ seen.add(name);
839
+ switch (name) {
840
+ case 'nextnonce':
841
+ result.nextnonce = param.value;
842
+ break;
843
+ case 'qop':
844
+ if (isDigestQop(param.value)) {
845
+ result.qop = param.value;
846
+ }
847
+ break;
848
+ case 'rspauth':
849
+ result.rspauth = param.value;
850
+ break;
851
+ case 'cnonce':
852
+ result.cnonce = param.value;
853
+ break;
854
+ case 'nc':
855
+ if (NC_REGEX.test(param.value)) {
856
+ result.nc = param.value;
857
+ }
858
+ break;
859
+ }
860
+ }
861
+ return result;
862
+ }
863
+ /**
864
+ * Format Authentication-Info header.
865
+ * RFC 7616 §3.5.
866
+ */
867
+ // RFC 7616 §3.5: Authentication-Info formatting.
868
+ export function formatDigestAuthenticationInfo(info) {
869
+ const parts = [];
870
+ // RFC 7616 §3.5: nextnonce is quoted-string
871
+ if (info.nextnonce) {
872
+ parts.push(`nextnonce=${quoteAuthParamValue(info.nextnonce)}`);
873
+ }
874
+ // RFC 7616 §3.5: qop is token
875
+ if (info.qop) {
876
+ parts.push(`qop=${info.qop}`);
877
+ }
878
+ // RFC 7616 §3.5: rspauth is quoted-string
879
+ if (info.rspauth) {
880
+ parts.push(`rspauth=${quoteAuthParamValue(info.rspauth)}`);
881
+ }
882
+ // RFC 7616 §3.5: cnonce is quoted-string
883
+ if (info.cnonce) {
884
+ parts.push(`cnonce=${quoteAuthParamValue(info.cnonce)}`);
885
+ }
886
+ // RFC 7616 §3.5: nc is token (8 hex digits)
887
+ if (info.nc) {
888
+ parts.push(`nc=${info.nc}`);
889
+ }
890
+ return parts.join(', ');
891
+ }
892
+ /**
893
+ * Compute A1 value for Digest authentication.
894
+ * RFC 7616 §3.4.2.
895
+ *
896
+ * @param username - Username
897
+ * @param realm - Realm
898
+ * @param password - Password
899
+ * @param algorithm - Algorithm (session algorithms require nonce and cnonce)
900
+ * @param nonce - Server nonce (required for -sess algorithms)
901
+ * @param cnonce - Client nonce (required for -sess algorithms)
902
+ * @returns A1 hash value (hex string)
903
+ */
904
+ // RFC 7616 §3.4.2: A1 computation.
905
+ export async function computeA1(username, realm, password, algorithm = 'MD5', nonce, cnonce) {
906
+ const hashAlg = getHashAlgorithm(algorithm);
907
+ const isSession = algorithm.endsWith('-sess');
908
+ // RFC 7616 §3.4.2: A1 = username ":" realm ":" passwd
909
+ const a1Base = `${username}:${realm}:${password}`;
910
+ if (isSession) {
911
+ // RFC 7616 §3.4.2: For -sess algorithms:
912
+ // A1 = H(username ":" realm ":" passwd) ":" nonce ":" cnonce
913
+ if (!nonce || !cnonce) {
914
+ throw new Error('nonce and cnonce are required for session algorithms');
915
+ }
916
+ const h = await computeHash(hashAlg, a1Base);
917
+ return `${h}:${nonce}:${cnonce}`;
918
+ }
919
+ return a1Base;
920
+ }
921
+ /**
922
+ * Compute A2 value for Digest authentication.
923
+ * RFC 7616 §3.4.3.
924
+ *
925
+ * @param method - HTTP method
926
+ * @param uri - Request URI
927
+ * @param qop - Quality of protection (optional)
928
+ * @param entityBody - Entity body for auth-int (optional)
929
+ * @returns A2 string value
930
+ */
931
+ // RFC 7616 §3.4.3: A2 computation.
932
+ export function computeA2(method, uri, qop, _entityBody) {
933
+ // RFC 7616 §3.4.3: qop=auth: A2 = Method ":" request-uri
934
+ // RFC 7616 §3.4.3: qop=auth-int: A2 = Method ":" request-uri ":" H(entity-body)
935
+ // Note: auth-int is out of scope for this implementation
936
+ if (qop === 'auth-int') {
937
+ throw new Error('auth-int is not supported');
938
+ }
939
+ return `${method}:${uri}`;
940
+ }
941
+ /**
942
+ * Compute the Digest response value.
943
+ * RFC 7616 §3.4.1.
944
+ *
945
+ * @param options - Computation options
946
+ * @returns Response hash value (hex string)
947
+ */
948
+ // RFC 7616 §3.4.1: Response computation.
949
+ export async function computeDigestResponse(options) {
950
+ const { username, password, realm, method, uri, nonce, cnonce, nc, qop, algorithm = 'MD5', } = options;
951
+ const hashAlg = getHashAlgorithm(algorithm);
952
+ // Compute A1
953
+ const a1 = await computeA1(username, realm, password, algorithm, nonce, cnonce);
954
+ const ha1 = await computeHash(hashAlg, a1);
955
+ // Compute A2
956
+ const a2 = computeA2(method, uri, qop);
957
+ const ha2 = await computeHash(hashAlg, a2);
958
+ // RFC 7616 §3.4.1: response computation
959
+ let responseData;
960
+ if (qop) {
961
+ // RFC 7616 §3.4.1: With qop:
962
+ // response = KD(H(A1), unq(nonce) ":" nc ":" unq(cnonce) ":" unq(qop) ":" H(A2))
963
+ // KD(secret, data) = H(secret ":" data)
964
+ if (!cnonce || !nc) {
965
+ throw new Error('cnonce and nc are required when qop is specified');
966
+ }
967
+ responseData = `${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`;
968
+ }
969
+ else {
970
+ // RFC 7616 §3.4.1: Without qop (legacy):
971
+ // response = KD(H(A1), unq(nonce) ":" H(A2))
972
+ responseData = `${ha1}:${nonce}:${ha2}`;
973
+ }
974
+ return computeHash(hashAlg, responseData);
975
+ }
976
+ /**
977
+ * Hash a username for userhash support.
978
+ * RFC 7616 §3.4.4.
979
+ *
980
+ * @param username - Username to hash
981
+ * @param realm - Realm
982
+ * @param algorithm - Algorithm to use
983
+ * @returns Hashed username (hex string)
984
+ */
985
+ // RFC 7616 §3.4.4: Username hashing.
986
+ export async function hashDigestUsername(username, realm, algorithm = 'SHA-256') {
987
+ const hashAlg = getHashAlgorithm(algorithm);
988
+ const data = `${username}:${realm}`;
989
+ return computeHash(hashAlg, data);
990
+ }
991
+ //# sourceMappingURL=auth.js.map