From b18e1a21241d816eb0754db2f117d18c55acaecc Mon Sep 17 00:00:00 2001 From: Phillip Barta Date: Thu, 26 Feb 2026 23:51:35 +0100 Subject: [PATCH 1/2] TypeScript Rewrite --- .editorconfig | 10 ++ .eslintignore | 2 - .eslintrc.yml | 2 - .github/workflows/ci.yml | 249 +++------------------------------------ .gitignore | 2 + HISTORY.md | 61 ++++------ README.md | 40 +++---- index.js | 133 --------------------- package.json | 53 +++++---- src/auth.spec.ts | 139 ++++++++++++++++++++++ src/index.ts | 134 +++++++++++++++++++++ src/parse.spec.ts | 103 ++++++++++++++++ test/.eslintrc.yml | 2 - test/basic-auth.js | 232 ------------------------------------ tsconfig.build.json | 5 + tsconfig.json | 13 ++ 16 files changed, 498 insertions(+), 682 deletions(-) create mode 100644 .editorconfig delete mode 100644 .eslintignore delete mode 100644 .eslintrc.yml delete mode 100644 index.js create mode 100644 src/auth.spec.ts create mode 100644 src/index.ts create mode 100644 src/parse.spec.ts delete mode 100644 test/.eslintrc.yml delete mode 100644 test/basic-auth.js create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ffc4e92 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +quote_type = single \ No newline at end of file diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 62562b7..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -coverage -node_modules diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 1eece14..0000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -root: true -extends: standard diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92053d0..8fff328 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,241 +1,26 @@ -name: ci - +name: CI on: -- pull_request -- push - + - push + - pull_request permissions: contents: read - jobs: test: - permissions: - checks: write # for coverallsapp/github-action to create new checks - contents: read # for actions/checkout to fetch code + name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: matrix: - name: - - Node.js 0.8 - - Node.js 0.10 - - Node.js 0.12 - - io.js 1.x - - io.js 2.x - - io.js 3.x - - Node.js 4.x - - Node.js 5.x - - Node.js 6.x - - Node.js 7.x - - Node.js 8.x - - Node.js 9.x - - Node.js 10.x - - Node.js 11.x - - Node.js 12.x - - Node.js 13.x - - Node.js 14.x - - Node.js 15.x - - Node.js 16.x - - Node.js 17.x - - Node.js 18.x - - Node.js 19.x - - Node.js 20.x - - Node.js 21.x - - Node.js 22.x - - Node.js 23.x - - Node.js 24.x - - Node.js 25.x - - include: - - name: Node.js 0.8 - node-version: "0.8" - npm-i: mocha@2.5.3 - npm-rm: nyc - - - name: Node.js 0.10 - node-version: "0.10" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: Node.js 0.12 - node-version: "0.12" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: io.js 1.x - node-version: "1.8" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: io.js 2.x - node-version: "2.5" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: io.js 3.x - node-version: "3.3" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: Node.js 4.x - node-version: "4.9" - npm-i: mocha@5.2.0 nyc@11.9.0 - - - name: Node.js 5.x - node-version: "5.12" - npm-i: mocha@5.2.0 nyc@11.9.0 - - - name: Node.js 6.x - node-version: "6.17" - npm-i: mocha@6.2.2 nyc@14.1.1 - - - name: Node.js 7.x - node-version: "7.10" - npm-i: mocha@6.2.2 nyc@14.1.1 - - - name: Node.js 8.x - node-version: "8.17" - npm-i: mocha@7.1.2 nyc@14.1.1 - - - name: Node.js 9.x - node-version: "9.11" - npm-i: mocha@7.1.2 nyc@14.1.1 - - - name: Node.js 10.x - node-version: "10.24" - npm-i: mocha@8.4.0 - - - name: Node.js 11.x - node-version: "11.15" - npm-i: mocha@8.4.0 - - - name: Node.js 12.x - node-version: "12.22" - npm-i: mocha@9.2.2 - - - name: Node.js 13.x - node-version: "13.14" - npm-i: mocha@9.2.2 - - - name: Node.js 14.x - node-version: "14.21" - - - name: Node.js 15.x - node-version: "15.14" - - - name: Node.js 16.x - node-version: "16.20" - - - name: Node.js 17.x - node-version: "17.9" - - - name: Node.js 18.x - node-version: "18.20" - - - name: Node.js 19.x - node-version: "19.9" - - - name: Node.js 20.x - node-version: "20.13" - - - name: Node.js 21.x - node-version: "21.7" - - - name: Node.js 22.x - node-version: "22.1" - - - name: Node.js 23.x - node-version: "23" - - - name: Node.js 24.x - node-version: "24" - - - name: Node.js 25.x - node-version: "25" - + node-version: + - 18 + - '*' steps: - - uses: actions/checkout@v6 - - - name: Install Node.js ${{ matrix.node-version }} - shell: bash -eo pipefail -l {0} - run: | - nvm install --default ${{ matrix.node-version }} - if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then - nvm install --alias=npm 0.10 - nvm use ${{ matrix.node-version }} - if [[ "$(npm -v)" == 1.1.* ]]; then - nvm exec npm npm install -g npm@1.1 - ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" - else - sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" - fi - npm config set strict-ssl false - fi - dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - - - name: Configure npm - run: | - if [[ "$(npm config get package-lock)" == "true" ]]; then - npm config set package-lock false - else - npm config set shrinkwrap false - fi - - - name: Remove npm module(s) ${{ matrix.npm-rm }} - run: npm rm --silent --save-dev ${{ matrix.npm-rm }} - if: matrix.npm-rm != '' - - - name: Install npm module(s) ${{ matrix.npm-i }} - run: npm install --save-dev ${{ matrix.npm-i }} - if: matrix.npm-i != '' - - - name: Setup Node.js version-specific dependencies - shell: bash - run: | - # eslint for linting - # - remove on Node.js < 12 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then - node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ - grep -E '^eslint(-|$)' | \ - sort -r | \ - xargs -n1 npm rm --silent --save-dev - fi - - - name: Install Node.js dependencies - run: npm install - - - name: List environment - id: list_env - shell: bash - run: | - echo "node@$(node -v)" - echo "npm@$(npm -v)" - npm -s ls ||: - (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" - - - name: Run tests - shell: bash - run: | - if npm -ps ls nyc | grep -q nyc; then - npm run test-ci - else - npm test - fi - - - name: Lint code - if: steps.list_env.outputs.eslint != '' - run: npm run lint - - - name: Collect code coverage - uses: coverallsapp/github-action@master - if: steps.list_env.outputs.nyc != '' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: run-${{ matrix.test_number }} - parallel: true - - coverage: - permissions: - checks: write # for coverallsapp/github-action to create new checks - needs: test - runs-on: ubuntu-latest - steps: - - name: Upload code coverage - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test + - uses: codecov/codecov-action@v5 + with: + name: Node.js ${{ matrix.node-version }} + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 84a4863..036b400 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ coverage node_modules npm-debug.log package-lock.json +dist/ +*.tsbuildinfo \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index 2c44a01..be807a5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,52 +1,43 @@ -2.0.1 / 2018-09-19 -================== +# 2.0.1 / 2018-09-19 - * deps: safe-buffer@5.1.2 +- deps: safe-buffer@5.1.2 -2.0.0 / 2017-09-12 -================== +# 2.0.0 / 2017-09-12 - * Drop support for Node.js below 0.8 - * Remove `auth(ctx)` signature -- pass in header or `auth(ctx.req)` - * Use `safe-buffer` for improved Buffer API +- Drop support for Node.js below 0.8 +- Remove `auth(ctx)` signature -- pass in header or `auth(ctx.req)` +- Use `safe-buffer` for improved Buffer API -1.1.0 / 2016-11-18 -================== +# 1.1.0 / 2016-11-18 - * Add `auth.parse` for low-level string parsing +- Add `auth.parse` for low-level string parsing -1.0.4 / 2016-05-10 -================== +# 1.0.4 / 2016-05-10 - * Improve error message when `req` argument is not an object - * Improve error message when `req` missing `headers` property +- Improve error message when `req` argument is not an object +- Improve error message when `req` missing `headers` property -1.0.3 / 2015-07-01 -================== +# 1.0.3 / 2015-07-01 - * Fix regression accepting a Koa context +- Fix regression accepting a Koa context -1.0.2 / 2015-06-12 -================== +# 1.0.2 / 2015-06-12 - * Improve error message when `req` argument missing - * perf: enable strict mode - * perf: hoist regular expression - * perf: parse with regular expressions - * perf: remove argument reassignment +- Improve error message when `req` argument missing +- perf: enable strict mode +- perf: hoist regular expression +- perf: parse with regular expressions +- perf: remove argument reassignment -1.0.1 / 2015-05-04 -================== +# 1.0.1 / 2015-05-04 - * Update readme +- Update readme -1.0.0 / 2014-07-01 -================== +# 1.0.0 / 2014-07-01 - * Support empty password - * Support empty username +- Support empty password +- Support empty username -0.0.1 / 2013-11-30 -================== +# 0.0.1 / 2013-11-30 - * Initial release +- Initial release diff --git a/README.md b/README.md index 85f1bac..2428623 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ $ npm install basic-auth ```js -var auth = require('basic-auth') +var auth = require('basic-auth'); ``` ### auth(req) @@ -45,8 +45,8 @@ Pass a Node.js request object to the module export. If parsing fails ```js -var auth = require('basic-auth') -var user = auth(req) +var auth = require('basic-auth'); +var user = auth(req); // => { name: 'something', pass: 'whatever' } ``` @@ -56,45 +56,45 @@ A header string from any other location can also be parsed with ```js -var auth = require('basic-auth') -var user = auth.parse(req.getHeader('Proxy-Authorization')) +var auth = require('basic-auth'); +var user = auth.parse(req.getHeader('Proxy-Authorization')); ``` ### With vanilla node.js http server ```js -var http = require('http') -var auth = require('basic-auth') -var compare = require('tsscmp') +var http = require('http'); +var auth = require('basic-auth'); +var compare = require('tsscmp'); // Create server var server = http.createServer(function (req, res) { - var credentials = auth(req) + var credentials = auth(req); // Check credentials // The "check" function will typically be against your user store if (!credentials || !check(credentials.name, credentials.pass)) { - res.statusCode = 401 - res.setHeader('WWW-Authenticate', 'Basic realm="example"') - res.end('Access denied') + res.statusCode = 401; + res.setHeader('WWW-Authenticate', 'Basic realm="example"'); + res.end('Access denied'); } else { - res.end('Access granted') + res.end('Access granted'); } -}) +}); // Basic function to validate credentials for example -function check (name, pass) { - var valid = true +function check(name, pass) { + var valid = true; // Simple method to prevent short-circuit and use timing-safe compare - valid = compare(name, 'john') && valid - valid = compare(pass, 'secret') && valid + valid = compare(name, 'john') && valid; + valid = compare(pass, 'secret') && valid; - return valid + return valid; } // Listen -server.listen(3000) +server.listen(3000); ``` # License diff --git a/index.js b/index.js deleted file mode 100644 index 9106e64..0000000 --- a/index.js +++ /dev/null @@ -1,133 +0,0 @@ -/*! - * basic-auth - * Copyright(c) 2013 TJ Holowaychuk - * Copyright(c) 2014 Jonathan Ong - * Copyright(c) 2015-2016 Douglas Christopher Wilson - * MIT Licensed - */ - -'use strict' - -/** - * Module dependencies. - * @private - */ - -var Buffer = require('safe-buffer').Buffer - -/** - * Module exports. - * @public - */ - -module.exports = auth -module.exports.parse = parse - -/** - * RegExp for basic auth credentials - * - * credentials = auth-scheme 1*SP token68 - * auth-scheme = "Basic" ; case insensitive - * token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" - * @private - */ - -var CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/ - -/** - * RegExp for basic auth user/pass - * - * user-pass = userid ":" password - * userid = * - * password = *TEXT - * @private - */ - -var USER_PASS_REGEXP = /^([^:]*):(.*)$/ - -/** - * Parse the Authorization header field of a request. - * - * @param {object} req - * @return {object} with .name and .pass - * @public - */ - -function auth (req) { - if (!req) { - throw new TypeError('argument req is required') - } - - if (typeof req !== 'object') { - throw new TypeError('argument req is required to be an object') - } - - // get header - var header = getAuthorization(req) - - // parse header - return parse(header) -} - -/** - * Decode base64 string. - * @private - */ - -function decodeBase64 (str) { - return Buffer.from(str, 'base64').toString() -} - -/** - * Get the Authorization header from request object. - * @private - */ - -function getAuthorization (req) { - if (!req.headers || typeof req.headers !== 'object') { - throw new TypeError('argument req is required to have headers property') - } - - return req.headers.authorization -} - -/** - * Parse basic auth to object. - * - * @param {string} string - * @return {object} - * @public - */ - -function parse (string) { - if (typeof string !== 'string') { - return undefined - } - - // parse header - var match = CREDENTIALS_REGEXP.exec(string) - - if (!match) { - return undefined - } - - // decode user pass - var userPass = USER_PASS_REGEXP.exec(decodeBase64(match[1])) - - if (!userPass) { - return undefined - } - - // return credentials object - return new Credentials(userPass[1], userPass[2]) -} - -/** - * Object to represent user credentials. - * @private - */ - -function Credentials (name, pass) { - this.name = name - this.pass = pass -} diff --git a/package.json b/package.json index 81e2a31..dde152b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,7 @@ { "name": "basic-auth", - "description": "node.js basic auth parser", "version": "2.0.1", - "license": "MIT", + "description": "node.js basic auth parser", "keywords": [ "basic", "auth", @@ -10,32 +9,38 @@ "basicauth" ], "repository": "jshttp/basic-auth", - "dependencies": { - "safe-buffer": "5.2.1" + "license": "MIT", + "type": "commonjs", + "exports": "./dist/index.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-scripts build", + "format": "ts-scripts format", + "lint": "ts-scripts lint", + "prepare": "ts-scripts install && npm run build", + "specs": "ts-scripts specs", + "test": "ts-scripts test" }, "devDependencies": { - "eslint": "5.6.0", - "eslint-config-standard": "12.0.0", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-markdown": "1.0.2", - "eslint-plugin-node": "7.0.1", - "eslint-plugin-promise": "4.3.1", - "eslint-plugin-standard": "4.1.0", - "mocha": "10.8.2", - "nyc": "15.1.0" + "@borderless/ts-scripts": "^0.15.0", + "@types/node": "^20.19.35", + "@vitest/coverage-v8": "^3.2.4", + "typescript": "^5.9.3", + "vitest": "^3.2.4" }, - "files": [ - "HISTORY.md", - "LICENSE", - "index.js" - ], "engines": { - "node": ">= 0.8" + "node": ">=18" }, - "scripts": { - "lint": "eslint --plugin markdown --ext js,md .", - "test": "mocha --reporter spec --bail --check-leaks test/", - "test-ci": "nyc --reporter=lcov --reporter=text npm test", - "test-cov": "nyc --reporter=html --reporter=text npm test" + "ts-scripts": { + "dist": [ + "dist" + ], + "project": [ + "tsconfig.build.json" + ] } } diff --git a/src/auth.spec.ts b/src/auth.spec.ts new file mode 100644 index 0000000..f5ee907 --- /dev/null +++ b/src/auth.spec.ts @@ -0,0 +1,139 @@ +import { describe, it, assert } from 'vitest'; +import auth from './index'; + +function request(authorization?: string) { + return { + headers: { + authorization: authorization, + }, + }; +} + +describe('auth(req)', function () { + describe('arguments', function () { + describe('req', function () { + it('should be required', function () { + assert.throws(auth as any, /argument req is required/); + }); + + it('should accept a request', function () { + var req = request('basic Zm9vOmJhcg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + + it('should reject null', function () { + assert.throws(auth.bind(null, null as any), /argument req is required/); + }); + + it('should reject a number', function () { + assert.throws(auth.bind(null, 42 as any), /argument req is required/); + }); + + it('should reject an object without headers', function () { + assert.throws(auth.bind(null, {}), /argument req is required/); + }); + }); + }); + + describe('with no Authorization field', function () { + it('should return undefined', function () { + var req = request(); + assert.strictEqual(auth(req), undefined); + }); + }); + + describe('with malformed Authorization field', function () { + it('should return undefined', function () { + var req = request('Something'); + assert.strictEqual(auth(req), undefined); + }); + }); + + describe('with malformed Authorization scheme', function () { + it('should return undefined', function () { + var req = request('basic_Zm9vOmJhcg=='); + assert.strictEqual(auth(req), undefined); + }); + }); + + describe('with malformed credentials', function () { + it('should return undefined', function () { + var req = request('basic Zm9vcgo='); + assert.strictEqual(auth(req), undefined); + }); + }); + + describe('with valid credentials', function () { + it('should return .name and .pass', function () { + var req = request('basic Zm9vOmJhcg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + }); + + describe('with empty password', function () { + it('should return .name and .pass', function () { + var req = request('basic Zm9vOg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, ''); + }); + }); + + describe('with empty userid', function () { + it('should return .name and .pass', function () { + var req = request('basic OnBhc3M='); + var creds = auth(req); + assert.strictEqual(creds?.name, ''); + assert.strictEqual(creds?.pass, 'pass'); + }); + }); + + describe('with empty userid and pass', function () { + it('should return .name and .pass', function () { + var req = request('basic Og=='); + var creds = auth(req); + assert.strictEqual(creds?.name, ''); + assert.strictEqual(creds?.pass, ''); + }); + }); + + describe('with colon in pass', function () { + it('should return .name and .pass', function () { + var req = request('basic Zm9vOnBhc3M6d29yZA=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'pass:word'); + }); + }); + + describe('with scheme "Basic"', function () { + it('should return .name and .pass', function () { + var req = request('Basic Zm9vOmJhcg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + }); + + describe('with scheme "BASIC"', function () { + it('should return .name and .pass', function () { + var req = request('BASIC Zm9vOmJhcg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + }); + + describe('with scheme "BaSiC"', function () { + it('should return .name and .pass', function () { + var req = request('BaSiC Zm9vOmJhcg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..98bc6c4 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,134 @@ +/*! + * basic-auth + * Copyright(c) 2013 TJ Holowaychuk + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015-2016 Douglas Christopher Wilson + * MIT Licensed + */ + +import { Buffer } from 'node:buffer'; + +export = auth; + +/** + * Parse the Authorization header field of a request + * @public + */ + +function auth(req: auth.IncomingMessageLike): auth.Credentials | undefined { + if (!req) { + throw new TypeError('argument req is required'); + } + + if (typeof req !== 'object') { + throw new TypeError('argument req is required to be an object'); + } + + // get header + const header = getAuthorization(req); + + // parse header + return auth.parse(header ?? ''); +} + +namespace auth { + /** + * Object to represent user credentials. + */ + export interface Credentials { + name: string; + pass: string; + } + + export interface IncomingMessageLike { + headers?: { + authorization?: string; + }; + } + + /** + * Parse basic auth to object. + * + * @param {string} string + * @return {object} + * @public + */ + + export function parse(string: string): auth.Credentials | undefined { + if (typeof string !== 'string') { + return undefined; + } + + // parse header + const match = CREDENTIALS_REGEXP.exec(string); + + if (!match) { + return undefined; + } + + // decode user pass + const userPass = USER_PASS_REGEXP.exec(decodeBase64(match[1])); + + if (!userPass) { + return undefined; + } + + // return credentials object + return new CredentialsImpl(userPass[1], userPass[2]); + } +} + +/** + * RegExp for basic auth credentials + * + * credentials = auth-scheme 1*SP token68 + * auth-scheme = "Basic" ; case insensitive + * token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" + * @private + */ + +const CREDENTIALS_REGEXP = + /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/; + +/** + * RegExp for basic auth user/pass + * + * user-pass = userid ":" password + * userid = * + * password = *TEXT + * @private + */ + +const USER_PASS_REGEXP = /^([^:]*):(.*)$/; + +/** + * Decode base64 string. + * @private + */ + +function decodeBase64(str: string): string { + return Buffer.from(str, 'base64').toString(); +} + +/** + * Get the Authorization header from request object. + * @private + */ + +function getAuthorization(req: auth.IncomingMessageLike) { + if (!req.headers || typeof req.headers !== 'object') { + throw new TypeError('argument req is required to have headers property'); + } + + return req.headers.authorization; +} + +class CredentialsImpl implements auth.Credentials { + name: string; + pass: string; + + constructor(name: string, pass: string) { + this.name = name; + this.pass = pass; + } +} diff --git a/src/parse.spec.ts b/src/parse.spec.ts new file mode 100644 index 0000000..06edfbf --- /dev/null +++ b/src/parse.spec.ts @@ -0,0 +1,103 @@ +import { describe, it, assert } from 'vitest'; +import auth from './index'; + +function request(authorization: string) { + return { + headers: { + authorization: authorization, + }, + }; +} + +describe('auth.parse(string)', function () { + describe('with undefined string', function () { + it('should return undefined', function () { + assert.strictEqual((auth as any).parse(), undefined); + }); + }); + + describe('with malformed string', function () { + it('should return undefined', function () { + assert.strictEqual(auth.parse('Something'), undefined); + }); + }); + + describe('with malformed scheme', function () { + it('should return undefined', function () { + assert.strictEqual(auth.parse('basic_Zm9vOmJhcg=='), undefined); + }); + }); + + describe('with malformed credentials', function () { + it('should return undefined', function () { + assert.strictEqual(auth.parse('basic Zm9vcgo='), undefined); + }); + }); + + describe('with valid credentials', function () { + it('should return .name and .pass', function () { + var creds = auth.parse('basic Zm9vOmJhcg=='); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + }); + + describe('with empty password', function () { + it('should return .name and .pass', function () { + var creds = auth.parse('basic Zm9vOg=='); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, ''); + }); + }); + + describe('with empty userid', function () { + it('should return .name and .pass', function () { + var creds = auth.parse('basic OnBhc3M='); + assert.strictEqual(creds?.name, ''); + assert.strictEqual(creds?.pass, 'pass'); + }); + }); + + describe('with empty userid and pass', function () { + it('should return .name and .pass', function () { + var creds = auth.parse('basic Og=='); + assert.strictEqual(creds?.name, ''); + assert.strictEqual(creds?.pass, ''); + }); + }); + + describe('with colon in pass', function () { + it('should return .name and .pass', function () { + var creds = auth.parse('basic Zm9vOnBhc3M6d29yZA=='); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'pass:word'); + }); + }); + + describe('with scheme "Basic"', function () { + it('should return .name and .pass', function () { + var req = request('Basic Zm9vOmJhcg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + }); + + describe('with scheme "BASIC"', function () { + it('should return .name and .pass', function () { + var req = request('BASIC Zm9vOmJhcg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + }); + + describe('with scheme "BaSiC"', function () { + it('should return .name and .pass', function () { + var req = request('BaSiC Zm9vOmJhcg=='); + var creds = auth(req); + assert.strictEqual(creds?.name, 'foo'); + assert.strictEqual(creds?.pass, 'bar'); + }); + }); +}); diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml deleted file mode 100644 index 9808c3b..0000000 --- a/test/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -env: - mocha: true diff --git a/test/basic-auth.js b/test/basic-auth.js deleted file mode 100644 index 96877f3..0000000 --- a/test/basic-auth.js +++ /dev/null @@ -1,232 +0,0 @@ -var assert = require('assert') -var auth = require('..') - -function request (authorization) { - return { - headers: { - authorization: authorization - } - } -} - -describe('auth(req)', function () { - describe('arguments', function () { - describe('req', function () { - it('should be required', function () { - assert.throws(auth, /argument req is required/) - }) - - it('should accept a request', function () { - var req = request('basic Zm9vOmJhcg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - - it('should reject null', function () { - assert.throws(auth.bind(null, null), /argument req is required/) - }) - - it('should reject a number', function () { - assert.throws(auth.bind(null, 42), /argument req is required/) - }) - - it('should reject an object without headers', function () { - assert.throws(auth.bind(null, {}), /argument req is required/) - }) - }) - }) - - describe('with no Authorization field', function () { - it('should return undefined', function () { - var req = request() - assert.strictEqual(auth(req), undefined) - }) - }) - - describe('with malformed Authorization field', function () { - it('should return undefined', function () { - var req = request('Something') - assert.strictEqual(auth(req), undefined) - }) - }) - - describe('with malformed Authorization scheme', function () { - it('should return undefined', function () { - var req = request('basic_Zm9vOmJhcg==') - assert.strictEqual(auth(req), undefined) - }) - }) - - describe('with malformed credentials', function () { - it('should return undefined', function () { - var req = request('basic Zm9vcgo=') - assert.strictEqual(auth(req), undefined) - }) - }) - - describe('with valid credentials', function () { - it('should return .name and .pass', function () { - var req = request('basic Zm9vOmJhcg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - }) - - describe('with empty password', function () { - it('should return .name and .pass', function () { - var req = request('basic Zm9vOg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, '') - }) - }) - - describe('with empty userid', function () { - it('should return .name and .pass', function () { - var req = request('basic OnBhc3M=') - var creds = auth(req) - assert.strictEqual(creds.name, '') - assert.strictEqual(creds.pass, 'pass') - }) - }) - - describe('with empty userid and pass', function () { - it('should return .name and .pass', function () { - var req = request('basic Og==') - var creds = auth(req) - assert.strictEqual(creds.name, '') - assert.strictEqual(creds.pass, '') - }) - }) - - describe('with colon in pass', function () { - it('should return .name and .pass', function () { - var req = request('basic Zm9vOnBhc3M6d29yZA==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'pass:word') - }) - }) - - describe('with scheme "Basic"', function () { - it('should return .name and .pass', function () { - var req = request('Basic Zm9vOmJhcg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - }) - - describe('with scheme "BASIC"', function () { - it('should return .name and .pass', function () { - var req = request('BASIC Zm9vOmJhcg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - }) - - describe('with scheme "BaSiC"', function () { - it('should return .name and .pass', function () { - var req = request('BaSiC Zm9vOmJhcg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - }) -}) - -describe('auth.parse(string)', function () { - describe('with undefined string', function () { - it('should return undefined', function () { - assert.strictEqual(auth.parse(), undefined) - }) - }) - - describe('with malformed string', function () { - it('should return undefined', function () { - assert.strictEqual(auth.parse('Something'), undefined) - }) - }) - - describe('with malformed scheme', function () { - it('should return undefined', function () { - assert.strictEqual(auth.parse('basic_Zm9vOmJhcg=='), undefined) - }) - }) - - describe('with malformed credentials', function () { - it('should return undefined', function () { - assert.strictEqual(auth.parse('basic Zm9vcgo='), undefined) - }) - }) - - describe('with valid credentials', function () { - it('should return .name and .pass', function () { - var creds = auth.parse('basic Zm9vOmJhcg==') - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - }) - - describe('with empty password', function () { - it('should return .name and .pass', function () { - var creds = auth.parse('basic Zm9vOg==') - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, '') - }) - }) - - describe('with empty userid', function () { - it('should return .name and .pass', function () { - var creds = auth.parse('basic OnBhc3M=') - assert.strictEqual(creds.name, '') - assert.strictEqual(creds.pass, 'pass') - }) - }) - - describe('with empty userid and pass', function () { - it('should return .name and .pass', function () { - var creds = auth.parse('basic Og==') - assert.strictEqual(creds.name, '') - assert.strictEqual(creds.pass, '') - }) - }) - - describe('with colon in pass', function () { - it('should return .name and .pass', function () { - var creds = auth.parse('basic Zm9vOnBhc3M6d29yZA==') - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'pass:word') - }) - }) - - describe('with scheme "Basic"', function () { - it('should return .name and .pass', function () { - var req = request('Basic Zm9vOmJhcg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - }) - - describe('with scheme "BASIC"', function () { - it('should return .name and .pass', function () { - var req = request('BASIC Zm9vOmJhcg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - }) - - describe('with scheme "BaSiC"', function () { - it('should return .name and .pass', function () { - var req = request('BaSiC Zm9vOmJhcg==') - var creds = auth(req) - assert.strictEqual(creds.name, 'foo') - assert.strictEqual(creds.pass, 'bar') - }) - }) -}) diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..fb2211f --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": {}, + "exclude": ["src/**/*.spec.ts", "src/**/*.bench.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8eef991 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@borderless/ts-scripts/configs/tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "rootDir": "src", + "outDir": "dist", + "module": "nodenext", + "moduleResolution": "nodenext", + "types": ["node"] + }, + "include": ["src/**/*"] +} From e2854e0d41cf58f915b327f5d0920d43670ef1c2 Mon Sep 17 00:00:00 2001 From: Phillip Barta Date: Fri, 27 Feb 2026 01:25:13 +0100 Subject: [PATCH 2/2] refactor: add cross-environment decodeBase64 method --- .github/workflows/ci.yml | 3 +- globals.d.ts | 61 ++++++++++++++++++++++++++++++++++++++++ package.json | 1 - src/index.ts | 39 +++++++++++++++++++++---- tsconfig.json | 2 +- 5 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 globals.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fff328..0612a79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,8 @@ jobs: matrix: node-version: - 18 - - '*' + - 25 # Node.js 25 support Uint8Array.fromBase64() + - 'lts/*' steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 diff --git a/globals.d.ts b/globals.d.ts new file mode 100644 index 0000000..20fd8b2 --- /dev/null +++ b/globals.d.ts @@ -0,0 +1,61 @@ +// Adapted from file://./node_modules/typescript/lib/lib.dom.d.ts so we don't have to include the entire DOM lib +// Ref: https://github.com/microsoft/TypeScript/issues/31535, https://github.com/microsoft/TypeScript/issues/41727, https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1685 + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ +declare function atob(data: string): string; +/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */ +declare function btoa(data: string): string; + +type AllowSharedBufferSource = + | ArrayBufferLike + | ArrayBufferView; + +interface TextDecodeOptions { + stream?: boolean; +} + +interface TextDecoderOptions { + fatal?: boolean; + ignoreBOM?: boolean; +} + +/** + * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder) + */ +interface TextDecoder extends TextDecoderCommon { + /** + * The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode) + */ + decode(input?: AllowSharedBufferSource, options?: TextDecodeOptions): string; +} + +// eslint-disable-next-line no-var +declare var TextDecoder: { + prototype: TextDecoder; + new (label?: string, options?: TextDecoderOptions): TextDecoder; +}; + +interface TextDecoderCommon { + /** + * Returns encoding's name, lowercased. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/encoding) + */ + readonly encoding: string; + /** + * Returns true if error mode is "fatal", otherwise false. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/fatal) + */ + readonly fatal: boolean; + /** + * Returns the value of ignore BOM. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/ignoreBOM) + */ + readonly ignoreBOM: boolean; +} diff --git a/package.json b/package.json index dde152b..98fe6ac 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ }, "devDependencies": { "@borderless/ts-scripts": "^0.15.0", - "@types/node": "^20.19.35", "@vitest/coverage-v8": "^3.2.4", "typescript": "^5.9.3", "vitest": "^3.2.4" diff --git a/src/index.ts b/src/index.ts index 98bc6c4..e90e7de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,6 @@ * MIT Licensed */ -import { Buffer } from 'node:buffer'; - export = auth; /** @@ -101,14 +99,45 @@ const CREDENTIALS_REGEXP = const USER_PASS_REGEXP = /^([^:]*):(.*)$/; +type Uint8ArrayWithBase64 = typeof Uint8Array & { + fromBase64?: (str: string) => Uint8Array; +}; + +type BufferLike = { + from( + input: string, + encoding: 'base64', + ): { toString(encoding: 'utf-8'): string }; +}; + +const NodeBuffer = (globalThis as any).Buffer as BufferLike | undefined; + +const textDecoder = new TextDecoder('utf-8'); + /** * Decode base64 string. * @private */ +const decodeBase64: (str: string) => string = (() => { + // 1) Modern Web / some runtimes + if (typeof (Uint8Array as Uint8ArrayWithBase64).fromBase64 === 'function') { + return (str: string) => + textDecoder.decode((Uint8Array as Uint8ArrayWithBase64).fromBase64!(str)); + } -function decodeBase64(str: string): string { - return Buffer.from(str, 'base64').toString(); -} + // 2) Node.js (fast path) + if (typeof NodeBuffer?.from === 'function') { + return (str: string) => NodeBuffer.from(str, 'base64').toString('utf-8'); + } + + // 3) Browser fallback + return (str: string) => { + const binary = atob(str); + return textDecoder.decode( + Uint8Array.from(binary, (char) => char.charCodeAt(0)), + ); + }; +})(); /** * Get the Authorization header from request object. diff --git a/tsconfig.json b/tsconfig.json index 8eef991..0e01057 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "outDir": "dist", "module": "nodenext", "moduleResolution": "nodenext", - "types": ["node"] + "types": ["./globals.d.ts"] }, "include": ["src/**/*"] }