cdk-common 2.1.33 → 2.1.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/.jsii CHANGED
@@ -3943,7 +3943,7 @@
3943
3943
  "stability": "experimental"
3944
3944
  },
3945
3945
  "homepage": "https://github.com/neilkuan/cdk-common.git",
3946
- "jsiiVersion": "5.9.37 (build 5176c0d)",
3946
+ "jsiiVersion": "5.9.39 (build cdf85b4)",
3947
3947
  "keywords": [
3948
3948
  "aws",
3949
3949
  "aws-cdk",
@@ -12984,6 +12984,6 @@
12984
12984
  "symbolId": "src/main:LambdaArmFunctionProps"
12985
12985
  }
12986
12986
  },
12987
- "version": "2.1.33",
12988
- "fingerprint": "TZIlr4chMoocsv+OgvSlkzU61k1HDWVIPNW4uujRGqA="
12987
+ "version": "2.1.35",
12988
+ "fingerprint": "NFDErK/8HKOBmjwZW7YyqvfsSvofOt2FOB5Or/og6Os="
12989
12989
  }
package/lib/main.js CHANGED
@@ -38,5 +38,5 @@ class LambdaArmFunction extends constructs_1.Construct {
38
38
  }
39
39
  exports.LambdaArmFunction = LambdaArmFunction;
40
40
  _a = JSII_RTTI_SYMBOL_1;
41
- LambdaArmFunction[_a] = { fqn: "cdk-common.LambdaArmFunction", version: "2.1.33" };
41
+ LambdaArmFunction[_a] = { fqn: "cdk-common.LambdaArmFunction", version: "2.1.35" };
42
42
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9tYWluLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUEsbUNBQW1DO0FBQ25DLGlEQUFpRDtBQUNqRCwyQ0FBdUM7QUFLdkMsTUFBYSxpQkFBa0IsU0FBUSxzQkFBUztJQUU5QyxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQTRCO1FBQ3BFLEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFFakIsTUFBTSxlQUFlLEdBQUcsSUFBSSxHQUFHLENBQXlCO1lBQ3RELENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsbUNBQW1DLENBQUM7WUFDakUsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLFdBQVcsRUFBRSxtQ0FBbUMsQ0FBQztZQUNqRSxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLG1DQUFtQyxDQUFDO1lBQ2pFLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsa0NBQWtDLENBQUM7WUFDaEUsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLFdBQVcsRUFBRSxrQ0FBa0MsQ0FBQztZQUNoRSxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLGtDQUFrQyxDQUFDO1lBQ2hFLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsa0NBQWtDLENBQUM7WUFDaEUsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLE9BQU8sRUFBRSw4QkFBOEIsQ0FBQztZQUN4RCxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsT0FBTyxFQUFFLDhCQUE4QixDQUFDO1lBQ3hELENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxRQUFRLEVBQUUsNkJBQTZCLENBQUM7WUFDeEQsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLFFBQVEsRUFBRSwrQkFBK0IsQ0FBQztZQUMxRCxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsUUFBUSxFQUFFLCtCQUErQixDQUFDO1NBQzNELENBQUMsQ0FBQztRQUVILE1BQU0sT0FBTyxHQUFHLGVBQWUsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ25ELElBQUksT0FBTyxFQUFFLENBQUM7WUFDWixHQUFHLENBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDL0MsQ0FBQzthQUFNLENBQUM7WUFDTixNQUFNLElBQUksS0FBSyxDQUFDLG1CQUFtQixLQUFLLENBQUMsT0FBTyx1R0FBdUcsQ0FBQyxDQUFDO1FBQzNKLENBQUM7UUFFRCxJQUFJLENBQUMsY0FBYyxHQUFHLElBQUksTUFBTSxDQUFDLFFBQVEsQ0FBQyxJQUFJLEVBQUUsZ0JBQWdCLEVBQUU7WUFDaEUsWUFBWSxFQUFFLEtBQUssQ0FBQyxZQUFZLElBQUksTUFBTSxDQUFDLFlBQVksQ0FBQyxNQUFNO1lBQzlELEdBQUcsS0FBSztTQUNULENBQUMsQ0FBQztJQUNMLENBQUM7O0FBL0JILDhDQWdDQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIGNkayBmcm9tICdhd3MtY2RrLWxpYic7XG5pbXBvcnQgKiBhcyBsYW1iZGEgZnJvbSAnYXdzLWNkay1saWIvYXdzLWxhbWJkYSc7XG5pbXBvcnQgeyBDb25zdHJ1Y3QgfSBmcm9tICdjb25zdHJ1Y3RzJztcbmV4cG9ydCBpbnRlcmZhY2UgTGFtYmRhQXJtRnVuY3Rpb25Qcm9wcyBleHRlbmRzIGxhbWJkYS5GdW5jdGlvblByb3BzIHtcblxufVxuXG5leHBvcnQgY2xhc3MgTGFtYmRhQXJtRnVuY3Rpb24gZXh0ZW5kcyBDb25zdHJ1Y3Qge1xuICBwdWJsaWMgcmVhZG9ubHkgbGFtYmRhRnVuY3Rpb246IGxhbWJkYS5GdW5jdGlvbjtcbiAgY29uc3RydWN0b3Ioc2NvcGU6IENvbnN0cnVjdCwgaWQ6IHN0cmluZywgcHJvcHM6TGFtYmRhQXJtRnVuY3Rpb25Qcm9wcykge1xuICAgIHN1cGVyKHNjb3BlLCBpZCk7XG5cbiAgICBjb25zdCBydW50aW1lV2FybmluZ3MgPSBuZXcgTWFwPGxhbWJkYS5SdW50aW1lLCBzdHJpbmc+KFtcbiAgICAgIFtsYW1iZGEuUnVudGltZS5OT0RFSlNfMjJfWCwgJ1lvdSBhcmUgdXNpbmcgTm9kZS5qcyAyMi54IGF0IEFSTSddLFxuICAgICAgW2xhbWJkYS5SdW50aW1lLk5PREVKU18yMF9YLCAnWW91IGFyZSB1c2luZyBOb2RlLmpzIDIwLnggYXQgQVJNJ10sXG4gICAgICBbbGFtYmRhLlJ1bnRpbWUuTk9ERUpTXzE4X1gsICdZb3UgYXJlIHVzaW5nIE5vZGUuanMgMTgueCBhdCBBUk0nXSxcbiAgICAgIFtsYW1iZGEuUnVudGltZS5QWVRIT05fM18xMywgJ1lvdSBhcmUgdXNpbmcgUHl0aG9uIDMuMTMgYXQgQVJNJ10sXG4gICAgICBbbGFtYmRhLlJ1bnRpbWUuUFlUSE9OXzNfMTIsICdZb3UgYXJlIHVzaW5nIFB5dGhvbiAzLjEyIGF0IEFSTSddLFxuICAgICAgW2xhbWJkYS5SdW50aW1lLlBZVEhPTl8zXzExLCAnWW91IGFyZSB1c2luZyBQeXRob24gMy4xMSBhdCBBUk0nXSxcbiAgICAgIFtsYW1iZGEuUnVudGltZS5QWVRIT05fM18xMCwgJ1lvdSBhcmUgdXNpbmcgUHl0aG9uIDMuMTAgYXQgQVJNJ10sXG4gICAgICBbbGFtYmRhLlJ1bnRpbWUuSkFWQV8yMSwgJ1lvdSBhcmUgdXNpbmcgSmF2YSAyMSBhdCBBUk0nXSxcbiAgICAgIFtsYW1iZGEuUnVudGltZS5KQVZBXzE3LCAnWW91IGFyZSB1c2luZyBKYXZhIDE3IGF0IEFSTSddLFxuICAgICAgW2xhbWJkYS5SdW50aW1lLkRPVE5FVF84LCAnWW91IGFyZSB1c2luZyAuTkVUIDggYXQgQVJNJ10sXG4gICAgICBbbGFtYmRhLlJ1bnRpbWUuUlVCWV8zXzQsICdZb3UgYXJlIHVzaW5nIFJ1YnkgMy40IGF0IEFSTSddLFxuICAgICAgW2xhbWJkYS5SdW50aW1lLlJVQllfM18zLCAnWW91IGFyZSB1c2luZyBSdWJ5IDMuMyBhdCBBUk0nXSxcbiAgICBdKTtcblxuICAgIGNvbnN0IHdhcm5pbmcgPSBydW50aW1lV2FybmluZ3MuZ2V0KHByb3BzLnJ1bnRpbWUpO1xuICAgIGlmICh3YXJuaW5nKSB7XG4gICAgICBjZGsuQW5ub3RhdGlvbnMub2YodGhpcykuYWRkV2FybmluZyh3YXJuaW5nKTtcbiAgICB9IGVsc2Uge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKGBJbnZhbGlkIFJ1bnRpbWUgJHtwcm9wcy5ydW50aW1lfSBhdCBBUk0sIFNlZSBodHRwczovL2RvY3MuYXdzLmFtYXpvbi5jb20vbGFtYmRhL2xhdGVzdC9kZy9mb3VuZGF0aW9uLWFyY2guaHRtbD9pY21waWQ9ZG9jc19sYW1iZGFfcnNzYCk7XG4gICAgfVxuXG4gICAgdGhpcy5sYW1iZGFGdW5jdGlvbiA9IG5ldyBsYW1iZGEuRnVuY3Rpb24odGhpcywgJ0xhbWJkYUZ1bmN0aW9uJywge1xuICAgICAgYXJjaGl0ZWN0dXJlOiBwcm9wcy5hcmNoaXRlY3R1cmUgPz8gbGFtYmRhLkFyY2hpdGVjdHVyZS5BUk1fNjQsXG4gICAgICAuLi5wcm9wcyxcbiAgICB9KTtcbiAgfVxufVxuIl19
@@ -14,6 +14,11 @@ on:
14
14
  - 'docs/**'
15
15
  - '*.md'
16
16
 
17
+ # This allows a subsequently queued workflow run to interrupt previous runs
18
+ concurrency:
19
+ group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
20
+ cancel-in-progress: true
21
+
17
22
  permissions:
18
23
  contents: read
19
24
 
@@ -24,11 +29,11 @@ jobs:
24
29
  permissions:
25
30
  contents: read
26
31
  steps:
27
- - uses: actions/checkout@v4
32
+ - uses: actions/checkout@v6
28
33
  with:
29
34
  persist-credentials: false
30
35
 
31
- - uses: actions/setup-node@v4
36
+ - uses: actions/setup-node@v6
32
37
  with:
33
38
  node-version: '10'
34
39
  cache: 'npm'
@@ -66,11 +71,11 @@ jobs:
66
71
  permissions:
67
72
  contents: read
68
73
  steps:
69
- - uses: actions/checkout@v4
74
+ - uses: actions/checkout@v6
70
75
  with:
71
76
  persist-credentials: false
72
77
 
73
- - uses: actions/setup-node@v4
78
+ - uses: actions/setup-node@v6
74
79
  with:
75
80
  node-version: '24'
76
81
  cache: 'npm'
@@ -80,21 +85,21 @@ jobs:
80
85
  - name: Install dependencies
81
86
  run: |
82
87
  npm install --ignore-scripts
83
-
88
+
84
89
  - if: ${{ matrix.os == 'windows-latest' }}
85
90
  run: npx playwright install winldd
86
91
 
87
92
  - name: Run browser tests
88
93
  run: |
89
94
  npm run test:browser:${{ matrix.browser }}
90
-
95
+
91
96
  test:
92
97
  needs:
93
98
  - test-regression-check-node10
94
99
  permissions:
95
100
  contents: write
96
101
  pull-requests: write
97
- uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
102
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6
98
103
  with:
99
104
  license-check: true
100
105
  lint: true
@@ -0,0 +1,19 @@
1
+ name: Lock Threads
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 0 1 * *'
6
+ workflow_dispatch:
7
+
8
+ concurrency:
9
+ group: lock
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ lock-threads:
16
+ permissions:
17
+ issues: write
18
+ pull-requests: write
19
+ uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
@@ -21,4 +21,4 @@ jobs:
21
21
  test:
22
22
  permissions:
23
23
  contents: read
24
- uses: fastify/workflows/.github/workflows/plugins-ci-package-manager.yml@v5
24
+ uses: fastify/workflows/.github/workflows/plugins-ci-package-manager.yml@v6
@@ -1,9 +1,7 @@
1
1
  Copyright (c) 2011-2021, Gary Court until https://github.com/garycourt/uri-js/commit/a1acf730b4bba3f1097c9f52e7d9d3aba8cdcaae
2
- Copyright (c) 2021-present The Fastify team
2
+ Copyright (c) 2021-present The Fastify team <https://github.com/fastify/fastify#team>
3
3
  All rights reserved.
4
4
 
5
- The Fastify team members are listed at https://github.com/fastify/fastify#team.
6
-
7
5
  Redistribution and use in source and binary forms, with or without
8
6
  modification, are permitted provided that the following conditions are met:
9
7
  * Redistributions of source code must retain the above copyright
@@ -1,13 +1,9 @@
1
1
  # fast-uri
2
2
 
3
- <div align="center">
4
-
5
3
  [![NPM version](https://img.shields.io/npm/v/fast-uri.svg?style=flat)](https://www.npmjs.com/package/fast-uri)
6
4
  [![CI](https://github.com/fastify/fast-uri/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fast-uri/actions/workflows/ci.yml)
7
5
  [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
8
6
 
9
- </div>
10
-
11
7
  Dependency-free RFC 3986 URI toolbox.
12
8
 
13
9
  ## Usage
@@ -16,6 +12,8 @@ Dependency-free RFC 3986 URI toolbox.
16
12
 
17
13
  All of the above functions can accept an additional options argument that is an object that can contain one or more of the following properties:
18
14
 
15
+ Malformed authorities and out-of-range ports are reported through the parsed component's `error` field. `normalize()` leaves malformed string inputs unchanged, and `equal()` returns `false` when either string input is malformed.
16
+
19
17
  * `scheme` (string)
20
18
  Indicates the scheme that the URI should be treated as, overriding the URI's normal scheme parsing behavior.
21
19
 
@@ -70,6 +68,17 @@ uri.resolve("uri://a/b/c/d?q", "../../g")
70
68
  "uri://a/g"
71
69
  ```
72
70
 
71
+ ### Normalize
72
+
73
+ ```js
74
+ const uri = require('fast-uri')
75
+ uri.normalize('http://example.com/a%2Fb')
76
+ // Output
77
+ "http://example.com/a%2Fb"
78
+ ```
79
+
80
+ Reserved path escapes such as `%2F` and `%2E` are preserved as path data during normalization and comparison.
81
+
73
82
  ### Equal
74
83
 
75
84
  ```js
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require('./lib/utils')
3
+ const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require('./lib/utils')
4
4
  const { SCHEMES, getSchemeHandler } = require('./lib/schemes')
5
5
 
6
6
  /**
@@ -11,7 +11,7 @@ const { SCHEMES, getSchemeHandler } = require('./lib/schemes')
11
11
  */
12
12
  function normalize (uri, options) {
13
13
  if (typeof uri === 'string') {
14
- uri = /** @type {T} */ (serialize(parse(uri, options), options))
14
+ uri = /** @type {T} */ (normalizeString(uri, options))
15
15
  } else if (typeof uri === 'object') {
16
16
  uri = /** @type {T} */ (parse(serialize(uri, options), options))
17
17
  }
@@ -106,21 +106,10 @@ function resolveComponent (base, relative, options, skipNormalization) {
106
106
  * @returns {boolean}
107
107
  */
108
108
  function equal (uriA, uriB, options) {
109
- if (typeof uriA === 'string') {
110
- uriA = unescape(uriA)
111
- uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true })
112
- } else if (typeof uriA === 'object') {
113
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true })
114
- }
115
-
116
- if (typeof uriB === 'string') {
117
- uriB = unescape(uriB)
118
- uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true })
119
- } else if (typeof uriB === 'object') {
120
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true })
121
- }
109
+ const normalizedA = normalizeComparableURI(uriA, options)
110
+ const normalizedB = normalizeComparableURI(uriB, options)
122
111
 
123
- return uriA.toLowerCase() === uriB.toLowerCase()
112
+ return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase()
124
113
  }
125
114
 
126
115
  /**
@@ -156,13 +145,13 @@ function serialize (cmpts, opts) {
156
145
 
157
146
  if (component.path !== undefined) {
158
147
  if (!options.skipEscape) {
159
- component.path = escape(component.path)
148
+ component.path = escapePreservingEscapes(component.path)
160
149
 
161
150
  if (component.scheme !== undefined) {
162
151
  component.path = component.path.split('%3A').join(':')
163
152
  }
164
153
  } else {
165
- component.path = unescape(component.path)
154
+ component.path = normalizePercentEncoding(component.path)
166
155
  }
167
156
  }
168
157
 
@@ -213,12 +202,29 @@ function serialize (cmpts, opts) {
213
202
 
214
203
  const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
215
204
 
205
+ /**
206
+ * @param {import('./types/index').URIComponent} parsed
207
+ * @param {RegExpMatchArray} matches
208
+ * @returns {string|undefined}
209
+ */
210
+ function getParseError (parsed, matches) {
211
+ if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') {
212
+ return 'URI path must start with "/" when authority is present.'
213
+ }
214
+
215
+ if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) {
216
+ return 'URI port is malformed.'
217
+ }
218
+
219
+ return undefined
220
+ }
221
+
216
222
  /**
217
223
  * @param {string} uri
218
224
  * @param {import('./types/index').Options} [opts]
219
- * @returns
225
+ * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }}
220
226
  */
221
- function parse (uri, opts) {
227
+ function parseWithStatus (uri, opts) {
222
228
  const options = Object.assign({}, opts)
223
229
  /** @type {import('./types/index').URIComponent} */
224
230
  const parsed = {
@@ -231,6 +237,8 @@ function parse (uri, opts) {
231
237
  fragment: undefined
232
238
  }
233
239
 
240
+ let malformedAuthorityOrPort = false
241
+
234
242
  let isIP = false
235
243
  if (options.reference === 'suffix') {
236
244
  if (options.scheme) {
@@ -256,6 +264,13 @@ function parse (uri, opts) {
256
264
  if (isNaN(parsed.port)) {
257
265
  parsed.port = matches[5]
258
266
  }
267
+
268
+ const parseError = getParseError(parsed, matches)
269
+ if (parseError !== undefined) {
270
+ parsed.error = parsed.error || parseError
271
+ malformedAuthorityOrPort = true
272
+ }
273
+
259
274
  if (parsed.host) {
260
275
  const ipv4result = isIPv4(parsed.host)
261
276
  if (ipv4result === false) {
@@ -304,14 +319,18 @@ function parse (uri, opts) {
304
319
  parsed.scheme = unescape(parsed.scheme)
305
320
  }
306
321
  if (parsed.host !== undefined) {
307
- parsed.host = unescape(parsed.host)
322
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
308
323
  }
309
324
  }
310
325
  if (parsed.path) {
311
- parsed.path = escape(unescape(parsed.path))
326
+ parsed.path = normalizePathEncoding(parsed.path)
312
327
  }
313
328
  if (parsed.fragment) {
314
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment))
329
+ try {
330
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment))
331
+ } catch {
332
+ parsed.error = parsed.error || 'URI malformed'
333
+ }
315
334
  }
316
335
  }
317
336
 
@@ -322,7 +341,54 @@ function parse (uri, opts) {
322
341
  } else {
323
342
  parsed.error = parsed.error || 'URI can not be parsed.'
324
343
  }
325
- return parsed
344
+ return { parsed, malformedAuthorityOrPort }
345
+ }
346
+
347
+ /**
348
+ * @param {string} uri
349
+ * @param {import('./types/index').Options} [opts]
350
+ * @returns
351
+ */
352
+ function parse (uri, opts) {
353
+ return parseWithStatus(uri, opts).parsed
354
+ }
355
+
356
+ /**
357
+ * @param {string} uri
358
+ * @param {import('./types/index').Options} [opts]
359
+ * @returns {string}
360
+ */
361
+ function normalizeString (uri, opts) {
362
+ return normalizeStringWithStatus(uri, opts).normalized
363
+ }
364
+
365
+ /**
366
+ * @param {string} uri
367
+ * @param {import('./types/index').Options} [opts]
368
+ * @returns {{ normalized: string, malformedAuthorityOrPort: boolean }}
369
+ */
370
+ function normalizeStringWithStatus (uri, opts) {
371
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
372
+ return {
373
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
374
+ malformedAuthorityOrPort
375
+ }
376
+ }
377
+
378
+ /**
379
+ * @param {import ('./types/index').URIComponent|string} uri
380
+ * @param {import('./types/index').Options} [opts]
381
+ * @returns {string|undefined}
382
+ */
383
+ function normalizeComparableURI (uri, opts) {
384
+ if (typeof uri === 'string') {
385
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts)
386
+ return malformedAuthorityOrPort ? undefined : normalized
387
+ }
388
+
389
+ if (typeof uri === 'object') {
390
+ return serialize(uri, opts)
391
+ }
326
392
  }
327
393
 
328
394
  const fastUri = {
@@ -6,6 +6,15 @@ const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\d
6
6
  /** @type {(value: string) => boolean} */
7
7
  const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u)
8
8
 
9
+ /** @type {(value: string) => boolean} */
10
+ const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
11
+
12
+ /** @type {(value: string) => boolean} */
13
+ const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
14
+
15
+ /** @type {(value: string) => boolean} */
16
+ const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
17
+
9
18
  /**
10
19
  * @param {Array<string>} input
11
20
  * @returns {string}
@@ -264,31 +273,126 @@ function removeDotSegments (path) {
264
273
  }
265
274
 
266
275
  /**
267
- * @param {import('../types/index').URIComponent} component
268
- * @param {boolean} esc
269
- * @returns {import('../types/index').URIComponent}
276
+ * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
277
+ * After the URI regex parses, these characters cannot be literal in the host
278
+ * field, so any that appear after decoding came from percent-encoding and
279
+ * must be restored to prevent authority structure changes.
280
+ *
281
+ * @param {string} host
282
+ * @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
283
+ * @returns {string}
270
284
  */
271
- function normalizeComponentEncoding (component, esc) {
272
- const func = esc !== true ? escape : unescape
273
- if (component.scheme !== undefined) {
274
- component.scheme = func(component.scheme)
275
- }
276
- if (component.userinfo !== undefined) {
277
- component.userinfo = func(component.userinfo)
278
- }
279
- if (component.host !== undefined) {
280
- component.host = func(component.host)
285
+ const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' }
286
+ const HOST_DELIM_RE = /[@/?#:]/g
287
+ const HOST_DELIM_NO_COLON_RE = /[@/?#]/g
288
+
289
+ function reescapeHostDelimiters (host, isIP) {
290
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE
291
+ re.lastIndex = 0
292
+ return host.replace(re, (ch) => HOST_DELIMS[ch])
293
+ }
294
+
295
+ /**
296
+ * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
297
+ * Reserved delimiters such as `%2F` and `%2E` stay escaped.
298
+ *
299
+ * @param {string} input
300
+ * @param {boolean} [decodeUnreserved=false]
301
+ * @returns {string}
302
+ */
303
+ function normalizePercentEncoding (input, decodeUnreserved = false) {
304
+ if (input.indexOf('%') === -1) {
305
+ return input
281
306
  }
282
- if (component.path !== undefined) {
283
- component.path = func(component.path)
307
+
308
+ let output = ''
309
+
310
+ for (let i = 0; i < input.length; i++) {
311
+ if (input[i] === '%' && i + 2 < input.length) {
312
+ const hex = input.slice(i + 1, i + 3)
313
+ if (isHexPair(hex)) {
314
+ const normalizedHex = hex.toUpperCase()
315
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
316
+
317
+ if (decodeUnreserved && isUnreserved(decoded)) {
318
+ output += decoded
319
+ } else {
320
+ output += '%' + normalizedHex
321
+ }
322
+
323
+ i += 2
324
+ continue
325
+ }
326
+ }
327
+
328
+ output += input[i]
284
329
  }
285
- if (component.query !== undefined) {
286
- component.query = func(component.query)
330
+
331
+ return output
332
+ }
333
+
334
+ /**
335
+ * Normalizes path data without turning reserved escapes into live path syntax.
336
+ * Valid escapes are uppercased, raw unsafe characters are escaped, and only
337
+ * unreserved bytes that are not `.` are decoded.
338
+ *
339
+ * @param {string} input
340
+ * @returns {string}
341
+ */
342
+ function normalizePathEncoding (input) {
343
+ let output = ''
344
+
345
+ for (let i = 0; i < input.length; i++) {
346
+ if (input[i] === '%' && i + 2 < input.length) {
347
+ const hex = input.slice(i + 1, i + 3)
348
+ if (isHexPair(hex)) {
349
+ const normalizedHex = hex.toUpperCase()
350
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
351
+
352
+ if (decoded !== '.' && isUnreserved(decoded)) {
353
+ output += decoded
354
+ } else {
355
+ output += '%' + normalizedHex
356
+ }
357
+
358
+ i += 2
359
+ continue
360
+ }
361
+ }
362
+
363
+ if (isPathCharacter(input[i])) {
364
+ output += input[i]
365
+ } else {
366
+ output += escape(input[i])
367
+ }
287
368
  }
288
- if (component.fragment !== undefined) {
289
- component.fragment = func(component.fragment)
369
+
370
+ return output
371
+ }
372
+
373
+ /**
374
+ * Escapes a component while preserving existing valid percent escapes.
375
+ *
376
+ * @param {string} input
377
+ * @returns {string}
378
+ */
379
+ function escapePreservingEscapes (input) {
380
+ let output = ''
381
+
382
+ for (let i = 0; i < input.length; i++) {
383
+ if (input[i] === '%' && i + 2 < input.length) {
384
+ const hex = input.slice(i + 1, i + 3)
385
+ if (isHexPair(hex)) {
386
+ output += '%' + hex.toUpperCase()
387
+ i += 2
388
+ continue
389
+ }
390
+ }
391
+
392
+ output += escape(input[i])
290
393
  }
291
- return component
394
+
395
+ return output
292
396
  }
293
397
 
294
398
  /**
@@ -310,7 +414,7 @@ function recomposeAuthority (component) {
310
414
  if (ipV6res.isIPV6 === true) {
311
415
  host = `[${ipV6res.escapedHost}]`
312
416
  } else {
313
- host = component.host
417
+ host = reescapeHostDelimiters(host, false)
314
418
  }
315
419
  }
316
420
  uriTokens.push(host)
@@ -327,7 +431,10 @@ function recomposeAuthority (component) {
327
431
  module.exports = {
328
432
  nonSimpleDomain,
329
433
  recomposeAuthority,
330
- normalizeComponentEncoding,
434
+ reescapeHostDelimiters,
435
+ normalizePercentEncoding,
436
+ normalizePathEncoding,
437
+ escapePreservingEscapes,
331
438
  removeDotSegments,
332
439
  isIPv4,
333
440
  isUUID,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fast-uri",
3
3
  "description": "Dependency-free RFC 3986 URI toolbox",
4
- "version": "3.1.0",
4
+ "version": "3.1.2",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -58,12 +58,11 @@
58
58
  "test:typescript": "tsd"
59
59
  },
60
60
  "devDependencies": {
61
- "@fastify/pre-commit": "^2.1.0",
62
61
  "ajv": "^8.16.0",
63
62
  "eslint": "^9.17.0",
64
- "neostandard": "^0.12.0",
63
+ "neostandard": "^0.13.0",
65
64
  "playwright-test": "^14.1.12",
66
65
  "tape": "^5.8.1",
67
- "tsd": "^0.32.0"
66
+ "tsd": "^0.33.0"
68
67
  }
69
68
  }
@@ -106,3 +106,12 @@ test('WSS Equal', (t) => {
106
106
  runTest(t, suite)
107
107
  t.end()
108
108
  })
109
+
110
+ test('URI Equals tolerates malformed fragments', (t) => {
111
+ t.equal(
112
+ fastURI.equal('http://example.com/#%E0%A4A', 'http://example.com/#%E0%A4A'),
113
+ true,
114
+ 'malformed fragment does not throw during equality checks'
115
+ )
116
+ t.end()
117
+ })
@@ -150,6 +150,11 @@ test('URI parse', (t) => {
150
150
  t.equal(components.query, undefined, 'query')
151
151
  t.equal(components.fragment, '%0D', 'fragment')
152
152
 
153
+ // malformed percent-encoded fragment must not throw
154
+ components = fastURI.parse('http://example.com/#%E0%A4A')
155
+ t.equal(components.error, 'URI malformed', 'malformed fragment errors')
156
+ t.equal(components.fragment, '%E0%A4A', 'malformed fragment is preserved')
157
+
153
158
  // all
154
159
  components = fastURI.parse('uri://user:pass@example.com:123/one/two.three?q1=a1&q2=a2#body')
155
160
  t.equal(components.error, undefined, 'all errors')
@@ -76,3 +76,12 @@ test('URN Resolving', (t) => {
76
76
  t.equal(fastURI.resolve('urn:some:other:prop', 'urn:some:ip:prop'), 'urn:some:ip:prop', 'urn:some:ip:prop')
77
77
  t.end()
78
78
  })
79
+
80
+ test('URI Resolving tolerates malformed fragments', (t) => {
81
+ t.equal(
82
+ fastURI.resolve('http://base.com/', 'http://example.com/#%E0%A4A'),
83
+ 'http://example.com/#%E0%A4A',
84
+ 'malformed fragment does not throw during resolve'
85
+ )
86
+ t.end()
87
+ })
@@ -0,0 +1,39 @@
1
+ 'use strict'
2
+
3
+ const test = require('tape')
4
+ const fastURI = require('..')
5
+
6
+ test('parse preserves reserved path escapes as data', (t) => {
7
+ const components = fastURI.parse('http://example.com/a%2Fb/public/%2e%2e/admin')
8
+
9
+ t.equal(components.path, '/a%2Fb/public/%2E%2E/admin')
10
+ t.end()
11
+ })
12
+
13
+ test('normalize preserves percent-encoded path separators and dot segments', (t) => {
14
+ t.equal(
15
+ fastURI.normalize('http://example.com/public/%2e%2e/admin'),
16
+ 'http://example.com/public/%2E%2E/admin'
17
+ )
18
+
19
+ t.equal(
20
+ fastURI.normalize('http://example.com/a%2Fb'),
21
+ 'http://example.com/a%2Fb'
22
+ )
23
+
24
+ t.end()
25
+ })
26
+
27
+ test('equal does not treat reserved path escapes as live path syntax', (t) => {
28
+ t.equal(
29
+ fastURI.equal('http://example.com/public/%2e%2e/admin', 'http://example.com/admin', {}),
30
+ false
31
+ )
32
+
33
+ t.equal(
34
+ fastURI.equal('http://example.com/a%2Fb', 'http://example.com/a/b', {}),
35
+ false
36
+ )
37
+
38
+ t.end()
39
+ })
@@ -0,0 +1,133 @@
1
+ 'use strict'
2
+
3
+ const test = require('tape')
4
+ const fastURI = require('..')
5
+
6
+ test('parse marks malformed authority and port inputs as errors', (t) => {
7
+ const malformedCases = [
8
+ {
9
+ input: 'http://[::1]foo',
10
+ expectedError: 'URI path must start with "/" when authority is present.'
11
+ },
12
+ {
13
+ input: 'http://[::1]:80abc/path',
14
+ expectedError: 'URI path must start with "/" when authority is present.'
15
+ },
16
+ {
17
+ input: 'http://example.com:80abc/path',
18
+ expectedError: 'URI path must start with "/" when authority is present.'
19
+ },
20
+ {
21
+ input: 'http://[::1]:65536',
22
+ expectedError: 'URI port is malformed.'
23
+ }
24
+ ]
25
+
26
+ t.plan(malformedCases.length)
27
+
28
+ malformedCases.forEach(({ input, expectedError }) => {
29
+ t.equal(fastURI.parse(input).error, expectedError, input)
30
+ })
31
+ })
32
+
33
+ test('normalize does not canonicalize malformed URLs into different valid URLs', (t) => {
34
+ const malformedCases = [
35
+ 'http://[::1]foo',
36
+ 'http://[::1]:80abc/path',
37
+ 'http://example.com:80abc/path',
38
+ 'http://[::1]:65536'
39
+ ]
40
+
41
+ t.plan(malformedCases.length)
42
+
43
+ malformedCases.forEach((input) => {
44
+ t.equal(fastURI.normalize(input), input, input)
45
+ })
46
+ })
47
+
48
+ test('equal returns false when either side is malformed', (t) => {
49
+ const malformedPairs = [
50
+ ['http://[::1]foo', 'http://[::1]/foo'],
51
+ ['http://[::1]:80abc/path', 'http://[::1]/abc/path'],
52
+ ['http://example.com:80abc/path', 'http://example.com/abc/path'],
53
+ ['http://[::1]:65536', 'http://[::1]:65536/']
54
+ ]
55
+
56
+ t.plan(malformedPairs.length)
57
+
58
+ malformedPairs.forEach(([left, right]) => {
59
+ t.equal(fastURI.equal(left, right), false, `${left} != ${right}`)
60
+ })
61
+ })
62
+
63
+ test('normalize preserves encoded authority delimiters in host', (t) => {
64
+ const cases = [
65
+ ['http://trusted.com%40evil.com/', 'http://trusted.com%40evil.com/'],
66
+ ['http://example.com%3A8080/', 'http://example.com%3A8080/'],
67
+ ['http://example.com%2Fevil.com/path', 'http://example.com%2Fevil.com/path'],
68
+ ['http://example.com%23fragment/path', 'http://example.com%23fragment/path'],
69
+ ['http://example.com%3Fq=evil/path', 'http://example.com%3Fq=evil/path'],
70
+ ['http://user%3Apass%40evil.com/', 'http://user%3Apass%40evil.com/'],
71
+ ['http://user@trusted.com%40evil.com/', 'http://user@trusted.com%40evil.com/'],
72
+ ['https://trusted.com%40evil.com/', 'https://trusted.com%40evil.com/'],
73
+ ['ws://trusted.com%40evil.com/chat', 'ws://trusted.com%40evil.com/chat'],
74
+ ['wss://trusted.com%40evil.com/chat', 'wss://trusted.com%40evil.com/chat']
75
+ ]
76
+
77
+ t.plan(cases.length)
78
+
79
+ cases.forEach(([input, expected]) => {
80
+ t.equal(fastURI.normalize(input), expected, input)
81
+ })
82
+ })
83
+
84
+ test('parse preserves encoded authority delimiters in host', (t) => {
85
+ const cases = [
86
+ ['http://trusted.com%40evil.com/', 'trusted.com%40evil.com'],
87
+ ['http://example.com%3A8080/', 'example.com%3A8080'],
88
+ ['http://user%3Apass%40evil.com/', 'user%3Apass%40evil.com']
89
+ ]
90
+
91
+ t.plan(cases.length)
92
+
93
+ cases.forEach(([input, expectedHost]) => {
94
+ t.equal(fastURI.parse(input).host, expectedHost, input)
95
+ })
96
+ })
97
+
98
+ test('equal returns false when encoded delimiters differ from live delimiters', (t) => {
99
+ const pairs = [
100
+ ['http://trusted.com%40evil.com/', 'http://trusted.com@evil.com/'],
101
+ ['http://example.com%3A8080/', 'http://example.com:8080/']
102
+ ]
103
+
104
+ t.plan(pairs.length)
105
+
106
+ pairs.forEach(([left, right]) => {
107
+ t.equal(fastURI.equal(left, right, {}), false, `${left} != ${right}`)
108
+ })
109
+ })
110
+
111
+ test('resolve preserves encoded authority delimiters', (t) => {
112
+ const result = fastURI.resolve('http://base.com/', '//trusted.com%40evil.com/path')
113
+ const parsed = fastURI.parse(result)
114
+
115
+ t.plan(1)
116
+ t.notEqual(parsed.host, 'evil.com', '//trusted.com%40evil.com/path')
117
+ })
118
+
119
+ test('serialize escapes authority delimiters in host field', (t) => {
120
+ const result = fastURI.serialize({ scheme: 'http', host: 'trusted.com@evil.com', path: '/' })
121
+ const parsed = fastURI.parse(result)
122
+
123
+ t.plan(1)
124
+ t.notEqual(parsed.host, 'evil.com', 'host: trusted.com@evil.com')
125
+ })
126
+
127
+ test('normalize does not double-decode %2540 into a live @', (t) => {
128
+ const result = fastURI.normalize('http://trusted.com%2540evil.com/')
129
+ const parsed = fastURI.parse(result)
130
+
131
+ t.plan(1)
132
+ t.notEqual(parsed.host, 'trusted.com@evil.com', 'http://trusted.com%2540evil.com/')
133
+ })
package/package.json CHANGED
@@ -88,7 +88,7 @@
88
88
  "access": "public",
89
89
  "provenance": true
90
90
  },
91
- "version": "2.1.33",
91
+ "version": "2.1.35",
92
92
  "jest": {
93
93
  "coverageProvider": "v8",
94
94
  "testMatch": [
@@ -1,21 +0,0 @@
1
- # Number of days of inactivity before an issue becomes stale
2
- daysUntilStale: 15
3
- # Number of days of inactivity before a stale issue is closed
4
- daysUntilClose: 7
5
- # Issues with these labels will never be considered stale
6
- exemptLabels:
7
- - "discussion"
8
- - "feature request"
9
- - "bug"
10
- - "help wanted"
11
- - "plugin suggestion"
12
- - "good first issue"
13
- # Label to use when marking an issue as stale
14
- staleLabel: stale
15
- # Comment to post when marking an issue as stale. Set to `false` to disable
16
- markComment: >
17
- This issue has been automatically marked as stale because it has not had
18
- recent activity. It will be closed if no further activity occurs. Thank you
19
- for your contributions.
20
- # Comment to post when closing a stale issue. Set to `false` to disable
21
- closeComment: false
@@ -1,8 +0,0 @@
1
- comment: |
2
- Hello! Thank you for contributing!
3
- It appears that you have changed the code, but the tests that verify your change are missing. Could you please add them?
4
- fileExtensions:
5
- - '.ts'
6
- - '.js'
7
-
8
- testDir: 'test'