@suveren/gateway 0.7.4 → 0.7.6
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/control-plane/index.mjs +248 -58
- package/dist/mcp-server/http.mjs +408 -65
- package/dist/ui/assets/{index-CkWaRck1.css → index-BYVb_0oG.css} +1 -1
- package/dist/ui/assets/index-CsrM6RQr.js +110 -0
- package/dist/ui/index.html +2 -2
- package/node_modules/@hap/core/dist/index.d.mts +258 -9
- package/node_modules/@hap/core/dist/index.d.ts +258 -9
- package/node_modules/@hap/core/dist/index.js +227 -5
- package/node_modules/@hap/core/dist/index.mjs +213 -4
- package/node_modules/@hap/core/package.json +3 -2
- package/node_modules/@hap/core/src/did-key.ts +126 -0
- package/node_modules/@hap/core/src/identity.ts +21 -5
- package/node_modules/@hap/core/src/index.ts +3 -0
- package/node_modules/@hap/core/src/mandate.ts +207 -0
- package/node_modules/@hap/core/src/receipt.ts +74 -0
- package/node_modules/@hap/core/src/types.ts +81 -5
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/accept.js +54 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/charset.js +10 -34
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/encoding.js +9 -30
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/language.js +15 -36
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/mediaType.js +16 -134
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/LICENSE +22 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/README.md +71 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.d.ts +46 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.js +176 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/package.json +52 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/package.json +16 -10
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/buffer.buffer.d.ts +5 -5
- package/node_modules/@types/node/crypto.d.ts +2 -2
- package/node_modules/@types/node/http.d.ts +16 -0
- package/node_modules/@types/node/http2.d.ts +10 -2
- package/node_modules/@types/node/package.json +2 -2
- package/node_modules/@types/node/process.d.ts +52 -0
- package/node_modules/@types/node/quic.d.ts +210 -28
- package/node_modules/@types/node/sqlite.d.ts +6 -2
- package/node_modules/@types/node/test.d.ts +36 -0
- package/node_modules/fast-uri/index.js +201 -48
- package/node_modules/fast-uri/lib/schemes.js +9 -4
- package/node_modules/fast-uri/lib/utils.js +388 -91
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/component-safe-serialization.test.js +105 -0
- package/node_modules/fast-uri/test/equal.test.js +31 -3
- package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
- package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/fast-uri/test/ipv6-validation.test.js +90 -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 +7 -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-validation.test.js +124 -0
- package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/fast-uri/test/security.test.js +75 -3
- package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/hono/dist/cjs/client/client.js +25 -11
- package/node_modules/hono/dist/cjs/client/utils.js +4 -1
- package/node_modules/hono/dist/cjs/context.js +4 -0
- package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +36 -2
- package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +1 -1
- package/node_modules/hono/dist/cjs/helper/ssg/utils.js +30 -10
- package/node_modules/hono/dist/cjs/jsx/dom/render.js +2 -0
- package/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/cors/index.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/csrf/index.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/etag/digest.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -3
- package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +3 -1
- package/node_modules/hono/dist/cjs/request.js +8 -4
- package/node_modules/hono/dist/cjs/router/linear-router/router.js +7 -2
- package/node_modules/hono/dist/cjs/router/pattern-router/router.js +3 -9
- package/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +10 -3
- package/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +47 -83
- package/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +2 -1
- package/node_modules/hono/dist/cjs/router/trie-router/node.js +46 -69
- package/node_modules/hono/dist/cjs/router/trie-router/router.js +3 -11
- package/node_modules/hono/dist/cjs/router/utils.js +27 -0
- package/node_modules/hono/dist/cjs/utils/body.js +15 -3
- package/node_modules/hono/dist/cjs/utils/cookie.js +1 -1
- package/node_modules/hono/dist/cjs/utils/ipaddr.js +5 -3
- package/node_modules/hono/dist/cjs/utils/stream.js +7 -1
- package/node_modules/hono/dist/cjs/utils/url.js +9 -1
- package/node_modules/hono/dist/client/client.js +25 -11
- package/node_modules/hono/dist/client/utils.js +4 -1
- package/node_modules/hono/dist/context.js +4 -0
- package/node_modules/hono/dist/helper/accepts/accepts.js +36 -2
- package/node_modules/hono/dist/helper/ssg/ssg.js +1 -1
- package/node_modules/hono/dist/helper/ssg/utils.js +30 -10
- package/node_modules/hono/dist/jsx/dom/render.js +2 -0
- package/node_modules/hono/dist/middleware/cache/index.js +1 -1
- package/node_modules/hono/dist/middleware/cors/index.js +1 -1
- package/node_modules/hono/dist/middleware/csrf/index.js +1 -1
- package/node_modules/hono/dist/middleware/etag/digest.js +1 -1
- package/node_modules/hono/dist/middleware/etag/index.js +3 -3
- package/node_modules/hono/dist/middleware/pretty-json/index.js +3 -1
- package/node_modules/hono/dist/request.js +8 -4
- package/node_modules/hono/dist/router/linear-router/router.js +7 -2
- package/node_modules/hono/dist/router/pattern-router/router.js +3 -9
- package/node_modules/hono/dist/router/reg-exp-router/node.js +6 -2
- package/node_modules/hono/dist/router/reg-exp-router/router.js +53 -84
- package/node_modules/hono/dist/router/reg-exp-router/trie.js +2 -1
- package/node_modules/hono/dist/router/trie-router/node.js +46 -69
- package/node_modules/hono/dist/router/trie-router/router.js +3 -11
- package/node_modules/hono/dist/router/utils.js +5 -0
- package/node_modules/hono/dist/types/context.d.ts +4 -0
- package/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +3 -0
- package/node_modules/hono/dist/types/router/trie-router/node.d.ts +1 -2
- package/node_modules/hono/dist/types/router/trie-router/router.d.ts +0 -1
- package/node_modules/hono/dist/types/router/utils.d.ts +1 -0
- package/node_modules/hono/dist/types/utils/url.d.ts +4 -0
- package/node_modules/hono/dist/utils/body.js +15 -3
- package/node_modules/hono/dist/utils/cookie.js +1 -1
- package/node_modules/hono/dist/utils/ipaddr.js +5 -3
- package/node_modules/hono/dist/utils/stream.js +7 -1
- package/node_modules/hono/dist/utils/url.js +9 -1
- package/node_modules/hono/package.json +1 -1
- package/node_modules/is-plain-object/README.md +39 -95
- package/node_modules/is-plain-object/dist/is-plain-object.js +0 -2
- package/node_modules/is-plain-object/is-plain-object.d.ts +6 -1
- package/node_modules/is-plain-object/package.json +8 -34
- package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +4 -3
- package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +4 -3
- package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +4 -3
- package/node_modules/jose/dist/types/jwks/remote.d.ts +4 -4
- package/node_modules/jose/dist/types/jws/general/verify.d.ts +3 -3
- package/node_modules/jose/dist/types/jwt/encrypt.d.ts +12 -17
- package/node_modules/jose/dist/types/jwt/sign.d.ts +8 -14
- package/node_modules/jose/dist/types/jwt/unsecured.d.ts +9 -15
- package/node_modules/jose/dist/types/types.d.ts +4 -2
- package/node_modules/jose/dist/webapi/jwe/compact/encrypt.js +30 -8
- package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +8 -4
- package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +6 -13
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +19 -19
- package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +32 -21
- package/node_modules/jose/dist/webapi/jwk/embedded.js +15 -1
- package/node_modules/jose/dist/webapi/jwk/thumbprint.js +11 -5
- package/node_modules/jose/dist/webapi/jwks/local.js +45 -53
- package/node_modules/jose/dist/webapi/jwks/remote.js +82 -103
- package/node_modules/jose/dist/webapi/jws/compact/sign.js +10 -9
- package/node_modules/jose/dist/webapi/jws/flattened/sign.js +2 -1
- package/node_modules/jose/dist/webapi/jws/flattened/verify.js +8 -7
- package/node_modules/jose/dist/webapi/jws/general/sign.js +5 -3
- package/node_modules/jose/dist/webapi/jws/general/verify.js +51 -21
- package/node_modules/jose/dist/webapi/jwt/decrypt.js +7 -9
- package/node_modules/jose/dist/webapi/jwt/encrypt.js +21 -50
- package/node_modules/jose/dist/webapi/jwt/sign.js +8 -41
- package/node_modules/jose/dist/webapi/jwt/unsecured.js +21 -42
- package/node_modules/jose/dist/webapi/key/generate_key_pair.js +6 -2
- package/node_modules/jose/dist/webapi/key/generate_secret.js +7 -6
- package/node_modules/jose/dist/webapi/key/import.js +16 -12
- package/node_modules/jose/dist/webapi/lib/asn1.js +7 -2
- package/node_modules/jose/dist/webapi/lib/content_encryption.js +7 -15
- package/node_modules/jose/dist/webapi/lib/deflate.js +8 -0
- package/node_modules/jose/dist/webapi/lib/helpers.js +1 -1
- package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +85 -28
- package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +45 -13
- package/node_modules/jose/dist/webapi/lib/jwk_metadata.js +20 -0
- package/node_modules/jose/dist/webapi/lib/jws_sign.js +47 -36
- package/node_modules/jose/dist/webapi/lib/jws_verify.js +77 -45
- package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +82 -47
- package/node_modules/jose/dist/webapi/lib/key.js +24 -9
- package/node_modules/jose/dist/webapi/lib/key_management.js +10 -4
- package/node_modules/jose/dist/webapi/lib/key_options.js +6 -0
- package/node_modules/jose/dist/webapi/lib/options.js +26 -0
- package/node_modules/jose/dist/webapi/lib/type_checks.js +11 -13
- package/node_modules/jose/package.json +1 -1
- package/package.json +2 -2
- package/dist/ui/assets/index-0Q2eNytX.js +0 -110
- package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/HISTORY.md +0 -114
|
@@ -13,7 +13,36 @@ const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
|
|
|
13
13
|
const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
|
|
14
14
|
|
|
15
15
|
/** @type {(value: string) => boolean} */
|
|
16
|
-
const isPathCharacter = RegExp.prototype.test.bind(/^[
|
|
16
|
+
const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u)
|
|
17
|
+
|
|
18
|
+
/** @type {(value: string) => boolean} */
|
|
19
|
+
const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u)
|
|
20
|
+
|
|
21
|
+
/** @type {(value: string) => boolean} */
|
|
22
|
+
const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u)
|
|
23
|
+
|
|
24
|
+
const BYTE_HEX = new Array(256)
|
|
25
|
+
{
|
|
26
|
+
const HEX_DIGITS = '0123456789ABCDEF'
|
|
27
|
+
for (let i = 0; i < 256; i++) {
|
|
28
|
+
BYTE_HEX[i] = '%' + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 0xF]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function percentEncodeNonAscii (cp) {
|
|
32
|
+
if (cp < 0x800) {
|
|
33
|
+
return BYTE_HEX[0xC0 | (cp >> 6)] +
|
|
34
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
35
|
+
}
|
|
36
|
+
if (cp < 0x10000) {
|
|
37
|
+
return BYTE_HEX[0xE0 | (cp >> 12)] +
|
|
38
|
+
BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
|
|
39
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
40
|
+
}
|
|
41
|
+
return BYTE_HEX[0xF0 | (cp >> 18)] +
|
|
42
|
+
BYTE_HEX[0x80 | ((cp >> 12) & 0x3F)] +
|
|
43
|
+
BYTE_HEX[0x80 | ((cp >> 6) & 0x3F)] +
|
|
44
|
+
BYTE_HEX[0x80 | (cp & 0x3F)]
|
|
45
|
+
}
|
|
17
46
|
|
|
18
47
|
/**
|
|
19
48
|
* @param {Array<string>} input
|
|
@@ -46,12 +75,14 @@ function stringArrayToHexStripped (input) {
|
|
|
46
75
|
return acc
|
|
47
76
|
}
|
|
48
77
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
/** @type {(value: string) => boolean} */
|
|
79
|
+
const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/)
|
|
80
|
+
|
|
81
|
+
/** @type {(value: string) => boolean} */
|
|
82
|
+
const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/)
|
|
83
|
+
|
|
84
|
+
/** @type {(value: string) => boolean} */
|
|
85
|
+
const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/)
|
|
55
86
|
|
|
56
87
|
/**
|
|
57
88
|
* @param {string} value
|
|
@@ -60,88 +91,104 @@ function stringArrayToHexStripped (input) {
|
|
|
60
91
|
const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
|
|
61
92
|
|
|
62
93
|
/**
|
|
63
|
-
* @param {
|
|
94
|
+
* @param {string} zone
|
|
64
95
|
* @returns {boolean}
|
|
65
96
|
*/
|
|
66
|
-
function
|
|
67
|
-
|
|
97
|
+
function isZoneIdentifier (zone) {
|
|
98
|
+
if (zone.length === 0) return false
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < zone.length; i++) {
|
|
101
|
+
if (isZoneCharacter(zone[i])) continue
|
|
102
|
+
if (zone[i] === '%' && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
|
|
103
|
+
i += 2
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
68
109
|
return true
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* @
|
|
113
|
+
* Compresses the longest run of zero hextets to "::" per RFC 5952. A run of a
|
|
114
|
+
* single zero hextet is left uncompressed. On ties the leftmost run wins.
|
|
115
|
+
*
|
|
116
|
+
* @param {string[]} hextets
|
|
117
|
+
* @returns {string}
|
|
76
118
|
*/
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
119
|
+
function compressIPv6ZeroRun (hextets) {
|
|
120
|
+
let bestStart = -1
|
|
121
|
+
let bestLength = 0
|
|
122
|
+
let runStart = -1
|
|
123
|
+
let runLength = 0
|
|
124
|
+
for (let i = 0; i < hextets.length; i++) {
|
|
125
|
+
if (hextets[i] === '0') {
|
|
126
|
+
if (runStart === -1) runStart = i
|
|
127
|
+
runLength++
|
|
128
|
+
if (runLength > bestLength) {
|
|
129
|
+
bestLength = runLength
|
|
130
|
+
bestStart = runStart
|
|
131
|
+
}
|
|
82
132
|
} else {
|
|
83
|
-
|
|
84
|
-
|
|
133
|
+
runStart = -1
|
|
134
|
+
runLength = 0
|
|
85
135
|
}
|
|
86
|
-
buffer.length = 0
|
|
87
136
|
}
|
|
88
|
-
|
|
137
|
+
|
|
138
|
+
if (bestLength < 2) return hextets.join(':')
|
|
139
|
+
|
|
140
|
+
const head = hextets.slice(0, bestStart).join(':')
|
|
141
|
+
const tail = hextets.slice(bestStart + bestLength).join(':')
|
|
142
|
+
return head + '::' + tail
|
|
89
143
|
}
|
|
90
144
|
|
|
91
145
|
/**
|
|
146
|
+
* Validates an IPv6 address against the alternatives in RFC 3986 section
|
|
147
|
+
* 3.2.2 and returns the same address with leading hextet zeroes removed.
|
|
148
|
+
* An embedded IPv4 address counts as two hextets and is only valid at the end.
|
|
149
|
+
*
|
|
92
150
|
* @param {string} input
|
|
93
|
-
* @returns {
|
|
151
|
+
* @returns {string|undefined}
|
|
94
152
|
*/
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
let consume = consumeHextets
|
|
153
|
+
function normalizeIPv6Address (input) {
|
|
154
|
+
const compression = input.indexOf('::')
|
|
155
|
+
if (compression !== -1 && input.indexOf('::', compression + 1) !== -1) return undefined
|
|
156
|
+
|
|
157
|
+
const left = compression === -1 ? input.split(':') : input.slice(0, compression).split(':')
|
|
158
|
+
const right = compression === -1 ? [] : input.slice(compression + 2).split(':')
|
|
159
|
+
if (compression !== -1) {
|
|
160
|
+
if (left.length === 1 && left[0] === '') left.length = 0
|
|
161
|
+
if (right.length === 1 && right[0] === '') right.length = 0
|
|
162
|
+
}
|
|
106
163
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
// not valid
|
|
117
|
-
output.error = true
|
|
118
|
-
break
|
|
119
|
-
}
|
|
120
|
-
if (i > 0 && input[i - 1] === ':') {
|
|
121
|
-
endipv6Encountered = true
|
|
122
|
-
}
|
|
123
|
-
address.push(':')
|
|
124
|
-
continue
|
|
125
|
-
} else if (cursor === '%') {
|
|
126
|
-
if (!consume(buffer, address, output)) { break }
|
|
127
|
-
// switch to zone detection
|
|
128
|
-
consume = consumeIsZone
|
|
129
|
-
} else {
|
|
130
|
-
buffer.push(cursor)
|
|
164
|
+
const parts = left.concat(right)
|
|
165
|
+
let hextetCount = 0
|
|
166
|
+
for (let i = 0; i < parts.length; i++) {
|
|
167
|
+
const part = parts[i]
|
|
168
|
+
if (part === '') return undefined
|
|
169
|
+
|
|
170
|
+
if (part.indexOf('.') !== -1) {
|
|
171
|
+
if (i !== parts.length - 1 || (compression !== -1 && right.length === 0) || !isIPv4(part)) return undefined
|
|
172
|
+
hextetCount += 2
|
|
131
173
|
continue
|
|
132
174
|
}
|
|
175
|
+
|
|
176
|
+
if (!isHextet(part)) return undefined
|
|
177
|
+
parts[i] = parseInt(part, 16).toString(16)
|
|
178
|
+
hextetCount++
|
|
133
179
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
address.push(buffer.join(''))
|
|
139
|
-
} else {
|
|
140
|
-
address.push(stringArrayToHexStripped(buffer))
|
|
141
|
-
}
|
|
180
|
+
|
|
181
|
+
if (compression === -1) {
|
|
182
|
+
if (hextetCount !== 8) return undefined
|
|
183
|
+
return compressIPv6ZeroRun(parts)
|
|
142
184
|
}
|
|
143
|
-
|
|
144
|
-
|
|
185
|
+
if (hextetCount >= 8) return undefined
|
|
186
|
+
|
|
187
|
+
// expand "::" then re-compress the longest run for a canonical result
|
|
188
|
+
const expanded = parts.slice(0, left.length)
|
|
189
|
+
for (let i = hextetCount; i < 8; i++) expanded.push('0')
|
|
190
|
+
for (let i = left.length; i < parts.length; i++) expanded.push(parts[i])
|
|
191
|
+
return compressIPv6ZeroRun(expanded)
|
|
145
192
|
}
|
|
146
193
|
|
|
147
194
|
/**
|
|
@@ -149,26 +196,49 @@ function getIPV6 (input) {
|
|
|
149
196
|
* @property {string} host - The normalized host.
|
|
150
197
|
* @property {string} [escapedHost] - The escaped host.
|
|
151
198
|
* @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
|
|
199
|
+
* @property {boolean} [isIPVFuture] - Indicates if the host is an IPvFuture literal.
|
|
200
|
+
* @property {boolean} [error] - Indicates if a bracketed IP literal is malformed.
|
|
152
201
|
*/
|
|
153
202
|
|
|
154
203
|
/**
|
|
204
|
+
* Validates and normalizes a bracketed IP literal. Raw zone separators remain
|
|
205
|
+
* accepted for backwards compatibility, while encoded separators and zone
|
|
206
|
+
* contents follow RFC 6874.
|
|
207
|
+
*
|
|
155
208
|
* @param {string} host
|
|
156
209
|
* @returns {NormalizeIPv6Result}
|
|
157
210
|
*/
|
|
158
211
|
function normalizeIPv6 (host) {
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
212
|
+
const bracketed = host[0] === '[' && host[host.length - 1] === ']'
|
|
213
|
+
const hasBracket = host[0] === '[' || host[host.length - 1] === ']'
|
|
214
|
+
if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }
|
|
215
|
+
|
|
216
|
+
let input = bracketed ? host.slice(1, -1) : host
|
|
217
|
+
if (bracketed && isIPvFuture(input)) {
|
|
218
|
+
input = input.toLowerCase()
|
|
219
|
+
return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (findToken(input, ':') < 2) {
|
|
223
|
+
return { host, isIPV6: false, error: bracketed }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let zoneIdentifier = ''
|
|
227
|
+
const zoneSeparator = input.indexOf('%')
|
|
228
|
+
if (zoneSeparator !== -1) {
|
|
229
|
+
const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === '%25' ? 3 : 1
|
|
230
|
+
zoneIdentifier = input.slice(zoneSeparator + separatorLength)
|
|
231
|
+
if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }
|
|
232
|
+
input = input.slice(0, zoneSeparator)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const address = normalizeIPv6Address(input)
|
|
236
|
+
if (address === undefined) return { host, isIPV6: false, error: true }
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
host: address + (zoneIdentifier ? '%' + zoneIdentifier : ''),
|
|
240
|
+
escapedHost: address + (zoneIdentifier ? '%25' + zoneIdentifier : ''),
|
|
241
|
+
isIPV6: true
|
|
172
242
|
}
|
|
173
243
|
}
|
|
174
244
|
|
|
@@ -294,7 +364,7 @@ function reescapeHostDelimiters (host, isIP) {
|
|
|
294
364
|
|
|
295
365
|
/**
|
|
296
366
|
* Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
|
|
297
|
-
* Reserved delimiters such as `%2F`
|
|
367
|
+
* Reserved delimiters such as `%2F` stay escaped; `%2E` is unreserved.
|
|
298
368
|
*
|
|
299
369
|
* @param {string} input
|
|
300
370
|
* @param {boolean} [decodeUnreserved=false]
|
|
@@ -343,7 +413,8 @@ function normalizePathEncoding (input) {
|
|
|
343
413
|
let output = ''
|
|
344
414
|
|
|
345
415
|
for (let i = 0; i < input.length; i++) {
|
|
346
|
-
|
|
416
|
+
const ch = input[i]
|
|
417
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
347
418
|
const hex = input.slice(i + 1, i + 3)
|
|
348
419
|
if (isHexPair(hex)) {
|
|
349
420
|
const normalizedHex = hex.toUpperCase()
|
|
@@ -360,10 +431,225 @@ function normalizePathEncoding (input) {
|
|
|
360
431
|
}
|
|
361
432
|
}
|
|
362
433
|
|
|
363
|
-
if (isPathCharacter(
|
|
364
|
-
output +=
|
|
434
|
+
if (isPathCharacter(ch)) {
|
|
435
|
+
output += ch
|
|
365
436
|
} else {
|
|
366
|
-
|
|
437
|
+
const code = input.charCodeAt(i)
|
|
438
|
+
if (code < 0x80) {
|
|
439
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
440
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
441
|
+
output += percentEncodeNonAscii(code)
|
|
442
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
443
|
+
const low = input.charCodeAt(i + 1)
|
|
444
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
445
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
446
|
+
i++
|
|
447
|
+
} else {
|
|
448
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return output
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Serializes a path without rewriting reserved data. Raw RFC 3986 path
|
|
461
|
+
* characters remain literal, valid escapes are preserved and uppercased, and
|
|
462
|
+
* everything else is UTF-8 percent-encoded. In a path-noscheme, a colon in the
|
|
463
|
+
* first segment must be escaped so the result cannot be parsed as a scheme.
|
|
464
|
+
*
|
|
465
|
+
* @param {string} input
|
|
466
|
+
* @param {boolean} [pathNoScheme=false]
|
|
467
|
+
* @returns {string}
|
|
468
|
+
*/
|
|
469
|
+
function serializePathEncoding (input, pathNoScheme = false) {
|
|
470
|
+
let output = ''
|
|
471
|
+
let firstSegment = pathNoScheme && input[0] !== '/'
|
|
472
|
+
|
|
473
|
+
for (let i = 0; i < input.length; i++) {
|
|
474
|
+
const ch = input[i]
|
|
475
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
476
|
+
const hex = input.slice(i + 1, i + 3)
|
|
477
|
+
if (isHexPair(hex)) {
|
|
478
|
+
output += '%' + hex.toUpperCase()
|
|
479
|
+
i += 2
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (ch === '/') {
|
|
485
|
+
firstSegment = false
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (isPathCharacter(ch) && (ch !== ':' || !firstSegment)) {
|
|
489
|
+
output += ch
|
|
490
|
+
} else {
|
|
491
|
+
const code = input.charCodeAt(i)
|
|
492
|
+
if (code < 0x80) {
|
|
493
|
+
output += BYTE_HEX[code]
|
|
494
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
495
|
+
output += percentEncodeNonAscii(code)
|
|
496
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
497
|
+
const low = input.charCodeAt(i + 1)
|
|
498
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
499
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
500
|
+
i++
|
|
501
|
+
} else {
|
|
502
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return output
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Percent-encodes a URI component using its RFC 3986 literal character set.
|
|
515
|
+
* Existing valid escapes are preserved and normalized to uppercase hex.
|
|
516
|
+
*
|
|
517
|
+
* @param {string} input
|
|
518
|
+
* @param {(value: string) => boolean} isAllowed
|
|
519
|
+
* @returns {string}
|
|
520
|
+
*/
|
|
521
|
+
function encodeComponent (input, isAllowed) {
|
|
522
|
+
let output = ''
|
|
523
|
+
|
|
524
|
+
for (let i = 0; i < input.length; i++) {
|
|
525
|
+
const ch = input[i]
|
|
526
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
527
|
+
const hex = input.slice(i + 1, i + 3)
|
|
528
|
+
if (isHexPair(hex)) {
|
|
529
|
+
output += '%' + hex.toUpperCase()
|
|
530
|
+
i += 2
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (isAllowed(ch)) {
|
|
536
|
+
output += ch
|
|
537
|
+
} else {
|
|
538
|
+
const code = input.charCodeAt(i)
|
|
539
|
+
if (code < 0x80) {
|
|
540
|
+
output += BYTE_HEX[code]
|
|
541
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
542
|
+
output += percentEncodeNonAscii(code)
|
|
543
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
544
|
+
const low = input.charCodeAt(i + 1)
|
|
545
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
546
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
547
|
+
i++
|
|
548
|
+
} else {
|
|
549
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return output
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Encodes userinfo while preserving its RFC 3986 §3.2.1 literal characters.
|
|
562
|
+
* In particular, authority delimiters such as `@`, `/`, `?`, and `#` are data.
|
|
563
|
+
*
|
|
564
|
+
* @param {string} input
|
|
565
|
+
* @returns {string}
|
|
566
|
+
*/
|
|
567
|
+
function encodeUserinfo (input) {
|
|
568
|
+
return encodeComponent(input, isUserinfoCharacter)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Encodes query data using the RFC 3986 §3.4 grammar. A literal `#` must be
|
|
573
|
+
* escaped because it would otherwise begin the fragment component.
|
|
574
|
+
*
|
|
575
|
+
* @param {string} input
|
|
576
|
+
* @returns {string}
|
|
577
|
+
*/
|
|
578
|
+
function encodeQuery (input) {
|
|
579
|
+
return encodeComponent(input, isQueryFragmentCharacter)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Encodes fragment data using the RFC 3986 §3.5 grammar.
|
|
584
|
+
*
|
|
585
|
+
* @param {string} input
|
|
586
|
+
* @returns {string}
|
|
587
|
+
*/
|
|
588
|
+
function encodeFragment (input) {
|
|
589
|
+
return encodeComponent(input, isQueryFragmentCharacter)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function isEscapeSafe (cp) {
|
|
593
|
+
return (
|
|
594
|
+
(cp >= 0x30 && cp <= 0x39) ||
|
|
595
|
+
(cp >= 0x41 && cp <= 0x5A) ||
|
|
596
|
+
(cp >= 0x61 && cp <= 0x7A) ||
|
|
597
|
+
cp === 0x2A || cp === 0x2B || cp === 0x2D || cp === 0x2E ||
|
|
598
|
+
cp === 0x2F || cp === 0x40 || cp === 0x5F
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Normalizes the percent-encoding of a query or fragment component.
|
|
604
|
+
*
|
|
605
|
+
* Like `normalizePathEncoding`, but uses the query/fragment character set
|
|
606
|
+
* (which additionally allows `?`) and decodes `.` since it has no dot-segment
|
|
607
|
+
* meaning outside of a path.
|
|
608
|
+
*
|
|
609
|
+
* @param {string} input
|
|
610
|
+
* @returns {string}
|
|
611
|
+
*/
|
|
612
|
+
function normalizeQueryFragmentEncoding (input) {
|
|
613
|
+
let output = ''
|
|
614
|
+
|
|
615
|
+
for (let i = 0; i < input.length; i++) {
|
|
616
|
+
const ch = input[i]
|
|
617
|
+
if (ch === '%' && i + 2 < input.length) {
|
|
618
|
+
const hex = input.slice(i + 1, i + 3)
|
|
619
|
+
if (isHexPair(hex)) {
|
|
620
|
+
const normalizedHex = hex.toUpperCase()
|
|
621
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
|
|
622
|
+
|
|
623
|
+
if (isUnreserved(decoded)) {
|
|
624
|
+
output += decoded
|
|
625
|
+
} else {
|
|
626
|
+
output += '%' + normalizedHex
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
i += 2
|
|
630
|
+
continue
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (isQueryFragmentCharacter(ch)) {
|
|
635
|
+
output += ch
|
|
636
|
+
} else {
|
|
637
|
+
const code = input.charCodeAt(i)
|
|
638
|
+
if (code < 0x80) {
|
|
639
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code]
|
|
640
|
+
} else if (code < 0xD800 || code > 0xDFFF) {
|
|
641
|
+
output += percentEncodeNonAscii(code)
|
|
642
|
+
} else if (code <= 0xDBFF && i + 1 < input.length) {
|
|
643
|
+
const low = input.charCodeAt(i + 1)
|
|
644
|
+
if (low >= 0xDC00 && low <= 0xDFFF) {
|
|
645
|
+
output += percentEncodeNonAscii(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00))
|
|
646
|
+
i++
|
|
647
|
+
} else {
|
|
648
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
output += percentEncodeNonAscii(0xFFFD)
|
|
652
|
+
}
|
|
367
653
|
}
|
|
368
654
|
}
|
|
369
655
|
|
|
@@ -403,15 +689,21 @@ function recomposeAuthority (component) {
|
|
|
403
689
|
const uriTokens = []
|
|
404
690
|
|
|
405
691
|
if (component.userinfo !== undefined) {
|
|
406
|
-
uriTokens.push(component.userinfo)
|
|
692
|
+
uriTokens.push(encodeUserinfo(component.userinfo))
|
|
407
693
|
uriTokens.push('@')
|
|
408
694
|
}
|
|
409
695
|
|
|
410
696
|
if (component.host !== undefined) {
|
|
411
|
-
let host =
|
|
697
|
+
let host = component.host
|
|
412
698
|
if (!isIPv4(host)) {
|
|
413
|
-
|
|
414
|
-
if (ipV6res.isIPV6
|
|
699
|
+
let ipV6res = normalizeIPv6(host)
|
|
700
|
+
if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
|
|
701
|
+
// Decode only unreserved bytes, once. In particular, keep %25 encoded
|
|
702
|
+
// so it cannot introduce a second escape during recomposition.
|
|
703
|
+
host = normalizePercentEncoding(host, true)
|
|
704
|
+
ipV6res = normalizeIPv6(host)
|
|
705
|
+
}
|
|
706
|
+
if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
|
|
415
707
|
host = `[${ipV6res.escapedHost}]`
|
|
416
708
|
} else {
|
|
417
709
|
host = reescapeHostDelimiters(host, false)
|
|
@@ -434,6 +726,11 @@ module.exports = {
|
|
|
434
726
|
reescapeHostDelimiters,
|
|
435
727
|
normalizePercentEncoding,
|
|
436
728
|
normalizePathEncoding,
|
|
729
|
+
serializePathEncoding,
|
|
730
|
+
normalizeQueryFragmentEncoding,
|
|
731
|
+
encodeUserinfo,
|
|
732
|
+
encodeQuery,
|
|
733
|
+
encodeFragment,
|
|
437
734
|
escapePreservingEscapes,
|
|
438
735
|
removeDotSegments,
|
|
439
736
|
isIPv4,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('tape')
|
|
4
|
+
const fastURI = require('..')
|
|
5
|
+
|
|
6
|
+
test('userinfo serialization cannot terminate the authority', (t) => {
|
|
7
|
+
const uri = fastURI.serialize({
|
|
8
|
+
scheme: 'http',
|
|
9
|
+
userinfo: 'attacker.example/',
|
|
10
|
+
host: 'trusted.example',
|
|
11
|
+
path: '/'
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
t.equal(uri, 'http://attacker.example%2F@trusted.example/', 'slash is encoded as userinfo data')
|
|
15
|
+
|
|
16
|
+
const reparsed = fastURI.parse(uri)
|
|
17
|
+
t.equal(reparsed.host, 'trusted.example', 'reparsing retains the supplied host')
|
|
18
|
+
t.equal(reparsed.userinfo, 'attacker.example%2F', 'encoded slash remains in userinfo')
|
|
19
|
+
t.equal(
|
|
20
|
+
fastURI.serialize({ userinfo: 'user@/?:#[]\\', host: 'example.test' }),
|
|
21
|
+
'//user%40%2F%3F:%23%5B%5D%5C@example.test',
|
|
22
|
+
'all userinfo component delimiters are encoded as data'
|
|
23
|
+
)
|
|
24
|
+
t.end()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('query and fragment serialization cannot inject component delimiters', (t) => {
|
|
28
|
+
const uri = fastURI.serialize({
|
|
29
|
+
scheme: 'x',
|
|
30
|
+
path: '/resource',
|
|
31
|
+
query: 'key=value#still-query',
|
|
32
|
+
fragment: 'first#still-fragment'
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
t.equal(
|
|
36
|
+
uri,
|
|
37
|
+
'x:/resource?key=value%23still-query#first%23still-fragment',
|
|
38
|
+
'raw hashes are encoded within their supplied components'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const reparsed = fastURI.parse(uri)
|
|
42
|
+
t.equal(reparsed.query, 'key=value%23still-query', 'hash remains query data')
|
|
43
|
+
t.equal(reparsed.fragment, 'first%23still-fragment', 'hash remains fragment data')
|
|
44
|
+
t.end()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('component serializers preserve each RFC 3986 literal character set', (t) => {
|
|
48
|
+
const unreservedAndSubDelims = "AZaz09-._~!$&'()*+,;="
|
|
49
|
+
const userinfo = unreservedAndSubDelims + ':'
|
|
50
|
+
const queryOrFragment = unreservedAndSubDelims + ':@/?'
|
|
51
|
+
|
|
52
|
+
t.equal(
|
|
53
|
+
fastURI.serialize({ userinfo, host: 'example.test' }),
|
|
54
|
+
'//' + userinfo + '@example.test',
|
|
55
|
+
'userinfo literals are preserved'
|
|
56
|
+
)
|
|
57
|
+
t.equal(
|
|
58
|
+
fastURI.serialize({ query: queryOrFragment }),
|
|
59
|
+
'?' + queryOrFragment,
|
|
60
|
+
'query literals are preserved'
|
|
61
|
+
)
|
|
62
|
+
t.equal(
|
|
63
|
+
fastURI.serialize({ fragment: queryOrFragment }),
|
|
64
|
+
'#' + queryOrFragment,
|
|
65
|
+
'fragment literals are preserved'
|
|
66
|
+
)
|
|
67
|
+
t.end()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('component serializers preserve escapes and encode Unicode as UTF-8', (t) => {
|
|
71
|
+
t.equal(
|
|
72
|
+
fastURI.serialize({ userinfo: 'café/%2f%GG', host: 'example.test' }),
|
|
73
|
+
'//caf%C3%A9%2F%2F%25GG@example.test',
|
|
74
|
+
'userinfo preserves valid escapes, uppercases hex, and escapes malformed percent data'
|
|
75
|
+
)
|
|
76
|
+
t.equal(
|
|
77
|
+
fastURI.serialize({ query: '日本/%2f#%GG' }),
|
|
78
|
+
'?%E6%97%A5%E6%9C%AC/%2F%23%25GG',
|
|
79
|
+
'query uses UTF-8 and does not double encode valid escapes'
|
|
80
|
+
)
|
|
81
|
+
t.equal(
|
|
82
|
+
fastURI.serialize({ fragment: '😀/%3f#%GG' }),
|
|
83
|
+
'#%F0%9F%98%80/%3F%23%25GG',
|
|
84
|
+
'fragment uses UTF-8 and does not double encode valid escapes'
|
|
85
|
+
)
|
|
86
|
+
t.equal(
|
|
87
|
+
fastURI.serialize({ query: 'Kſ' }),
|
|
88
|
+
'?%E2%84%AA%C5%BF',
|
|
89
|
+
'Unicode characters that case-fold to ASCII are still UTF-8 encoded'
|
|
90
|
+
)
|
|
91
|
+
t.end()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('parsed URI serialization remains stable', (t) => {
|
|
95
|
+
const input = 'x://user:pass@example.test/a%2Fb?x=a/b?c&y=%23#frag/a?b%23'
|
|
96
|
+
const serialized = fastURI.serialize(fastURI.parse(input))
|
|
97
|
+
|
|
98
|
+
t.equal(serialized, input, 'an already normalized parsed URI round-trips unchanged')
|
|
99
|
+
t.equal(
|
|
100
|
+
fastURI.serialize(fastURI.parse(serialized)),
|
|
101
|
+
serialized,
|
|
102
|
+
'repeated parse and serialize cycles are stable'
|
|
103
|
+
)
|
|
104
|
+
t.end()
|
|
105
|
+
})
|