@openclaw/lobster 2026.7.2-beta.7 → 2026.7.34
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.
- package/dist/index.js +149 -9
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/.github/dependabot.yml +2 -42
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/.github/workflows/ci.yml +3 -51
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/.github/workflows/package-manager-ci.yml +24 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/README.md +9 -9
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/index.js +246 -41
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/lib/schemes.js +9 -4
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/lib/utils.js +319 -157
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/package.json +4 -4
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/component-safe-serialization.test.js +163 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/equal.test.js +31 -54
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/ipv6-validation.test.js +124 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/parse.test.js +2 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/security.test.js +211 -3
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/uri-js.test.js +1 -1
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/util.test.js +0 -9
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/tsconfig.json +7 -8
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/types/index.d.ts +9 -0
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/types/index.test-d.ts +17 -0
- package/node_modules/typebox/build/compile/validator.d.mts +4 -1
- package/node_modules/typebox/build/guard/guard.d.mts +1 -1
- package/node_modules/typebox/build/guard/guard.mjs +2 -5
- package/node_modules/typebox/build/guard/string.mjs +4 -16
- package/node_modules/typebox/build/schema/engine/_functions.mjs +15 -34
- package/node_modules/typebox/build/type/action/readonly_object.d.mts +0 -5
- package/node_modules/typebox/build/type/action/readonly_object.mjs +0 -8
- package/node_modules/typebox/build/typebox.d.mts +1 -1
- package/node_modules/typebox/build/typebox.mjs +1 -1
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +32 -29
- package/openclaw.plugin.json +0 -1
- package/package.json +5 -5
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/benchmark/encoder.mjs +0 -54
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/scheme-normalization.test.js +0 -16
- package/node_modules/@clawdbot/lobster/node_modules/fast-uri/test/security-utf8-encoding.test.js +0 -0
- 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(/^[
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 {
|
|
97
|
+
* @param {string} zone
|
|
67
98
|
* @returns {boolean}
|
|
68
99
|
*/
|
|
69
|
-
function
|
|
70
|
-
|
|
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
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* @
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
136
|
+
runStart = -1
|
|
137
|
+
runLength = 0
|
|
88
138
|
}
|
|
89
|
-
buffer.length = 0
|
|
90
139
|
}
|
|
91
|
-
|
|
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 {
|
|
154
|
+
* @returns {string|undefined}
|
|
97
155
|
*/
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
700
|
+
let host = component.host
|
|
553
701
|
if (!isIPv4(host)) {
|
|
554
|
-
|
|
555
|
-
if (ipV6res.isIPV6
|
|
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(
|
|
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
|
+
"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": "
|
|
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
|
-
"
|
|
66
|
+
"tsd": "^0.33.0"
|
|
67
67
|
}
|
|
68
68
|
}
|