@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,357 @@
1
+ /**
2
+ * Content negotiation utilities per RFC 7231.
3
+ * RFC 7231 §5.3.1, §5.3.2.
4
+ */
5
+ import { isEmptyHeader, splitQuotedValue, unquote, parseQValue } from './header-utils.js';
6
+ /**
7
+ * Media type constants mapping format names to MIME types.
8
+ */
9
+ export const MEDIA_TYPES = {
10
+ json: 'application/json',
11
+ csv: 'text/csv',
12
+ html: 'text/html',
13
+ text: 'text/plain',
14
+ xml: 'application/xml',
15
+ };
16
+ /**
17
+ * Reverse mapping from MIME type to format name.
18
+ */
19
+ export const MIME_TO_FORMAT = Object.fromEntries(Object.entries(MEDIA_TYPES).map(([format, mime]) => [mime, format]));
20
+ /**
21
+ * Parse an Accept header into a list of entries sorted by preference.
22
+ *
23
+ * RFC 7231 Section 5.3.2 defines Accept header format.
24
+ *
25
+ * @param header - The Accept header value
26
+ * @returns Sorted array of AcceptEntry (highest preference first)
27
+ *
28
+ * Sorting rules:
29
+ * 1. Higher q value first
30
+ * 2. More specific type beats wildcard (text/html > text/star > star/star)
31
+ * 3. More parameters beats fewer
32
+ *
33
+ * Invalid q-values are rejected and the entry is skipped.
34
+ *
35
+ * @example
36
+ * parseAccept("text/html, application/json;q=0.9, text/star;q=0.8")
37
+ * // Returns: [text/html q=1], [application/json q=0.9], [text/star q=0.8]
38
+ */
39
+ // RFC 7231 §5.3.2: Accept header parsing and sorting.
40
+ export function parseAccept(header) {
41
+ if (isEmptyHeader(header)) {
42
+ return [];
43
+ }
44
+ const entries = [];
45
+ const parts = splitQuotedValue(header, ',');
46
+ for (const part of parts) {
47
+ const trimmed = part.trim();
48
+ if (!trimmed)
49
+ continue;
50
+ const entry = parseMediaRange(trimmed);
51
+ if (entry) {
52
+ entries.push(entry);
53
+ }
54
+ }
55
+ // Sort by preference
56
+ entries.sort((a, b) => {
57
+ // 1. Higher q value first
58
+ if (a.q !== b.q) {
59
+ return b.q - a.q;
60
+ }
61
+ // 2. More specific type beats wildcard
62
+ const specA = getSpecificity(a);
63
+ const specB = getSpecificity(b);
64
+ if (specA !== specB) {
65
+ return specB - specA;
66
+ }
67
+ // 3. More parameters beats fewer
68
+ return b.params.size - a.params.size;
69
+ });
70
+ return entries;
71
+ }
72
+ /**
73
+ * Parse a single media range with optional parameters.
74
+ */
75
+ function parseMediaRange(range) {
76
+ const parts = splitQuotedValue(range, ';').map(p => p.trim());
77
+ const mediaType = parts[0];
78
+ if (!mediaType || !mediaType.includes('/')) {
79
+ return null;
80
+ }
81
+ const [type, subtype] = mediaType.split('/');
82
+ if (!type || !subtype) {
83
+ return null;
84
+ }
85
+ let q = 1.0;
86
+ const params = new Map();
87
+ let seenQ = false;
88
+ for (let i = 1; i < parts.length; i++) {
89
+ const param = parts[i];
90
+ if (!param)
91
+ continue;
92
+ const eqIndex = param.indexOf('=');
93
+ if (eqIndex === -1)
94
+ continue;
95
+ const key = param.slice(0, eqIndex).trim().toLowerCase();
96
+ const value = unquote(param.slice(eqIndex + 1).trim());
97
+ if (key === 'q') {
98
+ const parsed = parseQValue(value);
99
+ if (parsed === null) {
100
+ return null;
101
+ }
102
+ q = parsed;
103
+ seenQ = true;
104
+ }
105
+ else {
106
+ if (!seenQ) {
107
+ params.set(key, value);
108
+ }
109
+ }
110
+ }
111
+ return {
112
+ type: type.toLowerCase(),
113
+ subtype: subtype.toLowerCase(),
114
+ q,
115
+ params,
116
+ };
117
+ }
118
+ /**
119
+ * Get specificity score for sorting.
120
+ * - exact match with params: 4
121
+ * - exact match no params: 3
122
+ * - type wildcard (text/star): 2
123
+ * - full wildcard (star/star): 1
124
+ */
125
+ function getSpecificity(entry) {
126
+ if (entry.type === '*' && entry.subtype === '*') {
127
+ return 1;
128
+ }
129
+ if (entry.subtype === '*') {
130
+ return 2;
131
+ }
132
+ // Exact match
133
+ return entry.params.size > 0 ? 4 : 3;
134
+ }
135
+ /**
136
+ * Check if an Accept entry matches a media type.
137
+ * Returns specificity score if match, 0 if no match.
138
+ */
139
+ function matchesMediaType(entry, mediaType) {
140
+ const mimeType = MEDIA_TYPES[mediaType];
141
+ return matchesMimeType(entry, mimeType);
142
+ }
143
+ /**
144
+ * Check if an Accept entry matches a MIME type string.
145
+ * Returns specificity score if match, 0 if no match.
146
+ */
147
+ function matchesMimeType(entry, mimeType) {
148
+ const parsed = parseMimeTypeWithParams(mimeType);
149
+ if (!parsed) {
150
+ return 0;
151
+ }
152
+ const { type, subtype, params } = parsed;
153
+ if (!paramsMatch(entry.params, params)) {
154
+ return 0;
155
+ }
156
+ if (!type || !subtype) {
157
+ return 0;
158
+ }
159
+ // Full wildcard matches anything
160
+ if (entry.type === '*' && entry.subtype === '*') {
161
+ return 1;
162
+ }
163
+ // Type must match for type wildcard
164
+ if (entry.type === type && entry.subtype === '*') {
165
+ return 2;
166
+ }
167
+ // Exact match
168
+ if (entry.type === type && entry.subtype === subtype) {
169
+ return entry.params.size > 0 ? 4 : 3;
170
+ }
171
+ return 0;
172
+ }
173
+ function parseMimeTypeWithParams(mimeType) {
174
+ const parts = splitQuotedValue(mimeType, ';').map(p => p.trim());
175
+ const mediaType = parts[0];
176
+ if (!mediaType || !mediaType.includes('/')) {
177
+ return null;
178
+ }
179
+ const [type, subtype] = mediaType.split('/');
180
+ if (!type || !subtype) {
181
+ return null;
182
+ }
183
+ const params = new Map();
184
+ for (let i = 1; i < parts.length; i++) {
185
+ const param = parts[i];
186
+ if (!param)
187
+ continue;
188
+ const eqIndex = param.indexOf('=');
189
+ if (eqIndex === -1)
190
+ continue;
191
+ const key = param.slice(0, eqIndex).trim().toLowerCase();
192
+ const value = unquote(param.slice(eqIndex + 1).trim());
193
+ if (key) {
194
+ params.set(key, value);
195
+ }
196
+ }
197
+ return {
198
+ type: type.toLowerCase(),
199
+ subtype: subtype.toLowerCase(),
200
+ params,
201
+ };
202
+ }
203
+ function paramsMatch(required, candidate) {
204
+ if (required.size === 0) {
205
+ return true;
206
+ }
207
+ for (const [key, value] of required.entries()) {
208
+ if (!candidate.has(key)) {
209
+ return false;
210
+ }
211
+ if (candidate.get(key) !== value) {
212
+ return false;
213
+ }
214
+ }
215
+ return true;
216
+ }
217
+ /**
218
+ * Negotiate the best media type from supported options.
219
+ *
220
+ * @param input - The Request object, Accept header string, or null/undefined
221
+ * @param supported - Array of supported media types (MIME strings like 'application/json')
222
+ * @returns Best matching media type or null if none acceptable
223
+ *
224
+ * Matching rules:
225
+ * - Exact match: "application/json" matches 'application/json'
226
+ * - Type wildcard: "text/\*" matches 'text/html', 'text/csv'
227
+ * - Full wildcard: "\*\/\*" matches anything
228
+ * - q=0 means explicitly not acceptable
229
+ */
230
+ // RFC 7231 §5.3.2: Media type selection based on Accept.
231
+ export function negotiate(input, supported) {
232
+ if (supported.length === 0) {
233
+ return null;
234
+ }
235
+ let acceptHeader;
236
+ if (input instanceof Request) {
237
+ acceptHeader = input.headers.get('Accept');
238
+ }
239
+ else {
240
+ acceptHeader = input ?? null;
241
+ }
242
+ // Empty or missing Accept header means accept anything
243
+ if (!acceptHeader || !acceptHeader.trim()) {
244
+ return supported[0] ?? null;
245
+ }
246
+ const entries = parseAccept(acceptHeader);
247
+ // No valid entries means accept anything
248
+ if (entries.length === 0) {
249
+ return supported[0] ?? null;
250
+ }
251
+ // Find best match
252
+ let bestMatch = null;
253
+ let bestScore = 0;
254
+ let bestQ = 0;
255
+ for (const entry of entries) {
256
+ // Skip explicitly not acceptable
257
+ if (entry.q === 0) {
258
+ continue;
259
+ }
260
+ for (const mimeType of supported) {
261
+ const score = matchesMimeType(entry, mimeType);
262
+ if (score > 0) {
263
+ // Prefer higher q, then higher specificity
264
+ if (entry.q > bestQ || (entry.q === bestQ && score > bestScore)) {
265
+ bestMatch = mimeType;
266
+ bestScore = score;
267
+ bestQ = entry.q;
268
+ }
269
+ }
270
+ }
271
+ }
272
+ return bestMatch;
273
+ }
274
+ /**
275
+ * Get response format from request or Accept header string.
276
+ * Only distinguishes between 'json' and 'csv'.
277
+ * Defaults to 'json' if no preference or Accept missing.
278
+ *
279
+ * @param input - The Request object or Accept header string
280
+ * @returns 'json', 'csv', or null when neither is acceptable
281
+ */
282
+ // RFC 7231 §5.3.2: Response format selection from Accept.
283
+ export function getResponseFormat(input) {
284
+ let acceptHeader;
285
+ if (input instanceof Request) {
286
+ acceptHeader = input.headers.get('Accept');
287
+ }
288
+ else {
289
+ acceptHeader = input ?? null;
290
+ }
291
+ if (!acceptHeader) {
292
+ return 'json';
293
+ }
294
+ if (!acceptHeader.trim()) {
295
+ return 'json';
296
+ }
297
+ const entries = parseAccept(acceptHeader);
298
+ if (entries.length === 0) {
299
+ return 'json';
300
+ }
301
+ const best = negotiate(acceptHeader, ['application/json', 'text/csv']);
302
+ if (!best) {
303
+ return null;
304
+ }
305
+ return best === 'text/csv' ? 'csv' : 'json';
306
+ }
307
+ /**
308
+ * Escape a value for CSV output.
309
+ */
310
+ function escapeCSVValue(value) {
311
+ if (value === null || value === undefined) {
312
+ return '';
313
+ }
314
+ let str;
315
+ if (typeof value === 'object') {
316
+ str = JSON.stringify(value);
317
+ }
318
+ else {
319
+ str = String(value);
320
+ }
321
+ // Check if quoting is needed
322
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
323
+ // Escape quotes by doubling them
324
+ return '"' + str.replace(/"/g, '""') + '"';
325
+ }
326
+ return str;
327
+ }
328
+ /**
329
+ * Convert an array of objects to CSV format.
330
+ *
331
+ * @param data - Array of objects (all should have same keys)
332
+ * @returns CSV string with headers
333
+ *
334
+ * Rules:
335
+ * - First row is headers (keys from first object)
336
+ * - Values containing commas, quotes, or newlines are quoted
337
+ * - Quotes in values are escaped as ""
338
+ * - null/undefined become empty string
339
+ * - Objects/arrays are JSON stringified
340
+ */
341
+ // Non-RFC: CSV formatting helper.
342
+ export function toCSV(data) {
343
+ if (data.length === 0) {
344
+ return '';
345
+ }
346
+ // Get headers from first object
347
+ const firstRow = data[0];
348
+ const headers = Object.keys(firstRow);
349
+ // Build header row
350
+ const headerRow = headers.map(h => escapeCSVValue(h)).join(',');
351
+ // Build data rows
352
+ const dataRows = data.map(row => {
353
+ return headers.map(header => escapeCSVValue(row[header])).join(',');
354
+ });
355
+ return [headerRow, ...dataRows].join('\n');
356
+ }
357
+ //# sourceMappingURL=negotiate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negotiate.js","sourceRoot":"","sources":["../src/negotiate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Pagination helpers with RFC 8288 Link header usage.
3
+ * RFC 8288 §3, §3.3.
4
+ */
5
+ import type { PaginationResult, DecodedCursor, PaginationLinks } from './types.js';
6
+ /** Default number of items per page */
7
+ export declare const DEFAULT_LIMIT = 50;
8
+ /** Maximum allowed items per page */
9
+ export declare const MAX_LIMIT = 100;
10
+ /**
11
+ * Decode a cursor string to get the offset.
12
+ * Cursor format: base64(JSON({ offset: number }))
13
+ *
14
+ * @param cursor - The cursor string
15
+ * @returns DecodedCursor or null if invalid
16
+ */
17
+ export declare function decodeCursor(cursor: string): DecodedCursor | null;
18
+ /**
19
+ * Encode an offset into a cursor string.
20
+ *
21
+ * @param offset - The offset to encode
22
+ * @returns Base64-encoded cursor
23
+ */
24
+ export declare function encodeCursor(offset: number): string;
25
+ /**
26
+ * Parse pagination parameters from URL query string.
27
+ *
28
+ * Supports two modes:
29
+ * 1. Cursor-based: ?cursor=<base64>
30
+ * 2. Offset-based: ?limit=N&offset=N
31
+ *
32
+ * Also extracts sort parameter if present.
33
+ *
34
+ * @param url - The URL to parse
35
+ * @param options - Configuration options
36
+ * @returns PaginationParams on success, PaginationError on failure
37
+ *
38
+ * Error cases:
39
+ * - Invalid cursor format
40
+ * - Invalid cursor payload (not JSON, missing offset, negative offset)
41
+ * - Non-numeric limit/offset
42
+ * - Negative offset
43
+ */
44
+ export declare function parsePaginationParams(url: URL, options?: {
45
+ defaultLimit?: number;
46
+ maxLimit?: number;
47
+ }): PaginationResult;
48
+ /**
49
+ * Calculate the last page offset given total count and limit.
50
+ */
51
+ export declare function lastPageOffset(totalCount: number, limit: number): number;
52
+ /**
53
+ * Check if we're on the first page.
54
+ */
55
+ export declare function isFirstPage(offset: number): boolean;
56
+ /**
57
+ * Check if we're on the last page.
58
+ */
59
+ export declare function isLastPage(offset: number, limit: number, totalCount: number): boolean;
60
+ /**
61
+ * Build pagination links for a result set.
62
+ *
63
+ * @param baseUrl - The base URL (without pagination params)
64
+ * @param totalCount - Total number of items
65
+ * @param limit - Items per page
66
+ * @param currentOffset - Current offset
67
+ * @param extraParams - Additional URL params to preserve
68
+ * @returns PaginationLinks object
69
+ *
70
+ * Links:
71
+ * - self: Current page
72
+ * - first: First page (offset=0)
73
+ * - last: Last page
74
+ * - next: Next page (omitted on last page)
75
+ * - prev: Previous page (omitted on first page)
76
+ *
77
+ * All links use cursor-based pagination for consistency.
78
+ */
79
+ export declare function buildPaginationLinks(baseUrl: string, totalCount: number, limit: number, currentOffset: number, extraParams?: URLSearchParams): PaginationLinks;
80
+ //# sourceMappingURL=pagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAGR,gBAAgB,EAChB,aAAa,EACb,eAAe,EAClB,MAAM,YAAY,CAAC;AAGpB,uCAAuC;AACvC,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC,qCAAqC;AACrC,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B;;;;;;GAMG;AAEH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAqBjE;AAED;;;;;GAKG;AAEH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEH,wBAAgB,qBAAqB,CACjC,GAAG,EAAE,GAAG,EACR,OAAO,CAAC,EAAE;IACN,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,GACF,gBAAgB,CAiDlB;AAED;;GAEG;AAEH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKxE;AAED;;GAEG;AAEH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;GAEG;AAEH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAKrF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,eAAe,GAC9B,eAAe,CAmCjB"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Pagination helpers with RFC 8288 Link header usage.
3
+ * RFC 8288 §3, §3.3.
4
+ */
5
+ /** Default number of items per page */
6
+ export const DEFAULT_LIMIT = 50;
7
+ /** Maximum allowed items per page */
8
+ export const MAX_LIMIT = 100;
9
+ /**
10
+ * Decode a cursor string to get the offset.
11
+ * Cursor format: base64(JSON({ offset: number }))
12
+ *
13
+ * @param cursor - The cursor string
14
+ * @returns DecodedCursor or null if invalid
15
+ */
16
+ // Non-RFC: Cursor decoding helper.
17
+ export function decodeCursor(cursor) {
18
+ try {
19
+ const decoded = atob(cursor);
20
+ const parsed = JSON.parse(decoded);
21
+ if (typeof parsed.offset !== 'number') {
22
+ return null;
23
+ }
24
+ if (parsed.offset < 0) {
25
+ return null;
26
+ }
27
+ if (!Number.isInteger(parsed.offset)) {
28
+ return null;
29
+ }
30
+ return { offset: parsed.offset };
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * Encode an offset into a cursor string.
38
+ *
39
+ * @param offset - The offset to encode
40
+ * @returns Base64-encoded cursor
41
+ */
42
+ // Non-RFC: Cursor encoding helper.
43
+ export function encodeCursor(offset) {
44
+ return btoa(JSON.stringify({ offset }));
45
+ }
46
+ /**
47
+ * Parse pagination parameters from URL query string.
48
+ *
49
+ * Supports two modes:
50
+ * 1. Cursor-based: ?cursor=<base64>
51
+ * 2. Offset-based: ?limit=N&offset=N
52
+ *
53
+ * Also extracts sort parameter if present.
54
+ *
55
+ * @param url - The URL to parse
56
+ * @param options - Configuration options
57
+ * @returns PaginationParams on success, PaginationError on failure
58
+ *
59
+ * Error cases:
60
+ * - Invalid cursor format
61
+ * - Invalid cursor payload (not JSON, missing offset, negative offset)
62
+ * - Non-numeric limit/offset
63
+ * - Negative offset
64
+ */
65
+ // Non-RFC: Pagination query parsing.
66
+ export function parsePaginationParams(url, options) {
67
+ const defaultLimit = options?.defaultLimit ?? DEFAULT_LIMIT;
68
+ const maxLimit = options?.maxLimit ?? MAX_LIMIT;
69
+ const cursorParam = url.searchParams.get('cursor');
70
+ const limitParam = url.searchParams.get('limit');
71
+ const offsetParam = url.searchParams.get('offset');
72
+ const sortParam = url.searchParams.get('sort');
73
+ let offset;
74
+ let limit;
75
+ // Cursor takes precedence over limit/offset
76
+ if (cursorParam !== null) {
77
+ const decoded = decodeCursor(cursorParam);
78
+ if (decoded === null) {
79
+ return { error: 'invalid_cursor' };
80
+ }
81
+ offset = decoded.offset;
82
+ }
83
+ else if (offsetParam !== null) {
84
+ const parsedOffset = Number(offsetParam);
85
+ if (Number.isNaN(parsedOffset)) {
86
+ return { error: 'invalid_offset' };
87
+ }
88
+ if (parsedOffset < 0) {
89
+ return { error: 'invalid_offset' };
90
+ }
91
+ offset = Math.floor(parsedOffset);
92
+ }
93
+ else {
94
+ offset = 0;
95
+ }
96
+ if (limitParam !== null) {
97
+ const parsedLimit = Number(limitParam);
98
+ if (Number.isNaN(parsedLimit)) {
99
+ return { error: 'invalid_limit' };
100
+ }
101
+ limit = Math.min(Math.max(1, Math.floor(parsedLimit)), maxLimit);
102
+ }
103
+ else {
104
+ limit = defaultLimit;
105
+ }
106
+ const result = { limit, offset };
107
+ if (sortParam !== null) {
108
+ result.sort = sortParam;
109
+ }
110
+ return result;
111
+ }
112
+ /**
113
+ * Calculate the last page offset given total count and limit.
114
+ */
115
+ // Non-RFC: Pagination helper.
116
+ export function lastPageOffset(totalCount, limit) {
117
+ if (totalCount <= 0) {
118
+ return 0;
119
+ }
120
+ return Math.floor((totalCount - 1) / limit) * limit;
121
+ }
122
+ /**
123
+ * Check if we're on the first page.
124
+ */
125
+ // Non-RFC: Pagination helper.
126
+ export function isFirstPage(offset) {
127
+ return offset === 0;
128
+ }
129
+ /**
130
+ * Check if we're on the last page.
131
+ */
132
+ // Non-RFC: Pagination helper.
133
+ export function isLastPage(offset, limit, totalCount) {
134
+ if (totalCount === 0) {
135
+ return true;
136
+ }
137
+ return offset + limit >= totalCount;
138
+ }
139
+ /**
140
+ * Build pagination links for a result set.
141
+ *
142
+ * @param baseUrl - The base URL (without pagination params)
143
+ * @param totalCount - Total number of items
144
+ * @param limit - Items per page
145
+ * @param currentOffset - Current offset
146
+ * @param extraParams - Additional URL params to preserve
147
+ * @returns PaginationLinks object
148
+ *
149
+ * Links:
150
+ * - self: Current page
151
+ * - first: First page (offset=0)
152
+ * - last: Last page
153
+ * - next: Next page (omitted on last page)
154
+ * - prev: Previous page (omitted on first page)
155
+ *
156
+ * All links use cursor-based pagination for consistency.
157
+ */
158
+ // RFC 8288 §3, §3.3: Link relations for pagination.
159
+ export function buildPaginationLinks(baseUrl, totalCount, limit, currentOffset, extraParams) {
160
+ const buildUrl = (offset) => {
161
+ const url = new URL(baseUrl);
162
+ url.searchParams.set('cursor', encodeCursor(offset));
163
+ url.searchParams.set('limit', String(limit));
164
+ if (extraParams) {
165
+ extraParams.forEach((value, key) => {
166
+ if (key !== 'cursor' && key !== 'limit' && key !== 'offset') {
167
+ url.searchParams.set(key, value);
168
+ }
169
+ });
170
+ }
171
+ return url.toString();
172
+ };
173
+ const lastOffset = lastPageOffset(totalCount, limit);
174
+ const links = {
175
+ self: buildUrl(currentOffset),
176
+ first: buildUrl(0),
177
+ last: buildUrl(lastOffset)
178
+ };
179
+ if (!isLastPage(currentOffset, limit, totalCount)) {
180
+ links.next = buildUrl(currentOffset + limit);
181
+ }
182
+ if (!isFirstPage(currentOffset)) {
183
+ const prevOffset = Math.max(0, currentOffset - limit);
184
+ links.prev = buildUrl(prevOffset);
185
+ }
186
+ return links;
187
+ }
188
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Prefer / Preference-Applied utilities per RFC 7240.
3
+ * RFC 7240 §2, §3.
4
+ */
5
+ import type { PreferMap, PreferToken } from './types.js';
6
+ /**
7
+ * Parse a Prefer header into a map of preference tokens.
8
+ */
9
+ export declare function parsePrefer(header: string): PreferMap;
10
+ /**
11
+ * Format Prefer header from tokens.
12
+ */
13
+ export declare function formatPrefer(preferences: PreferMap | PreferToken[]): string;
14
+ /**
15
+ * Format Preference-Applied header.
16
+ */
17
+ export declare function formatPreferenceApplied(preferences: PreferMap | string[]): string;
18
+ //# sourceMappingURL=prefer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer.d.ts","sourceRoot":"","sources":["../src/prefer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAe,MAAM,YAAY,CAAC;AAGtE;;GAEG;AAEH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAkDrD;AAED;;GAEG;AAEH,wBAAgB,YAAY,CAAC,WAAW,EAAE,SAAS,GAAG,WAAW,EAAE,GAAG,MAAM,CAiB3E;AAED;;GAEG;AAEH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,GAAG,MAAM,EAAE,GAAG,MAAM,CAYjF"}