@openclaw/lobster 2026.7.2-beta.7 → 2026.7.33

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 (45) hide show
  1. package/dist/index.js +149 -9
  2. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/.github/dependabot.yml +2 -42
  3. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/.github/workflows/ci.yml +3 -51
  4. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/.github/workflows/package-manager-ci.yml +24 -0
  5. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/README.md +9 -9
  6. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/index.js +246 -41
  7. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/lib/schemes.js +9 -4
  8. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/lib/utils.js +319 -157
  9. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/package.json +4 -4
  10. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/component-safe-serialization.test.js +163 -0
  11. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/equal.test.js +31 -54
  12. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
  13. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
  14. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/ipv6-validation.test.js +124 -0
  15. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
  16. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
  17. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/parse.test.js +2 -0
  18. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
  19. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
  20. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/security-normalization.test.js +101 -0
  21. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/security.test.js +211 -3
  22. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/uri-js.test.js +1 -1
  23. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
  24. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/util.test.js +0 -9
  25. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
  26. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/tsconfig.json +7 -8
  27. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/types/index.d.ts +9 -0
  28. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/types/index.test-d.ts +17 -0
  29. package/node_modules/typebox/build/compile/validator.d.mts +4 -1
  30. package/node_modules/typebox/build/guard/guard.d.mts +1 -1
  31. package/node_modules/typebox/build/guard/guard.mjs +2 -5
  32. package/node_modules/typebox/build/guard/string.mjs +4 -16
  33. package/node_modules/typebox/build/schema/engine/_functions.mjs +15 -34
  34. package/node_modules/typebox/build/type/action/readonly_object.d.mts +0 -5
  35. package/node_modules/typebox/build/type/action/readonly_object.mjs +0 -8
  36. package/node_modules/typebox/build/typebox.d.mts +1 -1
  37. package/node_modules/typebox/build/typebox.mjs +1 -1
  38. package/node_modules/typebox/package.json +1 -1
  39. package/node_modules/typebox/readme.md +32 -29
  40. package/openclaw.plugin.json +0 -1
  41. package/package.json +5 -5
  42. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/benchmark/encoder.mjs +0 -54
  43. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/scheme-normalization.test.js +0 -16
  44. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/security-utf8-encoding.test.js +0 -0
  45. package/node_modules/@clawdbot/lobster/node_modules/fast-uri/types/index.tst.ts +0 -15
@@ -6,6 +6,9 @@ const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\d
6
6
  /** @type {(value: string) => boolean} */
7
7
  const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u)
8
8
 
9
+ /** @type {(value: string) => boolean} */
10
+ const isPort = RegExp.prototype.test.bind(/^\d*$/u)
11
+
9
12
  /** @type {(value: string) => boolean} */
10
13
  const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
11
14
 
@@ -13,10 +16,36 @@ const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
13
16
  const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
14
17
 
15
18
  /** @type {(value: string) => boolean} */
16
- const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
19
+ const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u)
20
+
21
+ /** @type {(value: string) => boolean} */
22
+ const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u)
17
23
 
18
24
  /** @type {(value: string) => boolean} */
19
- const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/?]$/iu)
25
+ const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u)
26
+
27
+ const BYTE_HEX = new Array(256)
28
+ {
29
+ const HEX_DIGITS = '0123456789ABCDEF'
30
+ for (let i = 0; i < 256; i++) {
31
+ BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF]
32
+ }
33
+ }
34
+ function percentEncodeNonAscii (cp) {
35
+ if (cp < 0x800) {
36
+ return BYTE_HEX[0xC0 | (cp >> 6)] +
37
+ BYTE_HEX[0x80 | (cp & 0x3F)]
38
+ }
39
+ if (cp < 0x10000) {
40
+ return BYTE_HEX[0xE0 | (cp >> 12)] +
41
+ BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
42
+ BYTE_HEX[0x80 | (cp & 0x3F)]
43
+ }
44
+ return BYTE_HEX[0xF0 | (cp >> 18)] +
45
+ BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
46
+ BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
47
+ BYTE_HEX[0x80 | (cp & 0x3F)]
48
+ }
20
49
 
21
50
  /**
22
51
  * @param {Array<string>} input
@@ -49,12 +78,14 @@ function stringArrayToHexStripped (input) {
49
78
  return acc
50
79
  }
51
80
 
52
- /**
53
- * @typedef {Object} GetIPV6Result
54
- * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
55
- * @property {string} address - The parsed IPv6 address.
56
- * @property {string} [zone] - The zone identifier, if present.
57
- */
81
+ /** @type {(value: string) => boolean} */
82
+ const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/)
83
+
84
+ /** @type {(value: string) => boolean} */
85
+ const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/)
86
+
87
+ /** @type {(value: string) => boolean} */
88
+ const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/)
58
89
 
59
90
  /**
60
91
  * @param {string} value
@@ -63,88 +94,104 @@ function stringArrayToHexStripped (input) {
63
94
  const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
64
95
 
65
96
  /**
66
- * @param {Array<string>} buffer
97
+ * @param {string} zone
67
98
  * @returns {boolean}
68
99
  */
69
- function consumeIsZone (buffer) {
70
- buffer.length = 0
100
+ function isZoneIdentifier (zone) {
101
+ if (zone.length === 0) return false
102
+
103
+ for (let i = 0; i < zone.length; i++) {
104
+ if (isZoneCharacter(zone[i])) continue
105
+ if (zone[i] === '%' && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
106
+ i += 2
107
+ continue
108
+ }
109
+ return false
110
+ }
111
+
71
112
  return true
72
113
  }
73
114
 
74
115
  /**
75
- * @param {Array<string>} buffer
76
- * @param {Array<string>} address
77
- * @param {GetIPV6Result} output
78
- * @returns {boolean}
116
+ * Compresses the longest run of zero hextets to "::" per RFC 5952. A run of a
117
+ * single zero hextet is left uncompressed. On ties the leftmost run wins.
118
+ *
119
+ * @param {string[]} hextets
120
+ * @returns {string}
79
121
  */
80
- function consumeHextets (buffer, address, output) {
81
- if (buffer.length) {
82
- const hex = stringArrayToHexStripped(buffer)
83
- if (hex !== '') {
84
- address.push(hex)
122
+ function compressIPv6ZeroRun (hextets) {
123
+ let bestStart = -1
124
+ let bestLength = 0
125
+ let runStart = -1
126
+ let runLength = 0
127
+ for (let i = 0; i < hextets.length; i++) {
128
+ if (hextets[i] === '0') {
129
+ if (runStart === -1) runStart = i
130
+ runLength++
131
+ if (runLength > bestLength) {
132
+ bestLength = runLength
133
+ bestStart = runStart
134
+ }
85
135
  } else {
86
- output.error = true
87
- return false
136
+ runStart = -1
137
+ runLength = 0
88
138
  }
89
- buffer.length = 0
90
139
  }
91
- return true
140
+
141
+ if (bestLength < 2) return hextets.join(':')
142
+
143
+ const head = hextets.slice(0, bestStart).join(':')
144
+ const tail = hextets.slice(bestStart + bestLength).join(':')
145
+ return head + '::' + tail
92
146
  }
93
147
 
94
148
  /**
149
+ * Validates an IPv6 address against the alternatives in RFC 3986 section
150
+ * 3.2.2 and returns the same address with leading hextet zeroes removed.
151
+ * An embedded IPv4 address counts as two hextets and is only valid at the end.
152
+ *
95
153
  * @param {string} input
96
- * @returns {GetIPV6Result}
154
+ * @returns {string|undefined}
97
155
  */
98
- function getIPV6 (input) {
99
- let tokenCount = 0
100
- const output = { error: false, address: '', zone: '' }
101
- /** @type {Array<string>} */
102
- const address = []
103
- /** @type {Array<string>} */
104
- const buffer = []
105
- let endipv6Encountered = false
106
- let endIpv6 = false
107
-
108
- let consume = consumeHextets
156
+ function normalizeIPv6Address (input) {
157
+ const compression = input.indexOf('::')
158
+ if (compression !== -1 && input.indexOf('::', compression + 1) !== -1) return undefined
159
+
160
+ const left = compression === -1 ? input.split(':') : input.slice(0, compression).split(':')
161
+ const right = compression === -1 ? [] : input.slice(compression + 2).split(':')
162
+ if (compression !== -1) {
163
+ if (left.length === 1 && left[0] === '') left.length = 0
164
+ if (right.length === 1 && right[0] === '') right.length = 0
165
+ }
109
166
 
110
- for (let i = 0; i < input.length; i++) {
111
- const cursor = input[i]
112
- if (cursor === '[' || cursor === ']') { continue }
113
- if (cursor === ':') {
114
- if (endipv6Encountered === true) {
115
- endIpv6 = true
116
- }
117
- if (!consume(buffer, address, output)) { break }
118
- if (++tokenCount > 7) {
119
- // not valid
120
- output.error = true
121
- break
122
- }
123
- if (i > 0 && input[i - 1] === ':') {
124
- endipv6Encountered = true
125
- }
126
- address.push(':')
127
- continue
128
- } else if (cursor === '%') {
129
- if (!consume(buffer, address, output)) { break }
130
- // switch to zone detection
131
- consume = consumeIsZone
132
- } else {
133
- buffer.push(cursor)
167
+ const parts = left.concat(right)
168
+ let hextetCount = 0
169
+ for (let i = 0; i < parts.length; i++) {
170
+ const part = parts[i]
171
+ if (part === '') return undefined
172
+
173
+ if (part.indexOf('.') !== -1) {
174
+ if (i !== parts.length - 1 || (compression !== -1 && right.length === 0) || !isIPv4(part)) return undefined
175
+ hextetCount += 2
134
176
  continue
135
177
  }
178
+
179
+ if (!isHextet(part)) return undefined
180
+ parts[i] = parseInt(part, 16).toString(16)
181
+ hextetCount++
136
182
  }
137
- if (buffer.length) {
138
- if (consume === consumeIsZone) {
139
- output.zone = buffer.join('')
140
- } else if (endIpv6) {
141
- address.push(buffer.join(''))
142
- } else {
143
- address.push(stringArrayToHexStripped(buffer))
144
- }
183
+
184
+ if (compression === -1) {
185
+ if (hextetCount !== 8) return undefined
186
+ return compressIPv6ZeroRun(parts)
145
187
  }
146
- output.address = address.join('')
147
- return output
188
+ if (hextetCount >= 8) return undefined
189
+
190
+ // expand "::" then re-compress the longest run for a canonical result
191
+ const expanded = parts.slice(0, left.length)
192
+ for (let i = hextetCount; i < 8; i++) expanded.push('0')
193
+ for (let i = left.length; i < parts.length; i++) expanded.push(parts[i])
194
+ return compressIPv6ZeroRun(expanded)
148
195
  }
149
196
 
150
197
  /**
@@ -152,26 +199,49 @@ function getIPV6 (input) {
152
199
  * @property {string} host - The normalized host.
153
200
  * @property {string} [escapedHost] - The escaped host.
154
201
  * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
202
+ * @property {boolean} [isIPVFuture] - Indicates if the host is an IPvFuture literal.
203
+ * @property {boolean} [error] - Indicates if a bracketed IP literal is malformed.
155
204
  */
156
205
 
157
206
  /**
207
+ * Validates and normalizes a bracketed IP literal. Raw zone separators remain
208
+ * accepted for backwards compatibility, while encoded separators and zone
209
+ * contents follow RFC 6874.
210
+ *
158
211
  * @param {string} host
159
212
  * @returns {NormalizeIPv6Result}
160
213
  */
161
214
  function normalizeIPv6 (host) {
162
- if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
163
- const ipv6 = getIPV6(host)
164
-
165
- if (!ipv6.error) {
166
- let newHost = ipv6.address
167
- let escapedHost = ipv6.address
168
- if (ipv6.zone) {
169
- newHost += '%' + ipv6.zone
170
- escapedHost += '%25' + ipv6.zone
171
- }
172
- return { host: newHost, isIPV6: true, escapedHost }
173
- } else {
174
- return { host, isIPV6: false }
215
+ const bracketed = host[0] === '[' && host[host.length - 1] === ']'
216
+ const hasBracket = host[0] === '[' || host[host.length - 1] === ']'
217
+ if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }
218
+
219
+ let input = bracketed ? host.slice(1, -1) : host
220
+ if (bracketed && isIPvFuture(input)) {
221
+ input = input.toLowerCase()
222
+ return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }
223
+ }
224
+
225
+ if (findToken(input, ':') < 2) {
226
+ return { host, isIPV6: false, error: bracketed }
227
+ }
228
+
229
+ let zoneIdentifier = ''
230
+ const zoneSeparator = input.indexOf('%')
231
+ if (zoneSeparator !== -1) {
232
+ const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === '%25' ? 3 : 1
233
+ zoneIdentifier = input.slice(zoneSeparator + separatorLength)
234
+ if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }
235
+ input = input.slice(0, zoneSeparator)
236
+ }
237
+
238
+ const address = normalizeIPv6Address(input)
239
+ if (address === undefined) return { host, isIPV6: false, error: true }
240
+
241
+ return {
242
+ host: address + (zoneIdentifier ? '%' + zoneIdentifier : ''),
243
+ escapedHost: address + (zoneIdentifier ? '%25' + zoneIdentifier : ''),
244
+ isIPV6: true
175
245
  }
176
246
  }
177
247
 
@@ -221,7 +291,7 @@ function removeDotSegments (path) {
221
291
  continue
222
292
  }
223
293
  } else if (input[0] === '/') {
224
- if (input[1] === '.') {
294
+ if (input[1] === '.' || input[1] === '/') {
225
295
  output.push('/')
226
296
  break
227
297
  }
@@ -297,7 +367,7 @@ function reescapeHostDelimiters (host, isIP) {
297
367
 
298
368
  /**
299
369
  * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
300
- * Reserved delimiters such as `%2F` and `%2E` stay escaped.
370
+ * Reserved delimiters such as `%2F` stay escaped; `%2E` is unreserved.
301
371
  *
302
372
  * @param {string} input
303
373
  * @param {boolean} [decodeUnreserved=false]
@@ -334,55 +404,6 @@ function normalizePercentEncoding (input, decodeUnreserved = false) {
334
404
  return output
335
405
  }
336
406
 
337
- /** Pre-built `%XX` strings for every byte. Avoids per-call hex math/concat. */
338
- const BYTE_HEX = new Array(256)
339
- {
340
- const HEX_DIGITS = '0123456789ABCDEF'
341
- for (let i = 0; i < 256; i++) {
342
- BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF]
343
- }
344
- }
345
-
346
- /**
347
- * Mirrors the pass-through set of the deprecated `escape()` global:
348
- * `A-Z a-z 0-9 * + - . / @ _`. Keeps drop-in semantics for ASCII input.
349
- *
350
- * @param {number} cp
351
- * @returns {boolean}
352
- */
353
- function isEscapeSafe (cp) {
354
- return (
355
- (cp >= 0x30 && cp <= 0x39) ||
356
- (cp >= 0x41 && cp <= 0x5A) ||
357
- (cp >= 0x61 && cp <= 0x7A) ||
358
- cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
359
- cp === 0x2F || cp === 0x40 || cp === 0x5F
360
- )
361
- }
362
-
363
- /**
364
- * RFC 3986 percent-encode a non-ASCII Unicode code point as UTF-8 bytes.
365
- * Caller handles the ASCII branch inline for speed.
366
- *
367
- * @param {number} cp - Unicode code point (>= 0x80, <= 0x10FFFF)
368
- * @returns {string}
369
- */
370
- function percentEncodeNonAscii (cp) {
371
- if (cp < 0x800) {
372
- return BYTE_HEX[0xC0 | (cp >> 6)] +
373
- BYTE_HEX[0x80 | (cp & 0x3F)]
374
- }
375
- if (cp < 0x10000) {
376
- return BYTE_HEX[0xE0 | (cp >> 12)] +
377
- BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
378
- BYTE_HEX[0x80 | (cp & 0x3F)]
379
- }
380
- return BYTE_HEX[0xF0 | (cp >> 18)] +
381
- BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
382
- BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
383
- BYTE_HEX[0x80 | (cp & 0x3F)]
384
- }
385
-
386
407
  /**
387
408
  * Normalizes path data without turning reserved escapes into live path syntax.
388
409
  * Valid escapes are uppercased, raw unsafe characters are escaped, and only
@@ -438,6 +459,149 @@ function normalizePathEncoding (input) {
438
459
  return output
439
460
  }
440
461
 
462
+ /**
463
+ * Serializes a path without rewriting reserved data. Raw RFC 3986 path
464
+ * characters remain literal, valid escapes are preserved and uppercased, and
465
+ * everything else is UTF-8 percent-encoded. In a path-noscheme, a colon in the
466
+ * first segment must be escaped so the result cannot be parsed as a scheme.
467
+ *
468
+ * @param {string} input
469
+ * @param {boolean} [pathNoScheme=false]
470
+ * @returns {string}
471
+ */
472
+ function serializePathEncoding (input, pathNoScheme = false) {
473
+ let output = ''
474
+ let firstSegment = pathNoScheme && input[0] !== '/'
475
+
476
+ for (let i = 0; i < input.length; i++) {
477
+ const ch = input[i]
478
+ if (ch === '%' && i + 2 < input.length) {
479
+ const hex = input.slice(i + 1, i + 3)
480
+ if (isHexPair(hex)) {
481
+ output += '%' + hex.toUpperCase()
482
+ i += 2
483
+ continue
484
+ }
485
+ }
486
+
487
+ if (ch === '/') {
488
+ firstSegment = false
489
+ }
490
+
491
+ if (isPathCharacter(ch) && (ch !== ':' || !firstSegment)) {
492
+ output += ch
493
+ } else {
494
+ const code = input.charCodeAt(i)
495
+ if (code < 0x80) {
496
+ output += BYTE_HEX[code]
497
+ } else if (code < 0xD800 || code > 0xDFFF) {
498
+ output += percentEncodeNonAscii(code)
499
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
500
+ const low = input.charCodeAt(i + 1)
501
+ if (low >= 0xDC00 && low <= 0xDFFF) {
502
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
503
+ i++
504
+ } else {
505
+ output += percentEncodeNonAscii(0xFFFD)
506
+ }
507
+ } else {
508
+ output += percentEncodeNonAscii(0xFFFD)
509
+ }
510
+ }
511
+ }
512
+
513
+ return output
514
+ }
515
+
516
+ /**
517
+ * Percent-encodes a URI component using its RFC 3986 literal character set.
518
+ * Existing valid escapes are preserved and normalized to uppercase hex.
519
+ *
520
+ * @param {string} input
521
+ * @param {(value: string) => boolean} isAllowed
522
+ * @returns {string}
523
+ */
524
+ function encodeComponent (input, isAllowed) {
525
+ let output = ''
526
+
527
+ for (let i = 0; i < input.length; i++) {
528
+ const ch = input[i]
529
+ if (ch === '%' && i + 2 < input.length) {
530
+ const hex = input.slice(i + 1, i + 3)
531
+ if (isHexPair(hex)) {
532
+ output += '%' + hex.toUpperCase()
533
+ i += 2
534
+ continue
535
+ }
536
+ }
537
+
538
+ if (isAllowed(ch)) {
539
+ output += ch
540
+ } else {
541
+ const code = input.charCodeAt(i)
542
+ if (code < 0x80) {
543
+ output += BYTE_HEX[code]
544
+ } else if (code < 0xD800 || code > 0xDFFF) {
545
+ output += percentEncodeNonAscii(code)
546
+ } else if (code <= 0xDBFF && i + 1 < input.length) {
547
+ const low = input.charCodeAt(i + 1)
548
+ if (low >= 0xDC00 && low <= 0xDFFF) {
549
+ output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
550
+ i++
551
+ } else {
552
+ output += percentEncodeNonAscii(0xFFFD)
553
+ }
554
+ } else {
555
+ output += percentEncodeNonAscii(0xFFFD)
556
+ }
557
+ }
558
+ }
559
+
560
+ return output
561
+ }
562
+
563
+ /**
564
+ * Encodes userinfo while preserving its RFC 3986 §3.2.1 literal characters.
565
+ * In particular, authority delimiters such as `@`, `/`, `?`, and `#` are data.
566
+ *
567
+ * @param {string} input
568
+ * @returns {string}
569
+ */
570
+ function encodeUserinfo (input) {
571
+ return encodeComponent(input, isUserinfoCharacter)
572
+ }
573
+
574
+ /**
575
+ * Encodes query data using the RFC 3986 §3.4 grammar. A literal `#` must be
576
+ * escaped because it would otherwise begin the fragment component.
577
+ *
578
+ * @param {string} input
579
+ * @returns {string}
580
+ */
581
+ function encodeQuery (input) {
582
+ return encodeComponent(input, isQueryFragmentCharacter)
583
+ }
584
+
585
+ /**
586
+ * Encodes fragment data using the RFC 3986 §3.5 grammar.
587
+ *
588
+ * @param {string} input
589
+ * @returns {string}
590
+ */
591
+ function encodeFragment (input) {
592
+ return encodeComponent(input, isQueryFragmentCharacter)
593
+ }
594
+
595
+ function isEscapeSafe (cp) {
596
+ return (
597
+ (cp >= 0x30 && cp <= 0x39) ||
598
+ (cp >= 0x41 && cp <= 0x5A) ||
599
+ (cp >= 0x61 && cp <= 0x7A) ||
600
+ cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
601
+ cp === 0x2F || cp === 0x40 || cp === 0x5F
602
+ )
603
+ }
604
+
441
605
  /**
442
606
  * Normalizes the percent-encoding of a query or fragment component.
443
607
  *
@@ -505,8 +669,7 @@ function escapePreservingEscapes (input) {
505
669
  let output = ''
506
670
 
507
671
  for (let i = 0; i < input.length; i++) {
508
- const ch = input[i]
509
- if (ch === '%' && i + 2 < input.length) {
672
+ if (input[i] === '%' && i + 2 < input.length) {
510
673
  const hex = input.slice(i + 1, i + 3)
511
674
  if (isHexPair(hex)) {
512
675
  output += '%' + hex.toUpperCase()
@@ -515,22 +678,7 @@ function escapePreservingEscapes (input) {
515
678
  }
516
679
  }
517
680
 
518
- const code = input.charCodeAt(i)
519
- if (code < 0x80) {
520
- output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
521
- } else if (code < 0xD800 || code > 0xDFFF) {
522
- output += percentEncodeNonAscii(code)
523
- } else if (code <= 0xDBFF && i + 1 < input.length) {
524
- const low = input.charCodeAt(i + 1)
525
- if (low >= 0xDC00 && low <= 0xDFFF) {
526
- output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
527
- i++
528
- } else {
529
- output += percentEncodeNonAscii(0xFFFD)
530
- }
531
- } else {
532
- output += percentEncodeNonAscii(0xFFFD)
533
- }
681
+ output += escape(input[i])
534
682
  }
535
683
 
536
684
  return output
@@ -544,15 +692,21 @@ function recomposeAuthority (component) {
544
692
  const uriTokens = []
545
693
 
546
694
  if (component.userinfo !== undefined) {
547
- uriTokens.push(component.userinfo)
695
+ uriTokens.push(encodeUserinfo(component.userinfo))
548
696
  uriTokens.push('@')
549
697
  }
550
698
 
551
699
  if (component.host !== undefined) {
552
- let host = unescape(component.host)
700
+ let host = component.host
553
701
  if (!isIPv4(host)) {
554
- const ipV6res = normalizeIPv6(host)
555
- if (ipV6res.isIPV6 === true) {
702
+ let ipV6res = normalizeIPv6(host)
703
+ if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
704
+ // Decode only unreserved bytes, once. In particular, keep %25 encoded
705
+ // so it cannot introduce a second escape during recomposition.
706
+ host = normalizePercentEncoding(host, true)
707
+ ipV6res = normalizeIPv6(host)
708
+ }
709
+ if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
556
710
  host = `[${ipV6res.escapedHost}]`
557
711
  } else {
558
712
  host = reescapeHostDelimiters(host, false)
@@ -562,8 +716,12 @@ function recomposeAuthority (component) {
562
716
  }
563
717
 
564
718
  if (typeof component.port === 'number' || typeof component.port === 'string') {
719
+ const port = String(component.port)
720
+ if (!isPort(port)) {
721
+ throw new TypeError('URI port is malformed.')
722
+ }
565
723
  uriTokens.push(':')
566
- uriTokens.push(String(component.port))
724
+ uriTokens.push(port)
567
725
  }
568
726
 
569
727
  return uriTokens.length ? uriTokens.join('') : undefined
@@ -575,7 +733,11 @@ module.exports = {
575
733
  reescapeHostDelimiters,
576
734
  normalizePercentEncoding,
577
735
  normalizePathEncoding,
736
+ serializePathEncoding,
578
737
  normalizeQueryFragmentEncoding,
738
+ encodeUserinfo,
739
+ encodeQuery,
740
+ encodeFragment,
579
741
  escapePreservingEscapes,
580
742
  removeDotSegments,
581
743
  isIPv4,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fast-uri",
3
3
  "description": "Dependency-free RFC 3986 URI toolbox",
4
- "version": "4.1.1",
4
+ "version": "3.1.7",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -48,14 +48,14 @@
48
48
  "scripts": {
49
49
  "lint": "eslint",
50
50
  "lint:fix": "eslint --fix",
51
- "test": "npm run test:unit",
51
+ "test": "npm run test:unit && npm run test:typescript",
52
52
  "test:browser:chromium": "playwright-test ./test/* --runner tape --browser=chromium",
53
53
  "test:browser:firefox": "playwright-test ./test/* --runner tape --browser=firefox",
54
54
  "test:browser:webkit": "playwright-test ./test/* --runner tape --browser=webkit",
55
55
  "test:browser": "npm run test:browser:chromium && npm run test:browser:firefox && npm run test:browser:webkit",
56
56
  "test:unit": "tape test/**/*.js",
57
57
  "test:unit:dev": "npm run test:unit -- --coverage-report=html",
58
- "test:typescript": "tstyche"
58
+ "test:typescript": "tsd"
59
59
  },
60
60
  "devDependencies": {
61
61
  "ajv": "^8.16.0",
@@ -63,6 +63,6 @@
63
63
  "neostandard": "^0.13.0",
64
64
  "playwright-test": "^14.1.12",
65
65
  "tape": "^5.8.1",
66
- "tstyche": "^7.0.0"
66
+ "tsd": "^0.33.0"
67
67
  }
68
68
  }