From 7c38c648fa93eda8d77f57192cf017c494ddf723 Mon Sep 17 00:00:00 2001 From: Kyzgor Date: Tue, 23 Jun 2026 22:48:01 +0100 Subject: [PATCH 1/2] refactor(deps): replace url-parse with native URL API url-parse was used only to build the OPA client base URL. Node's native WHATWG URL (available since v10; engines.node already requires >=10) does the same, so extract a buildOpaBaseUrl() helper and drop url-parse and @types/url-parse. yarn.lock is pruned of url-parse and its now-orphaned transitive dependencies (querystringify, requires-port) only; every other entry is left byte-for-byte unchanged. --- package.json | 5 ++--- src/enforcement/enforcer.ts | 28 ++++++++++++++++++++++------ yarn.lock | 23 ----------------------- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index c8efff7..4e2bad8 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "fix:prettier": "prettier --config .prettierrc \"src/**/*.{ts,css,less,scss,js}\" --write", "fix:lint": "eslint src --ext .ts --fix", "test": "run-s test:*", + "test:unit": "run-s build && ava --verbose build/tests/enforcer.spec.js", "test:integration": "run-s build && ava --verbose build/tests/endpoints/**/*.spec.js", "test:module-imports": "run-s build && ava --verbose build/tests/module-imports/**/*.spec.js", "test:e2e:rbac": "run-s build && ava --verbose build/tests/e2e/rbac.e2e.spec.js", @@ -63,8 +64,7 @@ "path-to-regexp": "^6.2.1", "pino": "8.11.0", "pino-pretty": "10.2.0", - "require-in-the-middle": "^5.1.0", - "url-parse": "^1.5.10" + "require-in-the-middle": "^5.1.0" }, "devDependencies": { "@ava/typescript": "^1.1.1", @@ -74,7 +74,6 @@ "@types/express": "^4.17.9", "@types/lodash": "^4.14.166", "@types/node": "^14.14.14", - "@types/url-parse": "^1.4.11", "@typescript-eslint/eslint-plugin": "^4.0.1", "@typescript-eslint/parser": "^4.0.1", "ava": "^3.12.1", diff --git a/src/enforcement/enforcer.ts b/src/enforcement/enforcer.ts index cf1ec9f..0c46672 100644 --- a/src/enforcement/enforcer.ts +++ b/src/enforcement/enforcer.ts @@ -1,6 +1,5 @@ import axios, { AxiosInstance } from 'axios'; import { Logger } from 'pino'; -import URL from 'url-parse'; import { IPermitConfig } from '../config'; import { CheckConfig, Context, ContextStore } from '../utils/context'; @@ -118,6 +117,25 @@ export interface IEnforcer { ): Promise; } +/** + * Builds the OPA client base URL from the configured PDP URL by forcing the OPA + * port (8181) and appending the OPA data path, using the native WHATWG `URL` + * (Node >= 10) in place of the previous `url-parse` dependency (#106). + * + * @param pdp - The configured PDP base URL (e.g. `http://localhost:7766`). + * @returns The OPA base URL (e.g. `http://localhost:8181/v1/data/permit/`). A PDP + * path with no trailing slash is glued to the data path (`/prefix` -> + * `/prefixv1/data/permit/`), preserved from `url-parse` and locked by a test. + * @throws {TypeError} on input without a scheme (e.g. `localhost`); a `host:port` + * value like `localhost:7766` is misparsed, not rejected — pass a full URL. + */ +export function buildOpaBaseUrl(pdp: string): string { + const opaBaseUrl = new URL(pdp); + opaBaseUrl.port = '8181'; + opaBaseUrl.pathname = `${opaBaseUrl.pathname}v1/data/permit/`; + return opaBaseUrl.toString(); +} + /** * The {@link Enforcer} class is responsible for performing permission checks against the PDP. * It implements the {@link IEnforcer} interface. @@ -133,9 +151,7 @@ export class Enforcer implements IEnforcer { * @param logger - The logger instance for logging. */ constructor(private config: IPermitConfig, private logger: Logger) { - const opaBaseUrl = new URL(this.config.pdp); - opaBaseUrl.set('port', '8181'); - opaBaseUrl.set('pathname', `${opaBaseUrl.pathname}v1/data/permit/`); + const opaBaseUrl = buildOpaBaseUrl(this.config.pdp); const version = process.env.npm_package_version ?? 'unknown'; if (config.axiosInstance) { this.client = config.axiosInstance; @@ -151,11 +167,11 @@ export class Enforcer implements IEnforcer { } if (config.opaAxiosInstance) { this.opaClient = config.opaAxiosInstance; - this.opaClient.defaults.baseURL = opaBaseUrl.toString(); + this.opaClient.defaults.baseURL = opaBaseUrl; this.opaClient.defaults.headers.common['X-Permit-SDK-Version'] = `node:${version}`; } else { this.opaClient = axios.create({ - baseURL: opaBaseUrl.toString(), + baseURL: opaBaseUrl, headers: { 'X-Permit-SDK-Version': `node:${version}`, }, diff --git a/yarn.lock b/yarn.lock index bf3d8fb..9ddb093 100644 --- a/yarn.lock +++ b/yarn.lock @@ -807,11 +807,6 @@ "@types/mime" "*" "@types/node" "*" -"@types/url-parse@^1.4.11": - version "1.4.11" - resolved "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.11.tgz" - integrity sha512-FKvKIqRaykZtd4n47LbK/W/5fhQQ1X7cxxzG9A48h0BGN+S04NH7ervcCjM8tyR0lyGru83FAHSmw2ObgKoESg== - "@typescript-eslint/eslint-plugin@^4.0.1": version "4.33.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz" @@ -5311,11 +5306,6 @@ q@^1.5.1: resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -5527,11 +5517,6 @@ require-main-filename@^2.0.0: resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -6699,14 +6684,6 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.5.10: - version "1.5.10" - resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - urlgrey@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/urlgrey/-/urlgrey-1.0.0.tgz" From 575f1b3029bd36fff4dcd8bce0dc6bcb51113e4c Mon Sep 17 00:00:00 2001 From: Kyzgor Date: Tue, 23 Jun 2026 22:48:03 +0100 Subject: [PATCH 2/2] test(enforcer): cover OPA base URL construction and wiring Lock the exact OPA base URL produced for the default PDP, trailing-slash, explicit-port, https, and path-prefix inputs so the url-parse -> native URL refactor is proven behaviour-equivalent on valid input and any regression fails here; assert a scheme-less PDP (bare host or //host:port) throws; and assert the Enforcer wires the OPA client baseURL to buildOpaBaseUrl(pdp). --- src/tests/enforcer.spec.ts | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/tests/enforcer.spec.ts diff --git a/src/tests/enforcer.spec.ts b/src/tests/enforcer.spec.ts new file mode 100644 index 0000000..62780c8 --- /dev/null +++ b/src/tests/enforcer.spec.ts @@ -0,0 +1,57 @@ +import test from 'ava'; +import axios from 'axios'; + +import { buildOpaBaseUrl } from '../enforcement/enforcer'; +import { Permit } from '../index'; + +// The OPA client base URL is derived from the configured PDP URL by forcing the +// OPA port (8181) and appending the OPA data path. This was previously built +// with the `url-parse` package and is now built with the native WHATWG `URL` +// (#106). These assertions lock the produced string so the refactor is proven +// behaviour-equivalent and any regression in the construction logic fails here. + +test('buildOpaBaseUrl: default PDP', (t) => { + t.is(buildOpaBaseUrl('http://localhost:7766'), 'http://localhost:8181/v1/data/permit/'); +}); + +test('buildOpaBaseUrl: trailing slash yields the same URL as no trailing slash', (t) => { + t.is(buildOpaBaseUrl('http://localhost:7766/'), 'http://localhost:8181/v1/data/permit/'); +}); + +test('buildOpaBaseUrl: https host without an explicit port', (t) => { + t.is(buildOpaBaseUrl('https://pdp.example.com'), 'https://pdp.example.com:8181/v1/data/permit/'); +}); + +test('buildOpaBaseUrl: an existing port is overridden with 8181', (t) => { + t.is( + buildOpaBaseUrl('https://pdp.example.com:1234'), + 'https://pdp.example.com:8181/v1/data/permit/', + ); +}); + +test('buildOpaBaseUrl: a path-prefixed PDP preserves the pre-existing concatenation behaviour', (t) => { + // Pre-existing url-parse quirk: a path with no trailing slash glues onto the data path. + t.is( + buildOpaBaseUrl('http://localhost:7766/prefix'), + 'http://localhost:8181/prefixv1/data/permit/', + ); + t.is( + buildOpaBaseUrl('http://localhost:7766/prefix/'), + 'http://localhost:8181/prefix/v1/data/permit/', + ); +}); + +test('buildOpaBaseUrl: a PDP without a scheme throws (invalid absolute URL)', (t) => { + // Scheme-less input (bare host or `//host:port`) throws at construction; `localhost:7766` is misparsed, not rejected. + t.throws(() => buildOpaBaseUrl('localhost'), { instanceOf: TypeError }); + t.throws(() => buildOpaBaseUrl('//localhost:7766'), { instanceOf: TypeError }); +}); + +test('Enforcer wires the OPA client base URL to buildOpaBaseUrl(pdp)', (t) => { + // Guards the integration the refactor actually edits: the constructed OPA axios + // client must take its baseURL from the helper. Passing an opaAxiosInstance lets + // us read what the Enforcer set, without a live PDP or network call. + const opaAxiosInstance = axios.create(); + new Permit({ token: 'test-token', pdp: 'http://localhost:7766', opaAxiosInstance }); + t.is(opaAxiosInstance.defaults.baseURL, buildOpaBaseUrl('http://localhost:7766')); +});