@openclaw/lobster 2026.6.33 → 2026.6.35
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/node_modules/fast-uri/.github/dependabot.yml +42 -2
- package/node_modules/fast-uri/.github/workflows/ci.yml +54 -6
- package/node_modules/fast-uri/.github/workflows/lock-threads.yml +1 -1
- package/node_modules/fast-uri/.github/workflows/ossf-scorecard.yml +45 -0
- package/node_modules/fast-uri/README.md +10 -9
- package/node_modules/fast-uri/SECURITY.md +173 -0
- package/node_modules/fast-uri/benchmark/benchmark.mjs +71 -0
- package/node_modules/fast-uri/benchmark/encoder.mjs +54 -0
- package/node_modules/fast-uri/index.js +277 -45
- package/node_modules/fast-uri/lib/schemes.js +384 -7
- package/node_modules/fast-uri/lib/utils.js +458 -95
- package/node_modules/fast-uri/package.json +4 -4
- package/node_modules/fast-uri/test/component-safe-serialization.test.js +163 -0
- package/node_modules/fast-uri/test/equal.test.js +94 -3
- package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +24 -1
- package/node_modules/fast-uri/test/fixtures/uri-js-serialize.json +23 -1
- package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/fast-uri/test/ipv6-validation.test.js +154 -0
- package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
- package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
- package/node_modules/fast-uri/test/parse.test.js +150 -3
- package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
- package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
- package/node_modules/fast-uri/test/scheme-normalization.test.js +16 -0
- package/node_modules/fast-uri/test/scheme-validation.test.js +122 -0
- package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/fast-uri/test/security-utf8-encoding.test.js +0 -0
- package/node_modules/fast-uri/test/security.test.js +301 -3
- package/node_modules/fast-uri/test/serialize.test.js +144 -0
- package/node_modules/fast-uri/test/uri-js-compatibility.test.js +0 -3
- package/node_modules/fast-uri/test/uri-js.test.js +4 -4
- package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/fast-uri/test/util.test.js +9 -0
- package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/fast-uri/tsconfig.json +8 -7
- package/node_modules/fast-uri/types/index.d.ts +4 -9
- package/node_modules/fast-uri/types/index.tst.ts +31 -0
- package/npm-shrinkwrap.json +5 -5
- package/package.json +4 -4
- package/node_modules/fast-uri/.github/workflows/package-manager-ci.yml +0 -24
- package/node_modules/fast-uri/types/index.test-d.ts +0 -17
|
@@ -125,9 +125,307 @@ test('serialize escapes authority delimiters in host field', (t) => {
|
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
test('normalize does not double-decode %2540 into a live @', (t) => {
|
|
128
|
-
const
|
|
128
|
+
const input = 'http://trusted.com%2540evil.com/'
|
|
129
|
+
const result = fastURI.normalize(input)
|
|
129
130
|
const parsed = fastURI.parse(result)
|
|
130
131
|
|
|
131
|
-
t.plan(
|
|
132
|
-
t.
|
|
132
|
+
t.plan(2)
|
|
133
|
+
t.equal(result, input, 'the encoded percent sign is preserved')
|
|
134
|
+
t.notEqual(parsed.host, 'trusted.com@evil.com', input)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('parse canonicalises IDN / Unicode hosts to their ASCII form', (t) => {
|
|
138
|
+
const cases = [
|
|
139
|
+
{
|
|
140
|
+
input: 'http://127。0。0。1/',
|
|
141
|
+
expectedHost: '127.0.0.1',
|
|
142
|
+
description: 'full-width ideographic stops as octet separators'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
input: 'http://example.com/',
|
|
146
|
+
expectedHost: 'example.com',
|
|
147
|
+
description: 'fullwidth e as first letter'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
input: 'http://納豆.example.org/',
|
|
151
|
+
expectedHost: 'xn--99zt52a.example.org',
|
|
152
|
+
description: 'CJK label requiring punycode'
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
t.plan(cases.length * 2)
|
|
157
|
+
|
|
158
|
+
cases.forEach(({ input, expectedHost, description }) => {
|
|
159
|
+
const parsed = fastURI.parse(input)
|
|
160
|
+
t.notOk(parsed.error, `parse should not set error: ${description}`)
|
|
161
|
+
t.equal(parsed.host, expectedHost, `host canonicalised to ASCII: ${description}`)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('resolve canonicalises the host using the final resolved scheme', (t) => {
|
|
166
|
+
const cases = [
|
|
167
|
+
{
|
|
168
|
+
base: 'http://trusted.example/base',
|
|
169
|
+
relative: '//127\u30020\u30020\u30021/private',
|
|
170
|
+
expected: 'http://127.0.0.1/private',
|
|
171
|
+
expectedHost: '127.0.0.1',
|
|
172
|
+
description: 'scheme-relative loopback host'
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
base: 'https://ex\u00ADample.com/base',
|
|
176
|
+
relative: 'child',
|
|
177
|
+
expected: 'https://example.com/child',
|
|
178
|
+
expectedHost: 'example.com',
|
|
179
|
+
description: 'host inherited from the base'
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
base: 'http://trusted.example/base',
|
|
183
|
+
relative: 'http://ex\u200Bample.com/',
|
|
184
|
+
expected: 'http://example.com/',
|
|
185
|
+
expectedHost: 'example.com',
|
|
186
|
+
description: 'absolute relative reference'
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
t.plan(cases.length * 2)
|
|
191
|
+
|
|
192
|
+
cases.forEach(({ base, relative, expected, expectedHost, description }) => {
|
|
193
|
+
const resolved = fastURI.resolve(base, relative)
|
|
194
|
+
t.equal(resolved, expected, description)
|
|
195
|
+
t.equal(fastURI.parse(resolved).host, expectedHost, `${description} reparses consistently`)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('resolve applies domain canonicalisation only when the effective scheme opts in', (t) => {
|
|
200
|
+
const host = 'ex\u00ADample.com'
|
|
201
|
+
|
|
202
|
+
t.plan(2)
|
|
203
|
+
t.equal(
|
|
204
|
+
fastURI.resolve('uri://trusted.example/', `//${host}/`),
|
|
205
|
+
`uri://${host}/`,
|
|
206
|
+
'an unsupported scheme preserves the host'
|
|
207
|
+
)
|
|
208
|
+
t.equal(
|
|
209
|
+
fastURI.resolve('http://trusted.example/', `//${host}/`, { unicodeSupport: true }),
|
|
210
|
+
`http://${host}/`,
|
|
211
|
+
'unicodeSupport preserves the Unicode host'
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('resolve throws when the final scheme cannot canonicalise the host', (t) => {
|
|
216
|
+
const invalidHost = '\u200D.example'
|
|
217
|
+
const cases = [
|
|
218
|
+
['http://trusted.example/', `//${invalidHost}/`],
|
|
219
|
+
[`https://${invalidHost}/base`, 'child'],
|
|
220
|
+
['http://trusted.example/', `http://${invalidHost}/`],
|
|
221
|
+
['http://trusted.example/', `//${invalidHost}%2Etest/`]
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
t.plan(cases.length)
|
|
225
|
+
|
|
226
|
+
cases.forEach(([base, relative]) => {
|
|
227
|
+
t.throws(
|
|
228
|
+
() => fastURI.resolve(base, relative),
|
|
229
|
+
/Host's domain name can not be converted to ASCII/,
|
|
230
|
+
`${base} + ${relative}`
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('parse rejects a literal backslash in the authority as malformed (RFC 3986)', (t) => {
|
|
236
|
+
// Regression for the host-confusion bypass: a literal "\" is invalid RFC 3986
|
|
237
|
+
// syntax and must be flagged malformed, not silently rewritten. Otherwise "\"
|
|
238
|
+
// acts as a host delimiter here while Node's native URL parses a different
|
|
239
|
+
// host, defeating a host-based SSRF/redirect/origin allowlist.
|
|
240
|
+
const cases = [
|
|
241
|
+
'http://evil.com\\@allowed.com',
|
|
242
|
+
'https://169.254.169.254\\@trusted.example.com',
|
|
243
|
+
'http://127.0.0.1\\@public.example.com',
|
|
244
|
+
'https://attacker.com\\@api.internal',
|
|
245
|
+
'http://a\\@b',
|
|
246
|
+
'ws://evil.com\\@allowed.com/chat',
|
|
247
|
+
'wss://evil.com\\@allowed.com/chat',
|
|
248
|
+
'http://evil.com\\%40allowed.com',
|
|
249
|
+
'//evil.com\\@allowed.com'
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
t.plan(cases.length)
|
|
253
|
+
|
|
254
|
+
cases.forEach((input) => {
|
|
255
|
+
t.equal(
|
|
256
|
+
fastURI.parse(input).error,
|
|
257
|
+
'URI authority must not contain a literal backslash.',
|
|
258
|
+
input
|
|
259
|
+
)
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('normalize does not canonicalize a literal-backslash URI into a different valid URL', (t) => {
|
|
264
|
+
const cases = [
|
|
265
|
+
'http://evil.com\\@allowed.com',
|
|
266
|
+
'https://attacker.com\\@api.internal'
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
t.plan(cases.length)
|
|
270
|
+
|
|
271
|
+
cases.forEach((input) => {
|
|
272
|
+
t.equal(fastURI.normalize(input), input, input)
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('parse leaves percent-encoded %5C untouched as encoded data (not rejected)', (t) => {
|
|
277
|
+
// Only the literal "\" byte is rejected; %5C stays valid encoded data and
|
|
278
|
+
// does not diverge from the native URL parser, so it must not be flagged.
|
|
279
|
+
const input = 'http://evil.com%5C@allowed.com'
|
|
280
|
+
const parsed = fastURI.parse(input)
|
|
281
|
+
|
|
282
|
+
t.plan(2)
|
|
283
|
+
t.notOk(parsed.error, '%5C is valid encoded data, not malformed')
|
|
284
|
+
t.equal(parsed.host, new URL(input).hostname, '%5C host matches native URL (no divergence)')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('parse does not reject a literal backslash in the query or fragment', (t) => {
|
|
288
|
+
// The rejection is scoped to the authority/path (the host-confusion surface);
|
|
289
|
+
// a backslash after "?"/"#" is normalized as encoded data as before.
|
|
290
|
+
const parsed = fastURI.parse('http://host.example.com/?x=\\y#z\\w')
|
|
291
|
+
|
|
292
|
+
t.plan(2)
|
|
293
|
+
t.notOk(parsed.error, 'backslash in query/fragment does not mark the URI malformed')
|
|
294
|
+
t.equal(parsed.host, 'host.example.com', 'host parsed normally')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('parse rejects a malformed authority introducer (\\\\, /\\, \\/) in place of //', (t) => {
|
|
298
|
+
// Regression: "\\", "/\\", "\\/" after the scheme colon are not valid authority
|
|
299
|
+
// introducers. Node's URL treats "\\" as interchangeable with "/" on special
|
|
300
|
+
// schemes, so "http:\\\\evil.com/path" would be parsed as host "evil.com" by
|
|
301
|
+
// Node, but fast-uri must reject it as malformed to prevent SSRF/redirect bypass.
|
|
302
|
+
const cases = [
|
|
303
|
+
'http:\\\\evil.com/path',
|
|
304
|
+
'http:/\\evil.com/path',
|
|
305
|
+
'http:\\/evil.com/path',
|
|
306
|
+
'ws:\\\\evil.com/chat',
|
|
307
|
+
'wss:\\\\evil.com/chat',
|
|
308
|
+
'ftp:\\\\evil.com/',
|
|
309
|
+
'\\\\evil.com/path'
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
t.plan(cases.length)
|
|
313
|
+
|
|
314
|
+
cases.forEach((input) => {
|
|
315
|
+
t.equal(
|
|
316
|
+
fastURI.parse(input).error,
|
|
317
|
+
'URI authority must not contain a literal backslash.',
|
|
318
|
+
input
|
|
319
|
+
)
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('normalize does not canonicalize a malformed-authority-introducer URI', (t) => {
|
|
324
|
+
const cases = [
|
|
325
|
+
'http:\\\\evil.com/path',
|
|
326
|
+
'http:/\\evil.com/path'
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
t.plan(cases.length)
|
|
330
|
+
|
|
331
|
+
cases.forEach((input) => {
|
|
332
|
+
t.equal(fastURI.normalize(input), input, input)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test('equal returns false for malformed-authority-introducer URIs', (t) => {
|
|
337
|
+
const pairs = [
|
|
338
|
+
['http:\\\\evil.com/path', 'http://evil.com/path'],
|
|
339
|
+
['http:/\\evil.com/path', 'http://evil.com/path']
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
t.plan(pairs.length)
|
|
343
|
+
|
|
344
|
+
pairs.forEach(([left, right]) => {
|
|
345
|
+
t.equal(fastURI.equal(left, right), false, `${left} != ${right}`)
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('resolve throws on malformed authority introducer', (t) => {
|
|
350
|
+
// resolve() returns a plain string with no error field, so the only safe
|
|
351
|
+
// behavior is to throw when either component has a malformed authority.
|
|
352
|
+
const pairs = [
|
|
353
|
+
['https://allowed.com/', '\\\\evil.com/path'],
|
|
354
|
+
['\\\\evil.com/path', 'https://allowed.com/'],
|
|
355
|
+
['https://allowed.com/', 'http:/\\evil.com/path'],
|
|
356
|
+
['https://allowed.com/', 'http:\\/evil.com/path']
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
t.plan(pairs.length)
|
|
360
|
+
|
|
361
|
+
pairs.forEach(([base, rel]) => {
|
|
362
|
+
t.throws(
|
|
363
|
+
() => fastURI.resolve(base, rel),
|
|
364
|
+
/URI authority must not contain a literal backslash/,
|
|
365
|
+
`${base} + ${rel}`
|
|
366
|
+
)
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('parse rejects a whitespace-split authority introducer (TAB, LF, CR)', (t) => {
|
|
371
|
+
// The WHATWG URL parser removes TAB (U+0009), LF (U+000A) and CR (U+000D) from
|
|
372
|
+
// the input before parsing, so a stripped character wedged into the introducer
|
|
373
|
+
// ("/<TAB>\\", "/<TAB>/", or a leading "<TAB>//") reaches an authority in Node
|
|
374
|
+
// while fast-uri would otherwise fold it into the path. These must be rejected
|
|
375
|
+
// like the adjacent "\\", "/\\", "\\/" forms.
|
|
376
|
+
const cases = [
|
|
377
|
+
{ input: '/\t\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
|
|
378
|
+
{ input: '/\t/evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' },
|
|
379
|
+
{ input: '/\n\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
|
|
380
|
+
{ input: '/\r\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
|
|
381
|
+
{ input: '\t//evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' },
|
|
382
|
+
{ input: '\t/\\evil.com/path', expectedError: 'URI authority must not contain a literal backslash.' },
|
|
383
|
+
{ input: 'https:/\t/evil.com/path', expectedError: 'URI authority introducer must not contain whitespace.' }
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
t.plan(cases.length)
|
|
387
|
+
|
|
388
|
+
cases.forEach(({ input, expectedError }) => {
|
|
389
|
+
t.equal(fastURI.parse(input).error, expectedError, JSON.stringify(input))
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('resolve throws on a whitespace-split authority introducer', (t) => {
|
|
394
|
+
const pairs = [
|
|
395
|
+
['https://allowed.com/', '/\t\\evil.com/path'],
|
|
396
|
+
['https://allowed.com/', '/\t/evil.com/path'],
|
|
397
|
+
['https://allowed.com/', '/\n\\evil.com/path'],
|
|
398
|
+
['/\t/evil.com/path', 'https://allowed.com/']
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
t.plan(pairs.length)
|
|
402
|
+
|
|
403
|
+
pairs.forEach(([base, rel]) => {
|
|
404
|
+
t.throws(
|
|
405
|
+
() => fastURI.resolve(base, rel),
|
|
406
|
+
/URI authority (must not contain a literal backslash|introducer must not contain whitespace)/,
|
|
407
|
+
`${JSON.stringify(base)} + ${JSON.stringify(rel)}`
|
|
408
|
+
)
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('parse does not reject valid authority introducer patterns', (t) => {
|
|
413
|
+
// No false positives: "//" introducer and scheme-less "//" must be valid.
|
|
414
|
+
const cases = [
|
|
415
|
+
'http://good.com/',
|
|
416
|
+
'https://good.com/',
|
|
417
|
+
'ws://good.com/chat',
|
|
418
|
+
'wss://good.com/chat',
|
|
419
|
+
'ftp://good.com/',
|
|
420
|
+
'//good.com/path',
|
|
421
|
+
'/absolute/path',
|
|
422
|
+
'relative/path'
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
t.plan(cases.length)
|
|
426
|
+
|
|
427
|
+
cases.forEach((input) => {
|
|
428
|
+
const parsed = fastURI.parse(input)
|
|
429
|
+
t.notOk(parsed.error, input)
|
|
430
|
+
})
|
|
133
431
|
})
|
|
@@ -149,3 +149,147 @@ test('URN NID Override', (t) => {
|
|
|
149
149
|
t.equal(fastURI.serialize(components, { nid: 'uuid' }), 'urn:foo:f81d4fae-7dec-11d0-a765-00a0c91e6bf6')
|
|
150
150
|
t.end()
|
|
151
151
|
})
|
|
152
|
+
|
|
153
|
+
test('Mailto serialization preserves delimiters, escapes, and input state', (t) => {
|
|
154
|
+
const valueComponent = {
|
|
155
|
+
scheme: 'mailto',
|
|
156
|
+
to: ['user@example.org'],
|
|
157
|
+
headers: { x: 'a&b' }
|
|
158
|
+
}
|
|
159
|
+
const valueURI = fastURI.serialize(valueComponent)
|
|
160
|
+
t.equal(valueURI, 'mailto:user@example.org?x=a%26b', 'ampersand in header value is encoded')
|
|
161
|
+
// spread: `headers` has a null prototype, which t.deepEqual compares strictly
|
|
162
|
+
t.deepEqual({ ...fastURI.parse(valueURI).headers }, { x: 'a&b' }, 'header value round-trips')
|
|
163
|
+
|
|
164
|
+
const nameComponent = {
|
|
165
|
+
scheme: 'mailto',
|
|
166
|
+
to: ['user@example.org'],
|
|
167
|
+
headers: { 'x&y': 'z' }
|
|
168
|
+
}
|
|
169
|
+
const nameURI = fastURI.serialize(nameComponent)
|
|
170
|
+
t.equal(nameURI, 'mailto:user@example.org?x%26y=z', 'ampersand in header name is encoded')
|
|
171
|
+
t.deepEqual({ ...fastURI.parse(nameURI).headers }, { 'x&y': 'z' }, 'header name round-trips')
|
|
172
|
+
|
|
173
|
+
t.equal(
|
|
174
|
+
fastURI.serialize({ scheme: 'mailto', to: ['user@example.org'], subject: 'a%2fb' }),
|
|
175
|
+
'mailto:user@example.org?subject=a%2Fb',
|
|
176
|
+
'valid percent escape is preserved and uppercased'
|
|
177
|
+
)
|
|
178
|
+
t.equal(
|
|
179
|
+
fastURI.serialize({ scheme: 'mailto', to: ['user@example.org'], subject: 'a%GGb' }),
|
|
180
|
+
'mailto:user@example.org?subject=a%25GGb',
|
|
181
|
+
'malformed percent escape is encoded'
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const immutableComponent = {
|
|
185
|
+
scheme: 'mailto',
|
|
186
|
+
to: ['café@EXAMPLE.ORG'],
|
|
187
|
+
subject: 'hello',
|
|
188
|
+
headers: { cc: 'other@example.org' }
|
|
189
|
+
}
|
|
190
|
+
const expected = JSON.parse(JSON.stringify(immutableComponent))
|
|
191
|
+
fastURI.serialize(immutableComponent)
|
|
192
|
+
t.deepEqual(immutableComponent, expected, 'recipient and header containers are not mutated')
|
|
193
|
+
t.end()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('Mailto serialization does not let recipient data inject URI structure', (t) => {
|
|
197
|
+
// A recipient domain must never reach the output with a delimiter intact:
|
|
198
|
+
// "?" would inject header fields and "#" a fragment.
|
|
199
|
+
const cases = [
|
|
200
|
+
['user@example.org?subject=Injected', 'mailto:user@example.org%3Fsubject%3Dinjected'],
|
|
201
|
+
['user@example.org#frag', 'mailto:user@example.org%23frag'],
|
|
202
|
+
// domain literals accept the full RFC 5321 dcontent range, which includes "?"
|
|
203
|
+
['user@[x?subject=Injected]', 'mailto:user@[x%3Fsubject%3Dinjected]'],
|
|
204
|
+
// "," is the recipient delimiter, so it must be encoded inside an address
|
|
205
|
+
['a,b@example.org', 'mailto:a%2Cb@example.org']
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
for (const [recipient, expected] of cases) {
|
|
209
|
+
const uri = fastURI.serialize({ scheme: 'mailto', to: [recipient] })
|
|
210
|
+
t.equal(uri, expected, 'encodes delimiters in ' + recipient)
|
|
211
|
+
const reparsed = fastURI.parse(uri)
|
|
212
|
+
t.deepEqual(reparsed.to, [recipient.toLowerCase()], 'round-trips ' + recipient)
|
|
213
|
+
t.equal(reparsed.subject, undefined, 'no subject injected by ' + recipient)
|
|
214
|
+
t.equal(reparsed.headers, undefined, 'no headers injected by ' + recipient)
|
|
215
|
+
t.equal(reparsed.fragment, undefined, 'no fragment injected by ' + recipient)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
t.equal(
|
|
219
|
+
fastURI.serialize({ scheme: 'mailto', to: ['a@x.test', 'b@y.test'] }),
|
|
220
|
+
'mailto:a@x.test,b@y.test',
|
|
221
|
+
'the joining comma between recipients stays literal'
|
|
222
|
+
)
|
|
223
|
+
t.equal(
|
|
224
|
+
fastURI.serialize({ scheme: 'mailto', to: ['user@[IPv6:2001:db8::1]'] }),
|
|
225
|
+
'mailto:user@[ipv6:2001:db8::1]',
|
|
226
|
+
'a valid domain literal keeps its delimiters'
|
|
227
|
+
)
|
|
228
|
+
t.equal(
|
|
229
|
+
fastURI.serialize({ scheme: 'mailto', to: ['user@納豆.example.org'] }),
|
|
230
|
+
'mailto:user@xn--99zt52a.example.org',
|
|
231
|
+
'Unicode domain is still converted to ASCII'
|
|
232
|
+
)
|
|
233
|
+
t.equal(
|
|
234
|
+
fastURI.serialize({ scheme: 'mailto', to: ['user@納豆.example.org'] }, { unicodeSupport: true }),
|
|
235
|
+
'mailto:user@納豆.example.org',
|
|
236
|
+
'Unicode domain is still preserved when requested'
|
|
237
|
+
)
|
|
238
|
+
t.end()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('Mailto encoding is consistent across the fast/slow path boundary', (t) => {
|
|
242
|
+
// The encoder returns the input untouched when every character is allowed,
|
|
243
|
+
// and only then falls back to the per-character loop. Both branches must
|
|
244
|
+
// agree on what needs escaping.
|
|
245
|
+
const base = { scheme: 'mailto', to: ['user@example.org'] }
|
|
246
|
+
|
|
247
|
+
t.equal(
|
|
248
|
+
fastURI.serialize({ ...base, headers: { 'In-Reply-To': 'plain-value' } }),
|
|
249
|
+
'mailto:user@example.org?In-Reply-To=plain-value',
|
|
250
|
+
'fully allowed name and value are emitted verbatim'
|
|
251
|
+
)
|
|
252
|
+
t.equal(
|
|
253
|
+
fastURI.serialize({ ...base, headers: { 'x y': '<a>&b c' } }),
|
|
254
|
+
'mailto:user@example.org?x%20y=%3Ca%3E%26b%20c',
|
|
255
|
+
'disallowed characters are escaped in both name and value'
|
|
256
|
+
)
|
|
257
|
+
t.equal(
|
|
258
|
+
fastURI.serialize({ ...base, body: 'send current-issue\r\nsend index' }),
|
|
259
|
+
'mailto:user@example.org?body=send%20current-issue%0D%0Asend%20index',
|
|
260
|
+
'space and CRLF are escaped in a body'
|
|
261
|
+
)
|
|
262
|
+
t.equal(
|
|
263
|
+
fastURI.serialize({ ...base, subject: 'a%2fb%GGc' }),
|
|
264
|
+
'mailto:user@example.org?subject=a%2Fb%25GGc',
|
|
265
|
+
'valid escapes are uppercased and invalid ones escaped in one pass'
|
|
266
|
+
)
|
|
267
|
+
t.equal(
|
|
268
|
+
fastURI.serialize({ scheme: 'mailto', to: ['a.b-c_d~e!f@example.org'] }),
|
|
269
|
+
'mailto:a.b-c_d~e!f@example.org',
|
|
270
|
+
'an entirely allowed local part takes the fast path unchanged'
|
|
271
|
+
)
|
|
272
|
+
t.end()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('Mailto serialization ignores a caller-supplied path', (t) => {
|
|
276
|
+
// `to` is the only recipient source. A raw path has not been through this
|
|
277
|
+
// handler's encoder, and the handler sets `skipEscape`, so emitting it would
|
|
278
|
+
// put unescaped data (including CR/LF) straight into the URI.
|
|
279
|
+
t.equal(
|
|
280
|
+
fastURI.serialize({ scheme: 'mailto', path: 'a@b.test\r\nBcc: evil@evil.test' }),
|
|
281
|
+
'mailto:',
|
|
282
|
+
'a path containing CRLF is dropped, not emitted raw'
|
|
283
|
+
)
|
|
284
|
+
t.equal(
|
|
285
|
+
fastURI.serialize({ scheme: 'mailto', path: 'user@example.org?subject=Injected' }),
|
|
286
|
+
'mailto:',
|
|
287
|
+
'a path containing header fields is dropped'
|
|
288
|
+
)
|
|
289
|
+
t.equal(
|
|
290
|
+
fastURI.serialize({ scheme: 'mailto', to: ['a@b.test'], path: 'ignored?x=1' }),
|
|
291
|
+
'mailto:a@b.test',
|
|
292
|
+
'a path is ignored when recipients are present'
|
|
293
|
+
)
|
|
294
|
+
t.end()
|
|
295
|
+
})
|
|
@@ -13,9 +13,6 @@ test('uri-js compatibility Parse', (t) => {
|
|
|
13
13
|
if (value === '//10.10.000.10') {
|
|
14
14
|
return t.skip('Skipping //10.10.000.10 as it is not a valid URI per URI spec: https://datatracker.ietf.org/doc/html/rfc5954#section-4.1')
|
|
15
15
|
}
|
|
16
|
-
if (value.slice(0, 6) === 'mailto') {
|
|
17
|
-
return t.skip('Skipping mailto schema test as it is not supported by fastifyURI')
|
|
18
|
-
}
|
|
19
16
|
t.same(JSON.parse(JSON.stringify(fastURI.parse(value))), expected, 'Compatibility parse: ' + value)
|
|
20
17
|
})
|
|
21
18
|
t.end()
|
|
@@ -8,7 +8,7 @@ const fastURI = require('..')
|
|
|
8
8
|
*
|
|
9
9
|
* @fileoverview An RFC 3986 compliant, scheme extendable URI parsing/normalizing/resolving/serializing library for JavaScript.
|
|
10
10
|
* @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
|
|
11
|
-
* @see
|
|
11
|
+
* @see https://github.com/garycourt/uri-js
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -692,7 +692,7 @@ if (fastURI.SCHEMES.mailto) {
|
|
|
692
692
|
|
|
693
693
|
components = fastURI.parse('mailto:list@example.org?In-Reply-To=%3C3469A91.D10AF4C@example.com%3E')
|
|
694
694
|
t.deepEqual(components.to, ['list@example.org'], 'to')
|
|
695
|
-
t.deepEqual(components.headers, { 'In-Reply-To': '<3469A91.D10AF4C@example.com>' }, 'headers')
|
|
695
|
+
t.deepEqual({ ...components.headers }, { 'In-Reply-To': '<3469A91.D10AF4C@example.com>' }, 'headers')
|
|
696
696
|
|
|
697
697
|
components = fastURI.parse('mailto:majordomo@example.com?body=subscribe%20bamboo-l')
|
|
698
698
|
t.deepEqual(components.to, ['majordomo@example.com'], 'to')
|
|
@@ -701,7 +701,7 @@ if (fastURI.SCHEMES.mailto) {
|
|
|
701
701
|
components = fastURI.parse('mailto:joe@example.com?cc=bob@example.com&body=hello')
|
|
702
702
|
t.deepEqual(components.to, ['joe@example.com'], 'to')
|
|
703
703
|
t.equal(components.body, 'hello', 'body')
|
|
704
|
-
t.deepEqual(components.headers, { cc: 'bob@example.com' }, 'headers')
|
|
704
|
+
t.deepEqual({ ...components.headers }, { cc: 'bob@example.com' }, 'headers')
|
|
705
705
|
|
|
706
706
|
components = fastURI.parse('mailto:joe@example.com?cc=bob@example.com?body=hello')
|
|
707
707
|
if (fastURI.VALIDATE_SUPPORT) t.ok(components.error, 'invalid header fields')
|
|
@@ -711,7 +711,7 @@ if (fastURI.SCHEMES.mailto) {
|
|
|
711
711
|
|
|
712
712
|
components = fastURI.parse('mailto:unlikely%3Faddress@example.com?blat=foop')
|
|
713
713
|
t.deepEqual(components.to, ['unlikely?address@example.com'], 'to unlikely?address@example.com')
|
|
714
|
-
t.deepEqual(components.headers, { blat: 'foop' }, 'headers')
|
|
714
|
+
t.deepEqual({ ...components.headers }, { blat: 'foop' }, 'headers')
|
|
715
715
|
|
|
716
716
|
components = fastURI.parse('mailto:Mike%26family@example.org')
|
|
717
717
|
t.deepEqual(components.to, ['Mike&family@example.org'], 'to Mike&family@example.org')
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('tape')
|
|
4
|
+
const fastURI = require('..')
|
|
5
|
+
|
|
6
|
+
test('URN parsing validates the complete scheme-specific input', (t) => {
|
|
7
|
+
const embedded = fastURI.parse('urn:x|foo:bar')
|
|
8
|
+
t.equal(embedded.error, 'URN can not be parsed.', 'does not search for a later nid:nss substring')
|
|
9
|
+
t.equal(embedded.nid, undefined, 'does not adopt the later nid')
|
|
10
|
+
t.equal(embedded.nss, undefined, 'does not adopt the later nss')
|
|
11
|
+
|
|
12
|
+
const trailing = fastURI.parse('urn:foo:bar|ignored')
|
|
13
|
+
t.equal(trailing.error, 'URN can not be parsed.', 'rejects trailing input outside the RFC 2141 NSS grammar')
|
|
14
|
+
t.equal(trailing.nid, undefined, 'does not return an nid from a partial match')
|
|
15
|
+
t.equal(trailing.nss, undefined, 'does not return a truncated nss')
|
|
16
|
+
|
|
17
|
+
t.end()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('URN parsing preserves the complete RFC 2141 NSS', (t) => {
|
|
21
|
+
const parsed = fastURI.parse('urn:foo:allowed/evil')
|
|
22
|
+
t.equal(parsed.error, undefined, 'accepts slash in an RFC 2141 NSS')
|
|
23
|
+
t.equal(parsed.nid, 'foo', 'parses the nid')
|
|
24
|
+
t.equal(parsed.nss, 'allowed/evil', 'preserves the complete NSS')
|
|
25
|
+
t.equal(fastURI.normalize('urn:foo:allowed/evil'), 'urn:foo:allowed/evil', 'normalization retains slash data')
|
|
26
|
+
t.equal(fastURI.equal('urn:foo:allowed/evil', 'urn:foo:allowed'), false, 'distinct NSS values do not compare equal')
|
|
27
|
+
|
|
28
|
+
t.end()
|
|
29
|
+
})
|
|
@@ -29,6 +29,15 @@ test('removeDotSegments', (t) => {
|
|
|
29
29
|
// https://github.com/fastify/fast-uri/issues/139
|
|
30
30
|
testCases.push(['WS:/WS://1305G130505:1&%0D:1&C(XXXXX*)))))))XXX130505:UUVUaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$aaaaaaaaaaaa13a',
|
|
31
31
|
'WS:/WS://1305G130505:1&%0D:1&C(XXXXX*)))))))XXX130505:UUVUaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$aaaaaaaaaaaa13a'])
|
|
32
|
+
// trailing empty segments must be preserved: RFC 3986 5.2.4 only removes
|
|
33
|
+
// complete "." / ".." segments, never an empty segment
|
|
34
|
+
testCases.push(['//', '//'])
|
|
35
|
+
testCases.push(['/foo//', '/foo//'])
|
|
36
|
+
testCases.push(['/a/b//', '/a/b//'])
|
|
37
|
+
testCases.push(['/x//y//z', '/x//y//z'])
|
|
38
|
+
// "." / ".." segments still collapse
|
|
39
|
+
testCases.push(['/a/b/c/./../../g', '/a/g'])
|
|
40
|
+
testCases.push(['/.', '/'])
|
|
32
41
|
|
|
33
42
|
t.plan(testCases.length)
|
|
34
43
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('tape')
|
|
4
|
+
const fastURI = require('..')
|
|
5
|
+
|
|
6
|
+
test('WebSocket queries preserve additional question marks', (t) => {
|
|
7
|
+
for (const scheme of ['ws', 'wss']) {
|
|
8
|
+
const uri = `${scheme}://example.com/chat?a?b`
|
|
9
|
+
const truncatedURI = `${scheme}://example.com/chat?a`
|
|
10
|
+
const parsed = fastURI.parse(uri)
|
|
11
|
+
|
|
12
|
+
t.equal(parsed.resourceName, '/chat?a?b', `${scheme} parse preserves the resource name`)
|
|
13
|
+
t.equal(fastURI.serialize(parsed), uri, `${scheme} parsed components round-trip`)
|
|
14
|
+
t.equal(
|
|
15
|
+
fastURI.serialize({ scheme, host: 'example.com', resourceName: '/chat?a?b' }),
|
|
16
|
+
uri,
|
|
17
|
+
`${scheme} resource name serializes without truncation`
|
|
18
|
+
)
|
|
19
|
+
t.equal(fastURI.normalize(uri), uri, `${scheme} normalization preserves the full query`)
|
|
20
|
+
t.equal(fastURI.equal(uri, truncatedURI), false, `${scheme} equality distinguishes the full query`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
t.end()
|
|
24
|
+
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": true,
|
|
4
|
+
"checkJs": true,
|
|
5
|
+
"strict": true,
|
|
6
|
+
"noImplicitAny": true,
|
|
7
|
+
"target": "es2022",
|
|
8
|
+
"noEmit": true
|
|
9
|
+
}
|
|
9
10
|
}
|
|
@@ -16,6 +16,10 @@ declare namespace fastUri {
|
|
|
16
16
|
secure?: boolean;
|
|
17
17
|
uuid?: string;
|
|
18
18
|
error?: string;
|
|
19
|
+
to?: string[];
|
|
20
|
+
subject?: string;
|
|
21
|
+
body?: string;
|
|
22
|
+
headers?: { [hfname: string]: string };
|
|
19
23
|
}
|
|
20
24
|
export interface Options {
|
|
21
25
|
scheme?: string;
|
|
@@ -28,15 +32,6 @@ declare namespace fastUri {
|
|
|
28
32
|
nid?: string;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
/**
|
|
32
|
-
* @deprecated Use Options instead
|
|
33
|
-
*/
|
|
34
|
-
export type options = Options
|
|
35
|
-
/**
|
|
36
|
-
* @deprecated Use URIComponent instead
|
|
37
|
-
*/
|
|
38
|
-
export type URIComponents = URIComponent
|
|
39
|
-
|
|
40
35
|
export function normalize (uri: string, opts?: Options): string
|
|
41
36
|
export function normalize (uri: URIComponent, opts?: Options): URIComponent
|
|
42
37
|
export function normalize (uri: any, opts?: Options): any
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import uri, {
|
|
2
|
+
type URIComponent,
|
|
3
|
+
} from '..'
|
|
4
|
+
import { expect } from 'tstyche'
|
|
5
|
+
|
|
6
|
+
const parsed = uri.parse('foo')
|
|
7
|
+
expect(parsed).type.toBe<URIComponent>()
|
|
8
|
+
|
|
9
|
+
const parsed2 = uri.parse('foo', {
|
|
10
|
+
domainHost: true,
|
|
11
|
+
scheme: 'https',
|
|
12
|
+
unicodeSupport: false
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(parsed2).type.toBe<URIComponent>()
|
|
16
|
+
|
|
17
|
+
const mailtoComponent: URIComponent = {
|
|
18
|
+
scheme: 'mailto',
|
|
19
|
+
to: ['user@example.org'],
|
|
20
|
+
subject: 'Hello',
|
|
21
|
+
body: 'Message',
|
|
22
|
+
headers: { cc: 'other@example.org' }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
expect(uri.serialize(mailtoComponent, { unicodeSupport: true })).type.toBe<string>()
|
|
26
|
+
|
|
27
|
+
const parsedMailto = uri.parse('mailto:user@example.org')
|
|
28
|
+
expect(parsedMailto.to).type.toBe<string[] | undefined>()
|
|
29
|
+
expect(parsedMailto.subject).type.toBe<string | undefined>()
|
|
30
|
+
expect(parsedMailto.body).type.toBe<string | undefined>()
|
|
31
|
+
expect(parsedMailto.headers).type.toBe<{ [hfname: string]: string } | undefined>()
|