Skip to content

Commit

Permalink
fix: correct distinction of error types
Browse files Browse the repository at this point in the history
  • Loading branch information
JadsonLucena committed Feb 23, 2024
1 parent 995be5c commit f6dd00e
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 56 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ key(param?: string | ArrayBuffer | Buffer | TypedArray | DataView | KeyObject |

/**
* @throws {TypeError} Invalid ttl
* @throws {SyntaxError} Invalid ttl
* @see https://wikipedia.org/wiki/Time_to_live
*/
ttl(param?: number = 86400): void
Expand All @@ -75,12 +76,14 @@ ttl(param?: number = 86400): void
* @method
* @throws {TypeError} Invalid prefix
* @throws {TypeError} Invalid accessControlAllowMethods
* @throws {SyntaxError} Invalid accessControlAllowMethods
* @throws {TypeError} Invalid algorithm
* @throws {TypeError} Invalid key
* @throws {TypeError} Invalid nonce
* @throws {TypeError} Invalid remoteAddress
* @throws {SyntaxError} Invalid remoteAddress
* @throws {TypeError} Invalid ttl
* @throws {SyntaxError} Invalid ttl
*/
signCookie(
prefix: string, // A prefix encodes a scheme (either http:// or https://), FQDN, and an optional path. Ending the path with a / is optional but recommended. The prefix shouldn't include query parameters or fragments such as ? or #.
Expand Down Expand Up @@ -110,8 +113,8 @@ signCookie(
* @throws {TypeError} Invalid method
* @throws {TypeError} Invalid remoteAddress
* @throws {SyntaxError} Invalid remoteAddress
* @throws {SyntaxError} method required
* @throws {SyntaxError} remoteAddress required
* @throws {Error} method required
* @throws {Error} remoteAddress required
*/
verifyCookie(
url: string,
Expand All @@ -132,14 +135,17 @@ verifyCookie(
/**
* @method
* @throws {TypeError} Invalid url
* @throws {TypeError} Invalid accessControlAllowMethods
* @throws {TypeError} Invalid algorithm
* @throws {TypeError} Invalid accessControlAllowMethods
* @throws {SyntaxError} Invalid accessControlAllowMethods
* @throws {TypeError} Invalid key
* @throws {TypeError} Invalid nonce
* @throws {TypeError} Invalid pathname
* @throws {SyntaxError} Invalid pathname
* @throws {TypeError} Invalid remoteAddress
* @throws {SyntaxError} Invalid remoteAddress
* @throws {TypeError} Invalid ttl
* @throws {SyntaxError} Invalid ttl
*/
signURL(
url: string,
Expand Down Expand Up @@ -170,8 +176,8 @@ signURL(
* @throws {TypeError} Invalid method
* @throws {TypeError} Invalid remoteAddress
* @throws {SyntaxError} Invalid remoteAddress
* @throws {SyntaxError} method required
* @throws {SyntaxError} remoteAddress required
* @throws {Error} method required
* @throws {Error} remoteAddress required
*/
verifyURL(
url: string,
Expand Down
81 changes: 52 additions & 29 deletions src/SignedAccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,11 @@ class SignedAccess {
}

/**
* @getter
* @return {string}
*/
get algorithm () { return this.#algorithm }
/**
* @getter
* @return {Key}
*/
get key () { return this.#key }
/**
* @getter
* @return {number}
*/
get ttl () { return this.#ttl }

/**
* @setter
* @type {string}
* @default 'sha512'
*
* @throws {TypeError} Invalid algorithm
*
* @see https://nodejs.org/api/crypto.html#cryptogethashes
*/
set algorithm (
Expand All @@ -74,8 +60,15 @@ class SignedAccess {
}

/**
* @setter
* @return {string}
*/
get algorithm () { return this.#algorithm }

/**
* @type {Key}
*
* @throws {TypeError} Invalid key
*
* @see https://nodejs.org/api/crypto.html#cryptocreatehmacalgorithm-key-options
*/
set key (key) {
Expand All @@ -88,23 +81,38 @@ class SignedAccess {
this.#key = key
}

/**
* @return {Key}
*/
get key () { return this.#key }

/**
* Time to Live in seconds
* @setter
* @type {number}
* @default 86400
*
* @throws {TypeError} Invalid ttl
* @throws {SyntaxError} Invalid ttl
*
* @see https://wikipedia.org/wiki/Time_to_live
*/
set ttl (
ttl = 86400 // Seconds
) {
if (!Number.isSafeInteger(ttl) || ttl < 1) {
if (!Number.isSafeInteger(ttl)) {
throw new TypeError('Invalid ttl')
} else if (ttl < 1) {
throw new SyntaxError('Invalid ttl')
}

this.#ttl = ttl
}

/**
* @return {number}
*/
get ttl () { return this.#ttl }

#encodePrefix (prefix) {
prefix = new URL(prefix)

Expand Down Expand Up @@ -153,12 +161,15 @@ class SignedAccess {
* @throws {TypeError} Invalid url
* @throws {TypeError} Invalid algorithm
* @throws {TypeError} Invalid accessControlAllowMethods
* @throws {SyntaxError} Invalid accessControlAllowMethods
* @throws {TypeError} Invalid key
* @throws {TypeError} Invalid nonce
* @throws {TypeError} Invalid pathname
* @throws {SyntaxError} Invalid pathname
* @throws {TypeError} Invalid remoteAddress
* @throws {SyntaxError} Invalid remoteAddress
* @throws {TypeError} Invalid ttl
* @throws {SyntaxError} Invalid ttl
*
* @return {string} Signed URL
*/
Expand All @@ -176,18 +187,24 @@ class SignedAccess {
) {
url = new URL(url)

if (!new RegExp(`^\\s*(\\*|(${this.#HTTPMethods.join('|')})(\\s*,\\s*(${this.#HTTPMethods.join('|')}))*)\\s*$`, 'i').test(accessControlAllowMethods)) {
if (typeof accessControlAllowMethods !== 'string') {
throw new TypeError('Invalid accessControlAllowMethods')
} else if (!new RegExp(`^\\s*(\\*|(${this.#HTTPMethods.join('|')})(\\s*,\\s*(${this.#HTTPMethods.join('|')}))*)\\s*$`, 'i').test(accessControlAllowMethods)) {
throw new SyntaxError('Invalid accessControlAllowMethods')
} else if (typeof nonce !== 'string') {
throw new TypeError('Invalid nonce')
} else if (typeof pathname !== 'string' || !decodeURIComponent(url.pathname).startsWith(pathname)) {
} else if (typeof pathname !== 'string') {
throw new TypeError('Invalid pathname')
} else if (pathname && !decodeURIComponent(url.pathname).startsWith(pathname)) {
throw new SyntaxError('Invalid pathname')
} else if (typeof remoteAddress !== 'string') {
throw new TypeError('Invalid remoteAddress')
} else if (remoteAddress && net.isIP(remoteAddress) === 0) {
throw new SyntaxError('Invalid remoteAddress')
} else if (!Number.isSafeInteger(ttl) || ttl < 1) {
} else if (!Number.isSafeInteger(ttl)) {
throw new TypeError('Invalid ttl')
} else if (ttl < 1) {
throw new SyntaxError('Invalid ttl')
}

url.searchParams.delete('expires')
Expand Down Expand Up @@ -254,9 +271,9 @@ class SignedAccess {
}

if (url.searchParams.has('method') && !method.trim()) {
throw new SyntaxError('method required')
throw new Error('method required')
} else if (url.searchParams.has('ip') && !remoteAddress.trim()) {
throw new SyntaxError('remoteAddress required')
throw new Error('remoteAddress required')
}

const signature = url.searchParams.get('signature')
Expand Down Expand Up @@ -303,12 +320,14 @@ class SignedAccess {
*
* @throws {TypeError} Invalid prefix
* @throws {TypeError} Invalid accessControlAllowMethods
* @throws {SyntaxError} Invalid accessControlAllowMethods
* @throws {TypeError} Invalid algorithm
* @throws {TypeError} Invalid key
* @throws {TypeError} Invalid nonce
* @throws {TypeError} Invalid remoteAddress
* @throws {SyntaxError} Invalid remoteAddress
* @throws {TypeError} Invalid ttl
* @throws {SyntaxError} Invalid ttl
*
* @return {string} Signed cookie
*/
Expand All @@ -325,16 +344,20 @@ class SignedAccess {
) {
if (typeof prefix !== 'string') {
throw new TypeError('Invalid prefix')
} else if (!new RegExp(`^\\s*(\\*|(${this.#HTTPMethods.join('|')})(\\s*,\\s*(${this.#HTTPMethods.join('|')}))*)\\s*$`, 'i').test(accessControlAllowMethods)) {
} else if (typeof accessControlAllowMethods !== 'string') {
throw new TypeError('Invalid accessControlAllowMethods')
} else if (!new RegExp(`^\\s*(\\*|(${this.#HTTPMethods.join('|')})(\\s*,\\s*(${this.#HTTPMethods.join('|')}))*)\\s*$`, 'i').test(accessControlAllowMethods)) {
throw new SyntaxError('Invalid accessControlAllowMethods')
} else if (typeof nonce !== 'string') {
throw new TypeError('Invalid nonce')
} else if (typeof remoteAddress !== 'string') {
throw new TypeError('Invalid remoteAddress')
} else if (remoteAddress && net.isIP(remoteAddress) === 0) {
throw new SyntaxError('Invalid remoteAddress')
} else if (!Number.isSafeInteger(ttl) || ttl < 1) {
} else if (!Number.isSafeInteger(ttl)) {
throw new TypeError('Invalid ttl')
} else if (ttl < 1) {
throw new SyntaxError('Invalid ttl')
}

const cookie = new URLSearchParams()
Expand Down Expand Up @@ -396,9 +419,9 @@ class SignedAccess {
}

if (cookie.has('method') && !method.trim()) {
throw new SyntaxError('method required')
throw new Error('method required')
} else if (cookie.has('ip') && !remoteAddress.trim()) {
throw new SyntaxError('remoteAddress required')
throw new Error('remoteAddress required')
}

const signature = cookie.get('signature')
Expand Down
40 changes: 18 additions & 22 deletions test/SignedAccess.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const signedAccess = new SignedAccess({
describe('constructor', () => {
test('type guards', () => {
['xpto', 0, false, null].forEach(input => expect(() => new SignedAccess({ algorithm: input, key })).toThrowError(new TypeError('Invalid algorithm')));
['xpto', '', 0, Infinity, NaN, false, null].forEach(input => expect(() => new SignedAccess({ ttl: input, key })).toThrowError(new TypeError('Invalid ttl')));
['xpto', '', 0, Infinity, NaN, false, null].forEach(input => expect(() => new SignedAccess({ ttl: input, key })).toThrow('Invalid ttl'));
[0, false, null].forEach(input => expect(() => new SignedAccess({ key: input })).toThrowError(new TypeError('Invalid key')))
})

Expand Down Expand Up @@ -54,13 +54,12 @@ describe('signURL', () => {
test('type guards', () => {
[undefined, 0, false, null].forEach(input => expect(() => signedAccess.signURL(input)).toThrowError(new TypeError('Invalid URL')));
['xpto', 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { algorithm: input })).toThrowError(new TypeError('Invalid algorithm')));
['tomorrow', 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { ttl: input })).toThrowError(new TypeError('Invalid ttl')));
[127001, 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { remoteAddress: input })).toThrowError(new TypeError('Invalid remoteAddress')));
['127.000.000.001', '127.0.0.1/24', 'fhqwhgads'].forEach(input => expect(() => signedAccess.signURL(url, { remoteAddress: input })).toThrowError(new SyntaxError('Invalid remoteAddress')));
['tomorrow', 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { ttl: input })).toThrow('Invalid ttl'));
['127.000.000.001', '127.0.0.1/24', 'fhqwhgads', 127001, 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { remoteAddress: input })).toThrow('Invalid remoteAddress'));
[0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { key: input })).toThrowError(new TypeError('Invalid key')));
['GETTER', 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { accessControlAllowMethods: input })).toThrowError(new TypeError('Invalid accessControlAllowMethods')));
['GETTER', 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { accessControlAllowMethods: input })).toThrow('Invalid accessControlAllowMethods'));
[0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { nonce: input })).toThrowError(new TypeError('Invalid nonce')));
['/github/', 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { pathname: input })).toThrowError(new TypeError('Invalid pathname')))
['/github/', 0, false, null].forEach(input => expect(() => signedAccess.signURL(url, { pathname: input })).toThrow('Invalid pathname'))
})

test('default values', () => {
Expand Down Expand Up @@ -140,8 +139,7 @@ describe('verifyURL', () => {

[undefined, 0, false, null].forEach(input => expect(() => signedAccess.verifyURL(input)).toThrowError(new TypeError('Invalid URL')));
['xpto', 0, false, null].forEach(input => expect(() => signedAccess.verifyURL(signedURL, { algorithm: input })).toThrowError(new TypeError('Invalid algorithm')));
[127001, 0, false, null].forEach(input => expect(() => signedAccess.verifyURL(signedURL, { remoteAddress: input })).toThrowError(new TypeError('Invalid remoteAddress')));
['127.000.000.001', '127.0.0.1/24', 'fhqwhgads'].forEach(input => expect(() => signedAccess.verifyURL(signedURL, { remoteAddress: input })).toThrowError(new SyntaxError('Invalid remoteAddress')));
['127.000.000.001', '127.0.0.1/24', 'fhqwhgads', 127001, 0, false, null].forEach(input => expect(() => signedAccess.verifyURL(signedURL, { remoteAddress: input })).toThrow('Invalid remoteAddress'));
[0, false, null].forEach(input => expect(() => signedAccess.verifyURL(signedURL, { key: input })).toThrowError(new TypeError('Invalid key')));
['GETTER', 0, false, null].forEach(input => expect(() => signedAccess.verifyURL(signedURL, { method: input })).toThrowError(new TypeError('Invalid method')))
})
Expand All @@ -164,7 +162,7 @@ describe('verifyURL', () => {

signedURL = signedAccess.signURL(url, { remoteAddress: '127.0.0.1' })

expect(() => signedAccess.verifyURL(signedURL)).toThrowError(new SyntaxError('remoteAddress required'))
expect(() => signedAccess.verifyURL(signedURL)).toThrowError(new Error('remoteAddress required'))
expect(signedAccess.verifyURL(signedURL, { remoteAddress: '142.251.129.78' })).toBeFalsy()
expect(signedAccess.verifyURL(signedURL, { remoteAddress: '127.0.0.1' })).toBeTruthy()

Expand All @@ -175,7 +173,7 @@ describe('verifyURL', () => {

signedURL = signedAccess.signURL(url, { accessControlAllowMethods: 'POST' })

expect(() => signedAccess.verifyURL(signedURL)).toThrowError(new SyntaxError('method required'))
expect(() => signedAccess.verifyURL(signedURL)).toThrowError(new Error('method required'))
expect(signedAccess.verifyURL(signedURL, { method: 'PATCH' })).toBeFalsy()
expect(signedAccess.verifyURL(signedURL, { method: 'POST' })).toBeTruthy()

Expand Down Expand Up @@ -212,8 +210,8 @@ describe('verifyURL', () => {

mockSignedURL = `https://github.com/JadsonLucena/WebSocket.js?${new URL(signedURL).searchParams.toString()}`

expect(() => signedAccess.verifyURL(mockSignedURL, { method: 'POST' })).toThrowError(new SyntaxError('remoteAddress required'))
expect(() => signedAccess.verifyURL(mockSignedURL, { remoteAddress: '142.251.129.78' })).toThrowError(new SyntaxError('method required'))
expect(() => signedAccess.verifyURL(mockSignedURL, { method: 'POST' })).toThrowError(new Error('remoteAddress required'))
expect(() => signedAccess.verifyURL(mockSignedURL, { remoteAddress: '142.251.129.78' })).toThrowError(new Error('method required'))
expect(signedAccess.verifyURL(mockSignedURL, {
remoteAddress: '142.251.129.78',
method: 'DELETE'
Expand All @@ -239,11 +237,10 @@ describe('signCookie', () => {
test('type guards', () => {
[undefined, 0, false, null].forEach(input => expect(() => signedAccess.signCookie(input)).toThrowError(new TypeError('Invalid prefix')));
['xpto', 0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { algorithm: input })).toThrowError(new TypeError('Invalid algorithm')));
['tomorrow', 0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { ttl: input })).toThrowError(new TypeError('Invalid ttl')));
[127001, 0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { remoteAddress: input })).toThrowError(new TypeError('Invalid remoteAddress')));
['127.000.000.001', '127.0.0.1/24', 'fhqwhgads'].forEach(input => expect(() => signedAccess.signCookie(prefix, { remoteAddress: input })).toThrowError(new SyntaxError('Invalid remoteAddress')));
['tomorrow', 0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { ttl: input })).toThrow('Invalid ttl'));
['127.000.000.001', '127.0.0.1/24', 'fhqwhgads', 127001, 0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { remoteAddress: input })).toThrow('Invalid remoteAddress'));
[0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { key: input })).toThrowError(new TypeError('Invalid key')));
['GETTER', 0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { accessControlAllowMethods: input })).toThrowError(new TypeError('Invalid accessControlAllowMethods')));
['GETTER', 0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { accessControlAllowMethods: input })).toThrow('Invalid accessControlAllowMethods'));
[0, false, null].forEach(input => expect(() => signedAccess.signCookie(prefix, { nonce: input })).toThrowError(new TypeError('Invalid nonce')))
})

Expand Down Expand Up @@ -304,8 +301,7 @@ describe('verifyCookie', () => {
[undefined, 0, false, null].forEach(input => expect(() => signedAccess.verifyCookie(input, signedCookie)).toThrowError(new TypeError('Invalid URL')));
[undefined, 0, false, null].forEach(input => expect(() => signedAccess.verifyCookie(mockURL, input)).toThrowError(new TypeError('Invalid cookie')));
['xpto', 0, false, null].forEach(input => expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { algorithm: input })).toThrowError(new TypeError('Invalid algorithm')));
[127001, 0, false, null].forEach(input => expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { remoteAddress: input })).toThrowError(new TypeError('Invalid remoteAddress')));
['127.000.000.001', '127.0.0.1/24', 'fhqwhgads'].forEach(input => expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { remoteAddress: input })).toThrowError(new SyntaxError('Invalid remoteAddress')));
['127.000.000.001', '127.0.0.1/24', 'fhqwhgads', 127001, 0, false, null].forEach(input => expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { remoteAddress: input })).toThrow('Invalid remoteAddress'));
[0, false, null].forEach(input => expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { key: input })).toThrowError(new TypeError('Invalid key')));
['GETTER', 0, false, null].forEach(input => expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { method: input })).toThrowError(new TypeError('Invalid method')))
})
Expand All @@ -332,7 +328,7 @@ describe('verifyCookie', () => {

signedCookie = signedAccess.signCookie(prefix, { remoteAddress: '127.0.0.1' })

expect(() => signedAccess.verifyCookie(mockURL, signedCookie)).toThrowError(new SyntaxError('remoteAddress required'))
expect(() => signedAccess.verifyCookie(mockURL, signedCookie)).toThrowError(new Error('remoteAddress required'))
expect(signedAccess.verifyCookie(mockURL, signedCookie, { remoteAddress: '142.251.129.78' })).toBeFalsy()
expect(signedAccess.verifyCookie(mockURL, signedCookie, { remoteAddress: '127.0.0.1' })).toBeTruthy()

Expand All @@ -343,7 +339,7 @@ describe('verifyCookie', () => {

signedCookie = signedAccess.signCookie(prefix, { accessControlAllowMethods: 'POST' })

expect(() => signedAccess.verifyCookie(mockURL, signedCookie)).toThrowError(new SyntaxError('method required'))
expect(() => signedAccess.verifyCookie(mockURL, signedCookie)).toThrowError(new Error('method required'))
expect(signedAccess.verifyCookie(mockURL, signedCookie, { method: 'PATCH' })).toBeFalsy()
expect(signedAccess.verifyCookie(mockURL, signedCookie, { method: 'POST' })).toBeTruthy()

Expand All @@ -363,8 +359,8 @@ describe('verifyCookie', () => {
nonce: crypto.randomUUID()
})

expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { method: 'POST' })).toThrowError(new SyntaxError('remoteAddress required'))
expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { remoteAddress: '142.251.129.78' })).toThrowError(new SyntaxError('method required'))
expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { method: 'POST' })).toThrowError(new Error('remoteAddress required'))
expect(() => signedAccess.verifyCookie(mockURL, signedCookie, { remoteAddress: '142.251.129.78' })).toThrowError(new Error('method required'))
expect(signedAccess.verifyCookie(mockURL, signedCookie, {
remoteAddress: '142.251.129.78',
method: 'DELETE'
Expand Down

0 comments on commit f6dd00e

Please sign in to comment.