find-my-way
Advanced tools
| version: 2 | ||
| updates: | ||
| - package-ecosystem: "github-actions" | ||
| directory: "/" | ||
| commit-message: | ||
| # Prefix all commit messages with "chore: " | ||
| prefix: "chore" | ||
| schedule: | ||
| interval: "monthly" | ||
| open-pull-requests-limit: 10 | ||
| - package-ecosystem: "npm" | ||
| directory: "/" | ||
| commit-message: | ||
| # Prefix all commit messages with "chore: " | ||
| prefix: "chore" | ||
| schedule: | ||
| interval: "weekly" | ||
| open-pull-requests-limit: 10 | ||
| groups: | ||
| # Production dependencies without breaking changes | ||
| dependencies: | ||
| dependency-type: "production" | ||
| update-types: | ||
| - "minor" | ||
| - "patch" | ||
| # Production dependencies with breaking changes | ||
| dependencies-major: | ||
| dependency-type: "production" | ||
| update-types: | ||
| - "major" | ||
| # Development dependencies | ||
| dev-dependencies: | ||
| dependency-type: "development" |
| const t = require('tap') | ||
| const test = t.test | ||
| const FindMyWay = require('..') | ||
| const proxyquire = require('proxyquire') | ||
| const HandlerStorage = require('../lib/handler-storage') | ||
| const Constrainer = require('../lib/constrainer') | ||
| const { safeDecodeURIComponent } = require('../lib/url-sanitizer') | ||
| const acceptVersionStrategy = require('../lib/strategies/accept-version') | ||
| const httpMethodStrategy = require('../lib/strategies/http-method') | ||
| test('FULL_PATH_REGEXP and OPTIONAL_PARAM_REGEXP should be considered safe', (t) => { | ||
| t.plan(1) | ||
| t.doesNotThrow(() => require('..')) | ||
| }) | ||
| test('should throw an error for unsafe FULL_PATH_REGEXP', (t) => { | ||
| t.plan(1) | ||
| t.throws(() => proxyquire('..', { | ||
| 'safe-regex2': () => false | ||
| }), new Error('the FULL_PATH_REGEXP is not safe, update this module')) | ||
| }) | ||
| test('Should throw an error for unsafe OPTIONAL_PARAM_REGEXP', (t) => { | ||
| t.plan(1) | ||
| let callCount = 0 | ||
| t.throws(() => proxyquire('..', { | ||
| 'safe-regex2': () => { | ||
| return ++callCount < 2 | ||
| } | ||
| }), new Error('the OPTIONAL_PARAM_REGEXP is not safe, update this module')) | ||
| }) | ||
| test('double colon does not define parametric node', (t) => { | ||
| t.plan(2) | ||
| const findMyWay = FindMyWay() | ||
| findMyWay.on('GET', '/::id', () => {}) | ||
| const route1 = findMyWay.findRoute('GET', '/::id') | ||
| t.strictSame(route1.params, []) | ||
| findMyWay.on('GET', '/:foo(\\d+)::bar', () => {}) | ||
| const route2 = findMyWay.findRoute('GET', '/:foo(\\d+)::bar') | ||
| t.strictSame(route2.params, ['foo']) | ||
| }) | ||
| test('case insensitive static routes', (t) => { | ||
| t.plan(3) | ||
| const findMyWay = FindMyWay({ | ||
| caseSensitive: false | ||
| }) | ||
| findMyWay.on('GET', '/foo', () => {}) | ||
| findMyWay.on('GET', '/foo/bar', () => {}) | ||
| findMyWay.on('GET', '/foo/bar/baz', () => {}) | ||
| t.ok(findMyWay.findRoute('GET', '/FoO')) | ||
| t.ok(findMyWay.findRoute('GET', '/FOo/Bar')) | ||
| t.ok(findMyWay.findRoute('GET', '/fOo/Bar/bAZ')) | ||
| }) | ||
| test('wildcard must be the last character in the route', (t) => { | ||
| t.plan(3) | ||
| const expectedError = new Error('Wildcard must be the last character in the route') | ||
| const findMyWay = FindMyWay() | ||
| findMyWay.on('GET', '*', () => {}) | ||
| t.throws(() => findMyWay.findRoute('GET', '*1'), expectedError) | ||
| t.throws(() => findMyWay.findRoute('GET', '*/'), expectedError) | ||
| t.throws(() => findMyWay.findRoute('GET', '*?'), expectedError) | ||
| }) | ||
| test('does not find the route if maxParamLength is exceeded', t => { | ||
| t.plan(2) | ||
| const findMyWay = FindMyWay({ | ||
| maxParamLength: 2 | ||
| }) | ||
| findMyWay.on('GET', '/:id(\\d+)', () => {}) | ||
| t.equal(findMyWay.find('GET', '/123'), null) | ||
| t.ok(findMyWay.find('GET', '/12')) | ||
| }) | ||
| test('Should check if a regex is safe to use', (t) => { | ||
| t.plan(1) | ||
| const findMyWay = FindMyWay() | ||
| // we must pass a safe regex to register the route | ||
| // findRoute will still throws the expected assertion error if we try to access it with unsafe reggex | ||
| findMyWay.on('GET', '/test/:id(\\d+)', () => {}) | ||
| const unSafeRegex = /(x+x+)+y/ | ||
| t.throws(() => findMyWay.findRoute('GET', `/test/:id(${unSafeRegex.toString()})`), { | ||
| message: "The regex '(/(x+x+)+y/)' is not safe!" | ||
| }) | ||
| }) | ||
| test('Disable safe regex check', (t) => { | ||
| t.plan(1) | ||
| const findMyWay = FindMyWay({ allowUnsafeRegex: true }) | ||
| const unSafeRegex = /(x+x+)+y/ | ||
| findMyWay.on('GET', `/test2/:id(${unSafeRegex.toString()})`, () => {}) | ||
| t.doesNotThrow(() => findMyWay.findRoute('GET', `/test2/:id(${unSafeRegex.toString()})`)) | ||
| }) | ||
| test('throws error if no strategy registered for constraint key', (t) => { | ||
| t.plan(2) | ||
| const constrainer = new Constrainer() | ||
| const error = new Error('No strategy registered for constraint key invalid-constraint') | ||
| t.throws(() => constrainer.newStoreForConstraint('invalid-constraint'), error) | ||
| t.throws(() => constrainer.validateConstraints({ 'invalid-constraint': 'foo' }), error) | ||
| }) | ||
| test('throws error if pass an undefined constraint value', (t) => { | ||
| t.plan(1) | ||
| const constrainer = new Constrainer() | ||
| const error = new Error('Can\'t pass an undefined constraint value, must pass null or no key at all') | ||
| t.throws(() => constrainer.validateConstraints({ key: undefined }), error) | ||
| }) | ||
| test('Constrainer.noteUsage', (t) => { | ||
| t.plan(3) | ||
| const constrainer = new Constrainer() | ||
| t.equal(constrainer.strategiesInUse.size, 0) | ||
| constrainer.noteUsage() | ||
| t.equal(constrainer.strategiesInUse.size, 0) | ||
| constrainer.noteUsage({ host: 'fastify.io' }) | ||
| t.equal(constrainer.strategiesInUse.size, 1) | ||
| }) | ||
| test('Cannot derive constraints without active strategies.', (t) => { | ||
| t.plan(1) | ||
| const constrainer = new Constrainer() | ||
| const before = constrainer.deriveSyncConstraints | ||
| constrainer._buildDeriveConstraints() | ||
| t.sameStrict(constrainer.deriveSyncConstraints, before) | ||
| }) | ||
| test('getMatchingHandler should return null if not compiled', (t) => { | ||
| t.plan(1) | ||
| const handlerStorage = new HandlerStorage() | ||
| t.equal(handlerStorage.getMatchingHandler({ foo: 'bar' }), null) | ||
| }) | ||
| test('safeDecodeURIComponent should replace %3x to null for every x that is not a valid lowchar', (t) => { | ||
| t.plan(1) | ||
| t.equal(safeDecodeURIComponent('Hello%3xWorld'), 'HellonullWorld') | ||
| }) | ||
| test('SemVerStore version should be a string', (t) => { | ||
| t.plan(1) | ||
| const Storage = acceptVersionStrategy.storage | ||
| t.throws(() => new Storage().set(1), new TypeError('Version should be a string')) | ||
| }) | ||
| test('SemVerStore.maxMajor should increase automatically', (t) => { | ||
| t.plan(3) | ||
| const Storage = acceptVersionStrategy.storage | ||
| const storage = new Storage() | ||
| t.equal(storage.maxMajor, 0) | ||
| storage.set('2') | ||
| t.equal(storage.maxMajor, 2) | ||
| storage.set('1') | ||
| t.equal(storage.maxMajor, 2) | ||
| }) | ||
| test('SemVerStore.maxPatches should increase automatically', (t) => { | ||
| t.plan(3) | ||
| const Storage = acceptVersionStrategy.storage | ||
| const storage = new Storage() | ||
| storage.set('2.0.0') | ||
| t.sameStrict(storage.maxPatches, { '2.0': 0 }) | ||
| storage.set('2.0.2') | ||
| t.sameStrict(storage.maxPatches, { '2.0': 2 }) | ||
| storage.set('2.0.1') | ||
| t.sameStrict(storage.maxPatches, { '2.0': 2 }) | ||
| }) | ||
| test('Major version must be a numeric value', t => { | ||
| t.plan(1) | ||
| const findMyWay = FindMyWay() | ||
| t.throws(() => findMyWay.on('GET', '/test', { constraints: { version: 'x' } }, () => {}), | ||
| new TypeError('Major version must be a numeric value')) | ||
| }) | ||
| test('httpMethodStrategy storage handles set and get operations correctly', (t) => { | ||
| t.plan(2) | ||
| const storage = httpMethodStrategy.storage() | ||
| t.equal(storage.get('foo'), null) | ||
| storage.set('foo', { bar: 'baz' }) | ||
| t.strictSame(storage.get('foo'), { bar: 'baz' }) | ||
| }) | ||
| test('if buildPrettyMeta argument is undefined, will return an object', (t) => { | ||
| t.plan(1) | ||
| const findMyWay = FindMyWay() | ||
| t.sameStrict(findMyWay.buildPrettyMeta(), {}) | ||
| }) |
| name: Node CI | ||
| on: [push, pull_request] | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| - next | ||
| pull_request: | ||
@@ -15,13 +20,28 @@ permissions: | ||
| matrix: | ||
| node-version: [14.x, 16.x, 18.x, 20.x] | ||
| os: [ubuntu-latest, windows-latest, macOS-latest] | ||
| node-version: | ||
| - 14 | ||
| - 16 | ||
| - 18 | ||
| - 20 | ||
| - 21 | ||
| - 22 | ||
| os: | ||
| - ubuntu-latest | ||
| - windows-latest | ||
| - macOS-latest | ||
| exclude: | ||
| - os: windows-latest | ||
| node-version: 14.x | ||
| node-version: 14 | ||
| - os: macos-latest | ||
| node-version: 14 | ||
| - os: macos-latest | ||
| node-version: 16 | ||
| - os: windows-latest | ||
| node-version: 22 | ||
| steps: | ||
| - uses: actions/checkout@v3 | ||
| - uses: actions/checkout@v4 | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v3 | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
@@ -45,1 +65,15 @@ node-version: ${{ matrix.node-version }} | ||
| npm run test:typescript | ||
| automerge: | ||
| if: > | ||
| github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' | ||
| needs: test | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| actions: write | ||
| pull-requests: write | ||
| contents: write | ||
| steps: | ||
| - uses: fastify/github-action-merge-dependabot@9e7bfb249c69139d7bdcd8d984f9665edd49020b # v3.10.1 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} |
+27
-37
@@ -28,3 +28,3 @@ 'use strict' | ||
| const assert = require('assert') | ||
| const assert = require('node:assert') | ||
| const querystring = require('fast-querystring') | ||
@@ -119,3 +119,3 @@ const isRegexSafe = require('safe-regex2') | ||
| const pathFull = path.replace(OPTIONAL_PARAM_REGEXP, '$1$2') | ||
| const pathOptional = path.replace(OPTIONAL_PARAM_REGEXP, '$2') | ||
| const pathOptional = path.replace(OPTIONAL_PARAM_REGEXP, '$2') || '/' | ||
@@ -349,3 +349,2 @@ this.on(method, pathFull, opts, handler, store) | ||
| const isEndOfNode = charCode === 47 || j === pattern.length | ||
| if (isRegexParam || isStaticPart || isEndOfNode) { | ||
@@ -413,5 +412,3 @@ const paramName = pattern.slice(lastParamStartIndex, j) | ||
| currentNode = currentNode.getWildcardChild() | ||
| if (currentNode === null) { | ||
| return null | ||
| } | ||
| parentNodePathIndex = i + 1 | ||
@@ -429,6 +426,2 @@ | ||
| if (pattern === '*') { | ||
| pattern = '/*' | ||
| } | ||
| for (const existRoute of this.routes) { | ||
@@ -444,3 +437,3 @@ const routeConstraints = existRoute.opts.constraints || {} | ||
| store: existRoute.store, | ||
| params: existRoute.params || [] | ||
| params: existRoute.params | ||
| } | ||
@@ -651,33 +644,32 @@ } | ||
| if (currentNode.kind === NODE_TYPES.PARAMETRIC) { | ||
| let paramEndIndex = originPath.indexOf('/', pathIndex) | ||
| if (paramEndIndex === -1) { | ||
| paramEndIndex = pathLen | ||
| } | ||
| // parametric node | ||
| let paramEndIndex = originPath.indexOf('/', pathIndex) | ||
| if (paramEndIndex === -1) { | ||
| paramEndIndex = pathLen | ||
| } | ||
| let param = originPath.slice(pathIndex, paramEndIndex) | ||
| if (shouldDecodeParam) { | ||
| param = safeDecodeURIComponent(param) | ||
| } | ||
| let param = originPath.slice(pathIndex, paramEndIndex) | ||
| if (shouldDecodeParam) { | ||
| param = safeDecodeURIComponent(param) | ||
| } | ||
| if (currentNode.isRegex) { | ||
| const matchedParameters = currentNode.regex.exec(param) | ||
| if (matchedParameters === null) continue | ||
| if (currentNode.isRegex) { | ||
| const matchedParameters = currentNode.regex.exec(param) | ||
| if (matchedParameters === null) continue | ||
| for (let i = 1; i < matchedParameters.length; i++) { | ||
| const matchedParam = matchedParameters[i] | ||
| if (matchedParam.length > maxParamLength) { | ||
| return null | ||
| } | ||
| params.push(matchedParam) | ||
| } | ||
| } else { | ||
| if (param.length > maxParamLength) { | ||
| for (let i = 1; i < matchedParameters.length; i++) { | ||
| const matchedParam = matchedParameters[i] | ||
| if (matchedParam.length > maxParamLength) { | ||
| return null | ||
| } | ||
| params.push(param) | ||
| params.push(matchedParam) | ||
| } | ||
| } else { | ||
| if (param.length > maxParamLength) { | ||
| return null | ||
| } | ||
| params.push(param) | ||
| } | ||
| pathIndex = paramEndIndex | ||
| } | ||
| pathIndex = paramEndIndex | ||
| } | ||
@@ -752,4 +744,2 @@ } | ||
| if (Router.prototype[methodName]) throw new Error('Method already exists: ' + methodName) | ||
| Router.prototype[methodName] = function (path, handler, store) { | ||
@@ -756,0 +746,0 @@ return this.on(m, path, handler, store) |
@@ -5,3 +5,3 @@ 'use strict' | ||
| const acceptHostStrategy = require('./strategies/accept-host') | ||
| const assert = require('assert') | ||
| const assert = require('node:assert') | ||
@@ -157,6 +157,4 @@ class Constrainer { | ||
| lines.push(' version: req.headers[\'accept-version\'],') | ||
| } else if (key === 'host') { | ||
| } else { | ||
| lines.push(' host: req.headers.host || req.headers[\':authority\'],') | ||
| } else { | ||
| throw new Error('unknown non-custom strategy for compiling constraint derivation function') | ||
| } | ||
@@ -163,0 +161,0 @@ } else { |
@@ -50,4 +50,4 @@ 'use strict' | ||
| const isMergedTree = constraintsNames.includes(httpMethodStrategy.name) | ||
| if (!isMergedTree && this.handlers.length >= 32) { | ||
| throw new Error('find-my-way supports a maximum of 32 route handlers per node when there are constraints, limit reached') | ||
| if (!isMergedTree && this.handlers.length >= 31) { | ||
| throw new Error('find-my-way supports a maximum of 31 route handlers per node when there are constraints, limit reached') | ||
| } | ||
@@ -54,0 +54,0 @@ |
+1
-4
@@ -132,6 +132,3 @@ 'use strict' | ||
| getWildcardChild () { | ||
| if (this.wildcardChild) { | ||
| return this.wildcardChild | ||
| } | ||
| return null | ||
| return this.wildcardChild | ||
| } | ||
@@ -138,0 +135,0 @@ |
| 'use strict' | ||
| const assert = require('assert') | ||
| const assert = require('node:assert') | ||
@@ -4,0 +4,0 @@ function HostStorage () { |
| 'use strict' | ||
| const assert = require('assert') | ||
| const assert = require('node:assert') | ||
@@ -23,3 +23,7 @@ function SemVerStore () { | ||
| major = Number(major) || 0 | ||
| if (isNaN(major)) { | ||
| throw new TypeError('Major version must be a numeric value') | ||
| } | ||
| major = Number(major) | ||
| minor = Number(minor) || 0 | ||
@@ -42,3 +46,3 @@ patch = Number(patch) || 0 | ||
| if (patch >= (this.store[`${major}.${minor}`] || 0)) { | ||
| if (patch >= (this.maxPatches[`${major}.${minor}`] || 0)) { | ||
| this.maxPatches[`${major}.${minor}`] = patch | ||
@@ -45,0 +49,0 @@ this.store[`${major}.${minor}.x`] = store |
@@ -12,7 +12,4 @@ 'use strict' | ||
| }, | ||
| deriveConstraint: (req) => { | ||
| /* istanbul ignore next */ | ||
| return req.method | ||
| }, | ||
| deriveConstraint: /* istanbul ignore next */ (req) => req.method, | ||
| mustMatchWhenDerived: true | ||
| } |
+3
-2
| { | ||
| "name": "find-my-way", | ||
| "version": "8.1.0", | ||
| "version": "8.2.0", | ||
| "description": "Crazy fast http radix based router", | ||
@@ -43,2 +43,3 @@ "main": "index.js", | ||
| "pre-commit": "^1.2.2", | ||
| "proxyquire": "^2.1.3", | ||
| "rfdc": "^1.3.0", | ||
@@ -54,3 +55,3 @@ "simple-git": "^3.7.1", | ||
| "fast-querystring": "^1.0.0", | ||
| "safe-regex2": "^2.0.0" | ||
| "safe-regex2": "^3.1.0" | ||
| }, | ||
@@ -57,0 +58,0 @@ "tsd": { |
+1
-1
@@ -446,4 +446,4 @@ # find-my-way | ||
| ```js | ||
| #### lookup(request, response, [context], [done]) | ||
| Start a new search, `request` and `response` are the server req/res objects.<br> | ||
@@ -450,0 +450,0 @@ If a route is found it will automatically call the handler, otherwise the default route will be called.<br> |
@@ -6,2 +6,3 @@ 'use strict' | ||
| const FindMyWay = require('..') | ||
| const rfdc = require('rfdc')({ proto: true }) | ||
@@ -27,8 +28,11 @@ const customHeaderConstraint = { | ||
| test('should derive async constraint', t => { | ||
| test('should derive multiple async constraints', t => { | ||
| t.plan(2) | ||
| const router = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } }) | ||
| router.on('GET', '/', { constraints: { requestedBy: 'node' } }, () => 'asyncHandler') | ||
| const customHeaderConstraint2 = rfdc(customHeaderConstraint) | ||
| customHeaderConstraint2.name = 'requestedBy2' | ||
| const router = FindMyWay({ constraints: { requestedBy: customHeaderConstraint, requestedBy2: customHeaderConstraint2 } }) | ||
| router.on('GET', '/', { constraints: { requestedBy: 'node', requestedBy2: 'node' } }, () => 'asyncHandler') | ||
| router.lookup( | ||
@@ -35,0 +39,0 @@ { |
@@ -77,1 +77,30 @@ 'use strict' | ||
| }) | ||
| test('A route supports up to 31 host constraints', (t) => { | ||
| t.plan(1) | ||
| const findMyWay = FindMyWay() | ||
| for (let i = 0; i < 31; i++) { | ||
| const host = `h${i.toString().padStart(2, '0')}` | ||
| findMyWay.on('GET', '/', { constraints: { host } }, alpha) | ||
| } | ||
| t.equal(findMyWay.find('GET', '/', { host: 'h01' }).handler, alpha) | ||
| }) | ||
| test('A route throws when constraint limit exceeded', (t) => { | ||
| t.plan(1) | ||
| const findMyWay = FindMyWay() | ||
| for (let i = 0; i < 31; i++) { | ||
| const host = `h${i.toString().padStart(2, '0')}` | ||
| findMyWay.on('GET', '/', { constraints: { host } }, alpha) | ||
| } | ||
| t.throws( | ||
| () => findMyWay.on('GET', '/', { constraints: { host: 'h31' } }, beta), | ||
| 'find-my-way supports a maximum of 31 route handlers per node when there are constraints, limit reached' | ||
| ) | ||
| }) |
@@ -198,1 +198,21 @@ 'use strict' | ||
| }) | ||
| test('optional parameter on root', (t) => { | ||
| t.plan(2) | ||
| const findMyWay = FindMyWay({ | ||
| defaultRoute: (req, res) => { | ||
| t.fail('Should not be defaultRoute') | ||
| } | ||
| }) | ||
| findMyWay.on('GET', '/:optional?', (req, res, params) => { | ||
| if (params.optional) { | ||
| t.equal(params.optional, 'foo') | ||
| } else { | ||
| t.equal(params.optional, undefined) | ||
| } | ||
| }) | ||
| findMyWay.lookup({ method: 'GET', url: '/', headers: {} }, null) | ||
| findMyWay.lookup({ method: 'GET', url: '/foo', headers: {} }, null) | ||
| }) |
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
384117
2.52%92
2.22%9549
2.1%12
9.09%+ Added
+ Added
- Removed
- Removed
Updated