@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,168 @@
1
+ /**
2
+ * Parse a sort string into field/direction pairs.
3
+ *
4
+ * Format: "field1,-field2,field3"
5
+ * - Prefix with '-' for descending
6
+ * - No prefix for ascending
7
+ * - Multiple fields separated by comma
8
+ *
9
+ * @param sort - The sort string
10
+ * @returns Array of SortField objects
11
+ */
12
+ export function parseSortString(sort) {
13
+ if (!sort || !sort.trim()) {
14
+ return [];
15
+ }
16
+ return sort
17
+ .split(',')
18
+ .map(field => field.trim())
19
+ .filter(field => field.length > 0)
20
+ .map(field => {
21
+ if (field.startsWith('-')) {
22
+ return {
23
+ field: field.slice(1),
24
+ direction: 'desc'
25
+ };
26
+ }
27
+ return {
28
+ field,
29
+ direction: 'asc'
30
+ };
31
+ });
32
+ }
33
+ /**
34
+ * Get a nested value from an object using dot notation.
35
+ *
36
+ * @param obj - The object to access
37
+ * @param path - Dot-separated path (e.g., "user.name")
38
+ * @returns The value at the path, or undefined if not found
39
+ */
40
+ function getNestedValue(obj, path) {
41
+ const parts = path.split('.');
42
+ let current = obj;
43
+ for (const part of parts) {
44
+ if (current === null || current === undefined) {
45
+ return undefined;
46
+ }
47
+ if (typeof current !== 'object') {
48
+ return undefined;
49
+ }
50
+ current = current[part];
51
+ }
52
+ return current;
53
+ }
54
+ /**
55
+ * Compare two values for sorting.
56
+ * Internal helper exposed for custom comparators.
57
+ *
58
+ * @param a - First value
59
+ * @param b - Second value
60
+ * @param direction - Sort direction
61
+ * @returns Comparison result (-1, 0, 1)
62
+ */
63
+ export function compareValues(a, b, direction) {
64
+ const multiplier = direction === 'desc' ? -1 : 1;
65
+ // Handle null/undefined - always sort last
66
+ const aIsNullish = a === null || a === undefined;
67
+ const bIsNullish = b === null || b === undefined;
68
+ if (aIsNullish && bIsNullish) {
69
+ return 0;
70
+ }
71
+ if (aIsNullish) {
72
+ return 1; // a sorts last regardless of direction
73
+ }
74
+ if (bIsNullish) {
75
+ return -1; // b sorts last regardless of direction
76
+ }
77
+ // Handle dates
78
+ if (a instanceof Date && b instanceof Date) {
79
+ return multiplier * (a.getTime() - b.getTime());
80
+ }
81
+ // Handle booleans (false < true)
82
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
83
+ if (a === b)
84
+ return 0;
85
+ return multiplier * (a ? 1 : -1);
86
+ }
87
+ // Handle numbers
88
+ if (typeof a === 'number' && typeof b === 'number') {
89
+ return multiplier * (a - b);
90
+ }
91
+ // Handle strings (case-insensitive)
92
+ if (typeof a === 'string' && typeof b === 'string') {
93
+ const comparison = a.toLowerCase().localeCompare(b.toLowerCase());
94
+ return multiplier * comparison;
95
+ }
96
+ // Mixed types - convert to strings for comparison
97
+ const aStr = String(a).toLowerCase();
98
+ const bStr = String(b).toLowerCase();
99
+ return multiplier * aStr.localeCompare(bStr);
100
+ }
101
+ /**
102
+ * Apply sorting to an array of objects.
103
+ *
104
+ * @param data - Array to sort (not mutated, returns new array)
105
+ * @param sort - Sort string (e.g., "-date,title")
106
+ * @returns Sorted array
107
+ *
108
+ * Sorting rules:
109
+ * - undefined/null values sort last
110
+ * - Strings are compared case-insensitively
111
+ * - Numbers compared numerically
112
+ * - Dates compared by timestamp
113
+ * - Booleans: false < true
114
+ * - Multiple sort fields applied in order
115
+ */
116
+ export function applySorting(data, sort) {
117
+ if (!sort || !sort.trim()) {
118
+ return data;
119
+ }
120
+ const sortFields = parseSortString(sort);
121
+ if (sortFields.length === 0) {
122
+ return data;
123
+ }
124
+ // Create shallow copy to avoid mutating input
125
+ const result = [...data];
126
+ result.sort((a, b) => {
127
+ for (const { field, direction } of sortFields) {
128
+ const aValue = getNestedValue(a, field);
129
+ const bValue = getNestedValue(b, field);
130
+ const comparison = compareValues(aValue, bValue, direction);
131
+ if (comparison !== 0) {
132
+ return comparison;
133
+ }
134
+ }
135
+ return 0;
136
+ });
137
+ return result;
138
+ }
139
+ /**
140
+ * Validate that sort fields exist in the data.
141
+ *
142
+ * @param sort - Sort string
143
+ * @param allowedFields - Array of allowed field names
144
+ * @returns true if all fields are allowed, false otherwise
145
+ */
146
+ export function validateSortFields(sort, allowedFields) {
147
+ const sortFields = parseSortString(sort);
148
+ if (sortFields.length === 0) {
149
+ return true;
150
+ }
151
+ const allowedSet = new Set(allowedFields);
152
+ return sortFields.every(({ field }) => allowedSet.has(field));
153
+ }
154
+ /**
155
+ * Build a sort string from field/direction pairs.
156
+ * Inverse of parseSortString.
157
+ *
158
+ * @param fields - Array of SortField objects
159
+ * @returns Sort string
160
+ */
161
+ export function buildSortString(fields) {
162
+ return fields
163
+ .map(({ field, direction }) => {
164
+ return direction === 'desc' ? `-${field}` : field;
165
+ })
166
+ .join(',');
167
+ }
168
+ //# sourceMappingURL=sorting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sorting.js","sourceRoot":"","sources":["../src/sorting.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Structured Field Values per RFC 8941.
3
+ * RFC 8941 §3, §4.
4
+ */
5
+ import type { SfItem, SfList, SfDictionary } from './types.js';
6
+ /**
7
+ * Parse a Structured Field List.
8
+ */
9
+ export declare function parseSfList(value: string): SfList | null;
10
+ /**
11
+ * Parse a Structured Field Dictionary.
12
+ */
13
+ export declare function parseSfDict(value: string): SfDictionary | null;
14
+ /**
15
+ * Parse a Structured Field Item.
16
+ */
17
+ export declare function parseSfItem(value: string): SfItem | null;
18
+ /**
19
+ * Serialize a Structured Field List.
20
+ */
21
+ export declare function serializeSfList(list: SfList): string;
22
+ /**
23
+ * Serialize a Structured Field Dictionary.
24
+ */
25
+ export declare function serializeSfDict(dict: SfDictionary): string;
26
+ /**
27
+ * Serialize a Structured Field Item.
28
+ */
29
+ export declare function serializeSfItem(item: SfItem): string;
30
+ //# sourceMappingURL=structured-fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-fields.d.ts","sourceRoot":"","sources":["../src/structured-fields.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAc,MAAM,EAAe,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAiXxF;;GAEG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUxD;AAED;;GAEG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAU9D;AAED;;GAEG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUxD;AAqED;;GAEG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOpD;AAED;;GAEG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAoB1D;AAED;;GAEG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Structured Field Values per RFC 8941.
3
+ * RFC 8941 §3, §4.
4
+ */
5
+ import { Buffer } from 'node:buffer';
6
+ // RFC 8941 §3.3.4: sf-token allows ALPHA (case-insensitive), digits, and tchar plus : and /
7
+ const TOKEN_RE = /^[A-Za-z*][A-Za-z0-9!#$%&'*+\-.^_`|~:\/]*$/;
8
+ const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
9
+ const KEY_RE = /^[a-z*][a-z0-9_\-\.\*]*$/;
10
+ class Parser {
11
+ input;
12
+ index;
13
+ constructor(input) {
14
+ this.input = input;
15
+ this.index = 0;
16
+ }
17
+ eof() {
18
+ return this.index >= this.input.length;
19
+ }
20
+ peek() {
21
+ return this.input[this.index] ?? '';
22
+ }
23
+ consume() {
24
+ return this.input[this.index++] ?? '';
25
+ }
26
+ skipOWS() {
27
+ while (!this.eof()) {
28
+ const char = this.peek();
29
+ if (char === ' ' || char === '\t') {
30
+ this.consume();
31
+ }
32
+ else {
33
+ break;
34
+ }
35
+ }
36
+ }
37
+ isAtEnd() {
38
+ this.skipOWS();
39
+ return this.eof();
40
+ }
41
+ parseItem() {
42
+ const value = this.parseBareItem();
43
+ if (value === null) {
44
+ return null;
45
+ }
46
+ const params = this.parseParameters();
47
+ return params ? { value, params } : { value };
48
+ }
49
+ parseInnerList() {
50
+ if (this.peek() !== '(') {
51
+ return null;
52
+ }
53
+ this.consume();
54
+ const items = [];
55
+ this.skipOWS();
56
+ while (!this.eof() && this.peek() !== ')') {
57
+ const item = this.parseItem();
58
+ if (!item) {
59
+ return null;
60
+ }
61
+ items.push(item);
62
+ this.skipOWS();
63
+ }
64
+ if (this.peek() !== ')') {
65
+ return null;
66
+ }
67
+ this.consume();
68
+ const params = this.parseParameters();
69
+ return params ? { items, params } : { items };
70
+ }
71
+ parseList() {
72
+ const list = [];
73
+ while (true) {
74
+ this.skipOWS();
75
+ if (this.eof()) {
76
+ break;
77
+ }
78
+ const member = this.peek() === '(' ? this.parseInnerList() : this.parseItem();
79
+ if (!member) {
80
+ return null;
81
+ }
82
+ list.push(member);
83
+ this.skipOWS();
84
+ if (this.eof()) {
85
+ break;
86
+ }
87
+ if (this.peek() === ',') {
88
+ this.consume();
89
+ continue;
90
+ }
91
+ return null;
92
+ }
93
+ return list;
94
+ }
95
+ parseDictionary() {
96
+ const dict = {};
97
+ while (true) {
98
+ this.skipOWS();
99
+ if (this.eof()) {
100
+ break;
101
+ }
102
+ const key = this.parseKey();
103
+ if (!key) {
104
+ return null;
105
+ }
106
+ this.skipOWS();
107
+ let value;
108
+ if (this.peek() === '=') {
109
+ this.consume();
110
+ this.skipOWS();
111
+ if (this.peek() === '(') {
112
+ const list = this.parseInnerList();
113
+ if (!list) {
114
+ return null;
115
+ }
116
+ value = list;
117
+ }
118
+ else {
119
+ const item = this.parseItem();
120
+ if (!item) {
121
+ return null;
122
+ }
123
+ value = item;
124
+ }
125
+ }
126
+ else {
127
+ const params = this.parseParameters();
128
+ value = params ? { value: true, params } : { value: true };
129
+ }
130
+ dict[key] = value;
131
+ this.skipOWS();
132
+ if (this.eof()) {
133
+ break;
134
+ }
135
+ if (this.peek() === ',') {
136
+ this.consume();
137
+ continue;
138
+ }
139
+ return null;
140
+ }
141
+ return dict;
142
+ }
143
+ parseParameters() {
144
+ const params = {};
145
+ while (true) {
146
+ this.skipOWS();
147
+ if (this.peek() !== ';') {
148
+ break;
149
+ }
150
+ this.consume();
151
+ this.skipOWS();
152
+ const key = this.parseKey();
153
+ if (!key) {
154
+ return null;
155
+ }
156
+ let value = true;
157
+ this.skipOWS();
158
+ if (this.peek() === '=') {
159
+ this.consume();
160
+ this.skipOWS();
161
+ const bare = this.parseBareItem();
162
+ if (bare === null) {
163
+ return null;
164
+ }
165
+ value = bare;
166
+ }
167
+ params[key] = value;
168
+ }
169
+ return Object.keys(params).length > 0 ? params : null;
170
+ }
171
+ parseBareItem() {
172
+ if (this.eof()) {
173
+ return null;
174
+ }
175
+ const char = this.peek();
176
+ if (char === '"') {
177
+ return this.parseString();
178
+ }
179
+ if (char === '?') {
180
+ return this.parseBoolean();
181
+ }
182
+ if (char === ':') {
183
+ return this.parseByteSequence();
184
+ }
185
+ if (char === '-' || (char >= '0' && char <= '9')) {
186
+ return this.parseNumber();
187
+ }
188
+ return this.parseToken();
189
+ }
190
+ parseKey() {
191
+ let key = '';
192
+ while (!this.eof()) {
193
+ const char = this.peek();
194
+ if (!/[a-z0-9_\-\.\*]/.test(char)) {
195
+ break;
196
+ }
197
+ key += char;
198
+ this.consume();
199
+ }
200
+ if (!key || !KEY_RE.test(key)) {
201
+ return null;
202
+ }
203
+ return key;
204
+ }
205
+ // RFC 8941 §3.3.3: String bare item.
206
+ parseString() {
207
+ if (this.consume() !== '"') {
208
+ return null;
209
+ }
210
+ let result = '';
211
+ while (!this.eof()) {
212
+ const char = this.consume();
213
+ if (char === '"') {
214
+ return result;
215
+ }
216
+ if (char === '\\') {
217
+ if (this.eof()) {
218
+ return null;
219
+ }
220
+ const escapedChar = this.consume();
221
+ // RFC 8941 §3.3.3: Only \" and \\ are valid escape sequences.
222
+ if (escapedChar !== '"' && escapedChar !== '\\') {
223
+ return null;
224
+ }
225
+ result += escapedChar;
226
+ }
227
+ else {
228
+ // RFC 8941 §3.3.3: Validate unescaped chars are printable ASCII (excluding " and \).
229
+ const code = char.charCodeAt(0);
230
+ if (code < 0x20 || code > 0x7E || code === 0x22 || code === 0x5C) {
231
+ return null;
232
+ }
233
+ result += char;
234
+ }
235
+ }
236
+ return null;
237
+ }
238
+ // RFC 8941 §3.3.6: Boolean bare item.
239
+ parseBoolean() {
240
+ if (this.consume() !== '?') {
241
+ return null;
242
+ }
243
+ const char = this.consume();
244
+ if (char === '1') {
245
+ return true;
246
+ }
247
+ if (char === '0') {
248
+ return false;
249
+ }
250
+ return null;
251
+ }
252
+ // RFC 8941 §3.3.5: Byte sequence bare item.
253
+ parseByteSequence() {
254
+ if (this.consume() !== ':') {
255
+ return null;
256
+ }
257
+ let base64 = '';
258
+ while (!this.eof()) {
259
+ const char = this.consume();
260
+ if (char === ':') {
261
+ try {
262
+ if (base64.includes('\n') || base64.includes('\r')) {
263
+ return null;
264
+ }
265
+ if (!BASE64_RE.test(base64)) {
266
+ return null;
267
+ }
268
+ if (base64.length % 4 !== 0) {
269
+ return null;
270
+ }
271
+ const buffer = Buffer.from(base64, 'base64');
272
+ return new Uint8Array(buffer);
273
+ }
274
+ catch {
275
+ return null;
276
+ }
277
+ }
278
+ base64 += char;
279
+ }
280
+ return null;
281
+ }
282
+ // RFC 8941 §3.3.1: Numeric bare item.
283
+ parseNumber() {
284
+ const remaining = this.input.slice(this.index);
285
+ const match = remaining.match(/^(-?)(\d+)(?:\.(\d{1,3}))?/);
286
+ if (!match) {
287
+ return null;
288
+ }
289
+ const integerPart = match[2] ?? '';
290
+ const fractionalPart = match[3];
291
+ if (fractionalPart !== undefined) {
292
+ if (integerPart.length > 12) {
293
+ return null;
294
+ }
295
+ if (fractionalPart.length > 3) {
296
+ return null;
297
+ }
298
+ }
299
+ else if (integerPart.length > 15) {
300
+ return null;
301
+ }
302
+ this.index += match[0].length;
303
+ const value = Number(match[0]);
304
+ if (!Number.isFinite(value)) {
305
+ return null;
306
+ }
307
+ return value;
308
+ }
309
+ // RFC 8941 §3.3.4: Token parsing.
310
+ parseToken() {
311
+ let token = '';
312
+ while (!this.eof()) {
313
+ const char = this.peek();
314
+ // RFC 8941 §3.3.4: tchar plus ":" and "/"
315
+ if (!/[A-Za-z0-9!#$%&'*+\-.^_`|~:\/]/.test(char)) {
316
+ break;
317
+ }
318
+ token += char;
319
+ this.consume();
320
+ }
321
+ if (!token || !TOKEN_RE.test(token)) {
322
+ return null;
323
+ }
324
+ return token;
325
+ }
326
+ }
327
+ /**
328
+ * Parse a Structured Field List.
329
+ */
330
+ // RFC 8941 §3.1: Structured Field List.
331
+ export function parseSfList(value) {
332
+ const parser = new Parser(value);
333
+ const list = parser.parseList();
334
+ if (!list) {
335
+ return null;
336
+ }
337
+ if (!parser.isAtEnd()) {
338
+ return null;
339
+ }
340
+ return list;
341
+ }
342
+ /**
343
+ * Parse a Structured Field Dictionary.
344
+ */
345
+ // RFC 8941 §3.2: Structured Field Dictionary.
346
+ export function parseSfDict(value) {
347
+ const parser = new Parser(value);
348
+ const dict = parser.parseDictionary();
349
+ if (!dict) {
350
+ return null;
351
+ }
352
+ if (!parser.isAtEnd()) {
353
+ return null;
354
+ }
355
+ return dict;
356
+ }
357
+ /**
358
+ * Parse a Structured Field Item.
359
+ */
360
+ // RFC 8941 §3.3: Structured Field Item.
361
+ export function parseSfItem(value) {
362
+ const parser = new Parser(value.trim());
363
+ const item = parser.parseItem();
364
+ if (!item) {
365
+ return null;
366
+ }
367
+ if (!parser.isAtEnd()) {
368
+ return null;
369
+ }
370
+ return item;
371
+ }
372
+ // RFC 8941 §4: Structured Field serialization.
373
+ function serializeBareItem(value) {
374
+ if (typeof value === 'boolean') {
375
+ return value ? '?1' : '?0';
376
+ }
377
+ if (typeof value === 'number') {
378
+ if (!Number.isFinite(value)) {
379
+ throw new Error('Invalid numeric structured field value');
380
+ }
381
+ if (Number.isInteger(value)) {
382
+ // RFC 8941 §4.1.4: Integer range check.
383
+ if (value < -999_999_999_999_999 || value > 999_999_999_999_999) {
384
+ throw new Error('Integer out of range for structured field');
385
+ }
386
+ return String(value);
387
+ }
388
+ let encoded = value.toFixed(3);
389
+ encoded = encoded.replace(/0+$/, '');
390
+ if (encoded.endsWith('.')) {
391
+ encoded = encoded.slice(0, -1);
392
+ }
393
+ return encoded;
394
+ }
395
+ if (value instanceof Uint8Array) {
396
+ const base64 = Buffer.from(value).toString('base64');
397
+ return `:${base64}:`;
398
+ }
399
+ if (TOKEN_RE.test(value)) {
400
+ return value;
401
+ }
402
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
403
+ return `"${escaped}"`;
404
+ }
405
+ function serializeParams(params) {
406
+ if (!params) {
407
+ return '';
408
+ }
409
+ const parts = [];
410
+ for (const [key, value] of Object.entries(params)) {
411
+ if (value === true) {
412
+ parts.push(`;${key}`);
413
+ }
414
+ else {
415
+ parts.push(`;${key}=${serializeBareItem(value)}`);
416
+ }
417
+ }
418
+ return parts.join('');
419
+ }
420
+ function serializeItem(item) {
421
+ const base = serializeBareItem(item.value);
422
+ return base + serializeParams(item.params);
423
+ }
424
+ function serializeInnerList(list) {
425
+ const items = list.items.map(serializeItem).join(' ');
426
+ return `(${items})${serializeParams(list.params)}`;
427
+ }
428
+ /**
429
+ * Serialize a Structured Field List.
430
+ */
431
+ // RFC 8941 §4: Structured Field List serialization.
432
+ export function serializeSfList(list) {
433
+ return list.map(member => {
434
+ if ('items' in member) {
435
+ return serializeInnerList(member);
436
+ }
437
+ return serializeItem(member);
438
+ }).join(', ');
439
+ }
440
+ /**
441
+ * Serialize a Structured Field Dictionary.
442
+ */
443
+ // RFC 8941 §4: Structured Field Dictionary serialization.
444
+ export function serializeSfDict(dict) {
445
+ const parts = [];
446
+ for (const [key, value] of Object.entries(dict)) {
447
+ if ('items' in value) {
448
+ parts.push(`${key}=${serializeInnerList(value)}`);
449
+ continue;
450
+ }
451
+ const item = value;
452
+ const isBareTrue = item.value === true && !item.params;
453
+ if (isBareTrue) {
454
+ parts.push(key);
455
+ continue;
456
+ }
457
+ parts.push(`${key}=${serializeItem(item)}`);
458
+ }
459
+ return parts.join(', ');
460
+ }
461
+ /**
462
+ * Serialize a Structured Field Item.
463
+ */
464
+ // RFC 8941 §4: Structured Field Item serialization.
465
+ export function serializeSfItem(item) {
466
+ return serializeItem(item);
467
+ }
468
+ //# sourceMappingURL=structured-fields.js.map