@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.
Files changed (171) hide show
  1. package/dist/control-plane/index.mjs +248 -58
  2. package/dist/mcp-server/http.mjs +408 -65
  3. package/dist/ui/assets/{index-CkWaRck1.css → index-BYVb_0oG.css} +1 -1
  4. package/dist/ui/assets/index-CsrM6RQr.js +110 -0
  5. package/dist/ui/index.html +2 -2
  6. package/node_modules/@hap/core/dist/index.d.mts +258 -9
  7. package/node_modules/@hap/core/dist/index.d.ts +258 -9
  8. package/node_modules/@hap/core/dist/index.js +227 -5
  9. package/node_modules/@hap/core/dist/index.mjs +213 -4
  10. package/node_modules/@hap/core/package.json +3 -2
  11. package/node_modules/@hap/core/src/did-key.ts +126 -0
  12. package/node_modules/@hap/core/src/identity.ts +21 -5
  13. package/node_modules/@hap/core/src/index.ts +3 -0
  14. package/node_modules/@hap/core/src/mandate.ts +207 -0
  15. package/node_modules/@hap/core/src/receipt.ts +74 -0
  16. package/node_modules/@hap/core/src/types.ts +81 -5
  17. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/accept.js +54 -0
  18. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/charset.js +10 -34
  19. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/encoding.js +9 -30
  20. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/language.js +15 -36
  21. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/mediaType.js +16 -134
  22. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/LICENSE +22 -0
  23. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/README.md +71 -0
  24. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.d.ts +46 -0
  25. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.js +176 -0
  26. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.js.map +1 -0
  27. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/package.json +52 -0
  28. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/package.json +16 -10
  29. package/node_modules/@types/node/README.md +1 -1
  30. package/node_modules/@types/node/buffer.buffer.d.ts +5 -5
  31. package/node_modules/@types/node/crypto.d.ts +2 -2
  32. package/node_modules/@types/node/http.d.ts +16 -0
  33. package/node_modules/@types/node/http2.d.ts +10 -2
  34. package/node_modules/@types/node/package.json +2 -2
  35. package/node_modules/@types/node/process.d.ts +52 -0
  36. package/node_modules/@types/node/quic.d.ts +210 -28
  37. package/node_modules/@types/node/sqlite.d.ts +6 -2
  38. package/node_modules/@types/node/test.d.ts +36 -0
  39. package/node_modules/fast-uri/index.js +201 -48
  40. package/node_modules/fast-uri/lib/schemes.js +9 -4
  41. package/node_modules/fast-uri/lib/utils.js +388 -91
  42. package/node_modules/fast-uri/package.json +1 -1
  43. package/node_modules/fast-uri/test/component-safe-serialization.test.js +105 -0
  44. package/node_modules/fast-uri/test/equal.test.js +31 -3
  45. package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
  46. package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
  47. package/node_modules/fast-uri/test/ipv6-validation.test.js +90 -0
  48. package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
  49. package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
  50. package/node_modules/fast-uri/test/parse.test.js +7 -3
  51. package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
  52. package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
  53. package/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
  54. package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
  55. package/node_modules/fast-uri/test/security.test.js +75 -3
  56. package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
  57. package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
  58. package/node_modules/hono/dist/cjs/client/client.js +25 -11
  59. package/node_modules/hono/dist/cjs/client/utils.js +4 -1
  60. package/node_modules/hono/dist/cjs/context.js +4 -0
  61. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +36 -2
  62. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +1 -1
  63. package/node_modules/hono/dist/cjs/helper/ssg/utils.js +30 -10
  64. package/node_modules/hono/dist/cjs/jsx/dom/render.js +2 -0
  65. package/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
  66. package/node_modules/hono/dist/cjs/middleware/cors/index.js +1 -1
  67. package/node_modules/hono/dist/cjs/middleware/csrf/index.js +1 -1
  68. package/node_modules/hono/dist/cjs/middleware/etag/digest.js +1 -1
  69. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -3
  70. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +3 -1
  71. package/node_modules/hono/dist/cjs/request.js +8 -4
  72. package/node_modules/hono/dist/cjs/router/linear-router/router.js +7 -2
  73. package/node_modules/hono/dist/cjs/router/pattern-router/router.js +3 -9
  74. package/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +10 -3
  75. package/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +47 -83
  76. package/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +2 -1
  77. package/node_modules/hono/dist/cjs/router/trie-router/node.js +46 -69
  78. package/node_modules/hono/dist/cjs/router/trie-router/router.js +3 -11
  79. package/node_modules/hono/dist/cjs/router/utils.js +27 -0
  80. package/node_modules/hono/dist/cjs/utils/body.js +15 -3
  81. package/node_modules/hono/dist/cjs/utils/cookie.js +1 -1
  82. package/node_modules/hono/dist/cjs/utils/ipaddr.js +5 -3
  83. package/node_modules/hono/dist/cjs/utils/stream.js +7 -1
  84. package/node_modules/hono/dist/cjs/utils/url.js +9 -1
  85. package/node_modules/hono/dist/client/client.js +25 -11
  86. package/node_modules/hono/dist/client/utils.js +4 -1
  87. package/node_modules/hono/dist/context.js +4 -0
  88. package/node_modules/hono/dist/helper/accepts/accepts.js +36 -2
  89. package/node_modules/hono/dist/helper/ssg/ssg.js +1 -1
  90. package/node_modules/hono/dist/helper/ssg/utils.js +30 -10
  91. package/node_modules/hono/dist/jsx/dom/render.js +2 -0
  92. package/node_modules/hono/dist/middleware/cache/index.js +1 -1
  93. package/node_modules/hono/dist/middleware/cors/index.js +1 -1
  94. package/node_modules/hono/dist/middleware/csrf/index.js +1 -1
  95. package/node_modules/hono/dist/middleware/etag/digest.js +1 -1
  96. package/node_modules/hono/dist/middleware/etag/index.js +3 -3
  97. package/node_modules/hono/dist/middleware/pretty-json/index.js +3 -1
  98. package/node_modules/hono/dist/request.js +8 -4
  99. package/node_modules/hono/dist/router/linear-router/router.js +7 -2
  100. package/node_modules/hono/dist/router/pattern-router/router.js +3 -9
  101. package/node_modules/hono/dist/router/reg-exp-router/node.js +6 -2
  102. package/node_modules/hono/dist/router/reg-exp-router/router.js +53 -84
  103. package/node_modules/hono/dist/router/reg-exp-router/trie.js +2 -1
  104. package/node_modules/hono/dist/router/trie-router/node.js +46 -69
  105. package/node_modules/hono/dist/router/trie-router/router.js +3 -11
  106. package/node_modules/hono/dist/router/utils.js +5 -0
  107. package/node_modules/hono/dist/types/context.d.ts +4 -0
  108. package/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +3 -0
  109. package/node_modules/hono/dist/types/router/trie-router/node.d.ts +1 -2
  110. package/node_modules/hono/dist/types/router/trie-router/router.d.ts +0 -1
  111. package/node_modules/hono/dist/types/router/utils.d.ts +1 -0
  112. package/node_modules/hono/dist/types/utils/url.d.ts +4 -0
  113. package/node_modules/hono/dist/utils/body.js +15 -3
  114. package/node_modules/hono/dist/utils/cookie.js +1 -1
  115. package/node_modules/hono/dist/utils/ipaddr.js +5 -3
  116. package/node_modules/hono/dist/utils/stream.js +7 -1
  117. package/node_modules/hono/dist/utils/url.js +9 -1
  118. package/node_modules/hono/package.json +1 -1
  119. package/node_modules/is-plain-object/README.md +39 -95
  120. package/node_modules/is-plain-object/dist/is-plain-object.js +0 -2
  121. package/node_modules/is-plain-object/is-plain-object.d.ts +6 -1
  122. package/node_modules/is-plain-object/package.json +8 -34
  123. package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +4 -3
  124. package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +4 -3
  125. package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +4 -3
  126. package/node_modules/jose/dist/types/jwks/remote.d.ts +4 -4
  127. package/node_modules/jose/dist/types/jws/general/verify.d.ts +3 -3
  128. package/node_modules/jose/dist/types/jwt/encrypt.d.ts +12 -17
  129. package/node_modules/jose/dist/types/jwt/sign.d.ts +8 -14
  130. package/node_modules/jose/dist/types/jwt/unsecured.d.ts +9 -15
  131. package/node_modules/jose/dist/types/types.d.ts +4 -2
  132. package/node_modules/jose/dist/webapi/jwe/compact/encrypt.js +30 -8
  133. package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +8 -4
  134. package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +6 -13
  135. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +19 -19
  136. package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +32 -21
  137. package/node_modules/jose/dist/webapi/jwk/embedded.js +15 -1
  138. package/node_modules/jose/dist/webapi/jwk/thumbprint.js +11 -5
  139. package/node_modules/jose/dist/webapi/jwks/local.js +45 -53
  140. package/node_modules/jose/dist/webapi/jwks/remote.js +82 -103
  141. package/node_modules/jose/dist/webapi/jws/compact/sign.js +10 -9
  142. package/node_modules/jose/dist/webapi/jws/flattened/sign.js +2 -1
  143. package/node_modules/jose/dist/webapi/jws/flattened/verify.js +8 -7
  144. package/node_modules/jose/dist/webapi/jws/general/sign.js +5 -3
  145. package/node_modules/jose/dist/webapi/jws/general/verify.js +51 -21
  146. package/node_modules/jose/dist/webapi/jwt/decrypt.js +7 -9
  147. package/node_modules/jose/dist/webapi/jwt/encrypt.js +21 -50
  148. package/node_modules/jose/dist/webapi/jwt/sign.js +8 -41
  149. package/node_modules/jose/dist/webapi/jwt/unsecured.js +21 -42
  150. package/node_modules/jose/dist/webapi/key/generate_key_pair.js +6 -2
  151. package/node_modules/jose/dist/webapi/key/generate_secret.js +7 -6
  152. package/node_modules/jose/dist/webapi/key/import.js +16 -12
  153. package/node_modules/jose/dist/webapi/lib/asn1.js +7 -2
  154. package/node_modules/jose/dist/webapi/lib/content_encryption.js +7 -15
  155. package/node_modules/jose/dist/webapi/lib/deflate.js +8 -0
  156. package/node_modules/jose/dist/webapi/lib/helpers.js +1 -1
  157. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +85 -28
  158. package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +45 -13
  159. package/node_modules/jose/dist/webapi/lib/jwk_metadata.js +20 -0
  160. package/node_modules/jose/dist/webapi/lib/jws_sign.js +47 -36
  161. package/node_modules/jose/dist/webapi/lib/jws_verify.js +77 -45
  162. package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +82 -47
  163. package/node_modules/jose/dist/webapi/lib/key.js +24 -9
  164. package/node_modules/jose/dist/webapi/lib/key_management.js +10 -4
  165. package/node_modules/jose/dist/webapi/lib/key_options.js +6 -0
  166. package/node_modules/jose/dist/webapi/lib/options.js +26 -0
  167. package/node_modules/jose/dist/webapi/lib/type_checks.js +11 -13
  168. package/node_modules/jose/package.json +1 -1
  169. package/package.json +2 -2
  170. package/dist/ui/assets/index-0Q2eNytX.js +0 -110
  171. 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(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
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
- * @typedef {Object} GetIPV6Result
51
- * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
52
- * @property {string} address - The parsed IPv6 address.
53
- * @property {string} [zone] - The zone identifier, if present.
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 {Array<string>} buffer
94
+ * @param {string} zone
64
95
  * @returns {boolean}
65
96
  */
66
- function consumeIsZone (buffer) {
67
- buffer.length = 0
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
- * @param {Array<string>} buffer
73
- * @param {Array<string>} address
74
- * @param {GetIPV6Result} output
75
- * @returns {boolean}
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 consumeHextets (buffer, address, output) {
78
- if (buffer.length) {
79
- const hex = stringArrayToHexStripped(buffer)
80
- if (hex !== '') {
81
- address.push(hex)
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
- output.error = true
84
- return false
133
+ runStart = -1
134
+ runLength = 0
85
135
  }
86
- buffer.length = 0
87
136
  }
88
- return true
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 {GetIPV6Result}
151
+ * @returns {string|undefined}
94
152
  */
95
- function getIPV6 (input) {
96
- let tokenCount = 0
97
- const output = { error: false, address: '', zone: '' }
98
- /** @type {Array<string>} */
99
- const address = []
100
- /** @type {Array<string>} */
101
- const buffer = []
102
- let endipv6Encountered = false
103
- let endIpv6 = false
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
- for (let i = 0; i < input.length; i++) {
108
- const cursor = input[i]
109
- if (cursor === '[' || cursor === ']') { continue }
110
- if (cursor === ':') {
111
- if (endipv6Encountered === true) {
112
- endIpv6 = true
113
- }
114
- if (!consume(buffer, address, output)) { break }
115
- if (++tokenCount > 7) {
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
- if (buffer.length) {
135
- if (consume === consumeIsZone) {
136
- output.zone = buffer.join('')
137
- } else if (endIpv6) {
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
- output.address = address.join('')
144
- return output
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
- if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
160
- const ipv6 = getIPV6(host)
161
-
162
- if (!ipv6.error) {
163
- let newHost = ipv6.address
164
- let escapedHost = ipv6.address
165
- if (ipv6.zone) {
166
- newHost += '%' + ipv6.zone
167
- escapedHost += '%25' + ipv6.zone
168
- }
169
- return { host: newHost, isIPV6: true, escapedHost }
170
- } else {
171
- return { host, isIPV6: false }
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` and `%2E` stay escaped.
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
- if (input[i] === '%' && i + 2 < input.length) {
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(input[i])) {
364
- output += input[i]
434
+ if (isPathCharacter(ch)) {
435
+ output += ch
365
436
  } else {
366
- output += escape(input[i])
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 = unescape(component.host)
697
+ let host = component.host
412
698
  if (!isIPv4(host)) {
413
- const ipV6res = normalizeIPv6(host)
414
- if (ipV6res.isIPV6 === true) {
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,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fast-uri",
3
3
  "description": "Dependency-free RFC 3986 URI toolbox",
4
- "version": "3.1.5",
4
+ "version": "3.1.6",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -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
+ })