@workos-inc/node
Advanced tools
Comparing version 7.9.0 to 7.10.0
@@ -10,1 +10,2 @@ export * from './event.interface'; | ||
export * from './pagination-options.interface'; | ||
export * from './http-client.interface'; |
@@ -26,1 +26,2 @@ "use strict"; | ||
__exportStar(require("./pagination-options.interface"), exports); | ||
__exportStar(require("./http-client.interface"), exports); |
@@ -8,2 +8,3 @@ import { AppInfo } from './app-info.interface'; | ||
appInfo?: AppInfo; | ||
fetchFn?: typeof fetch; | ||
} |
import { Event } from '../common/interfaces'; | ||
export declare class Webhooks { | ||
private encoder; | ||
private cryptoProvider; | ||
constructor(subtleCrypto?: typeof crypto.subtle); | ||
constructEvent({ payload, sigHeader, secret, tolerance, }: { | ||
@@ -18,3 +19,2 @@ payload: unknown; | ||
computeSignature(timestamp: any, payload: any, secret: string): Promise<string>; | ||
secureCompare(stringA: string, stringB: string): Promise<boolean>; | ||
} |
@@ -15,5 +15,11 @@ "use strict"; | ||
const serializers_1 = require("../common/serializers"); | ||
const crypto_1 = require("../common/crypto"); | ||
class Webhooks { | ||
constructor() { | ||
this.encoder = new TextEncoder(); | ||
constructor(subtleCrypto) { | ||
if (typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined') { | ||
this.cryptoProvider = new crypto_1.SubtleCryptoProvider(subtleCrypto); | ||
} | ||
else { | ||
this.cryptoProvider = new crypto_1.NodeCryptoProvider(); | ||
} | ||
} | ||
@@ -38,3 +44,4 @@ constructEvent({ payload, sigHeader, secret, tolerance = 180000, }) { | ||
const expectedSig = yield this.computeSignature(timestamp, payload, secret); | ||
if ((yield this.secureCompare(expectedSig, signatureHash)) === false) { | ||
if ((yield this.cryptoProvider.secureCompare(expectedSig, signatureHash)) === | ||
false) { | ||
throw new exceptions_1.SignatureVerificationException('Signature hash does not match the expected signature hash for payload'); | ||
@@ -59,39 +66,6 @@ } | ||
const signedPayload = `${timestamp}.${payload}`; | ||
const key = yield crypto.subtle.importKey('raw', this.encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); | ||
const signatureBuffer = yield crypto.subtle.sign('HMAC', key, this.encoder.encode(signedPayload)); | ||
// crypto.subtle returns the signature in base64 format. This must be | ||
// encoded in hex to match the CryptoProvider contract. We map each byte in | ||
// the buffer to its corresponding hex octet and then combine into a string. | ||
const signatureBytes = new Uint8Array(signatureBuffer); | ||
const signatureHexCodes = new Array(signatureBytes.length); | ||
for (let i = 0; i < signatureBytes.length; i++) { | ||
signatureHexCodes[i] = byteHexMapping[signatureBytes[i]]; | ||
} | ||
return signatureHexCodes.join(''); | ||
return yield this.cryptoProvider.computeHMACSignatureAsync(signedPayload, secret); | ||
}); | ||
} | ||
secureCompare(stringA, stringB) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const bufferA = this.encoder.encode(stringA); | ||
const bufferB = this.encoder.encode(stringB); | ||
if (bufferA.length !== bufferB.length) { | ||
return false; | ||
} | ||
const algorithm = { name: 'HMAC', hash: 'SHA-256' }; | ||
const key = (yield crypto.subtle.generateKey(algorithm, false, [ | ||
'sign', | ||
'verify', | ||
])); | ||
const hmac = yield crypto.subtle.sign(algorithm, key, bufferA); | ||
const equal = yield crypto.subtle.verify(algorithm, key, hmac, bufferB); | ||
return equal; | ||
}); | ||
} | ||
} | ||
exports.Webhooks = Webhooks; | ||
// Cached mapping of byte to hex representation. We do this once to avoid re- | ||
// computing every time we need to convert the result of a signature to hex. | ||
const byteHexMapping = new Array(256); | ||
for (let i = 0; i < byteHexMapping.length; i++) { | ||
byteHexMapping[i] = i.toString(16).padStart(2, '0'); | ||
} |
@@ -20,2 +20,3 @@ "use strict"; | ||
const exceptions_1 = require("../common/exceptions"); | ||
const crypto_2 = require("../common/crypto"); | ||
describe('Webhooks', () => { | ||
@@ -191,2 +192,30 @@ let payload; | ||
}); | ||
describe('when in an environment that supports SubtleCrypto', () => { | ||
it('automatically uses the subtle crypto library', () => { | ||
// tslint:disable-next-line | ||
expect(workos.webhooks['cryptoProvider']).toBeInstanceOf(crypto_2.SubtleCryptoProvider); | ||
}); | ||
}); | ||
describe('CryptoProvider', () => { | ||
describe('when computing HMAC signature', () => { | ||
it('returns the same for the Node crypto and Web Crypto versions', () => __awaiter(void 0, void 0, void 0, function* () { | ||
const nodeCryptoProvider = new crypto_2.NodeCryptoProvider(); | ||
const subtleCryptoProvider = new crypto_2.SubtleCryptoProvider(); | ||
const stringifiedPayload = JSON.stringify(payload); | ||
const payloadHMAC = `${timestamp}.${stringifiedPayload}`; | ||
const nodeCompare = yield nodeCryptoProvider.computeHMACSignatureAsync(payloadHMAC, secret); | ||
const subtleCompare = yield subtleCryptoProvider.computeHMACSignatureAsync(payloadHMAC, secret); | ||
expect(nodeCompare).toEqual(subtleCompare); | ||
})); | ||
}); | ||
describe('when securely comparing', () => { | ||
it('returns the same for the Node crypto and Web Crypto versions', () => __awaiter(void 0, void 0, void 0, function* () { | ||
const nodeCryptoProvider = new crypto_2.NodeCryptoProvider(); | ||
const subtleCryptoProvider = new crypto_2.SubtleCryptoProvider(); | ||
const signature = yield workos.webhooks.computeSignature(timestamp, payload, secret); | ||
expect(nodeCryptoProvider.secureCompare(signature, signatureHash)).toEqual(subtleCryptoProvider.secureCompare(signature, signatureHash)); | ||
expect(nodeCryptoProvider.secureCompare(signature, 'foo')).toEqual(subtleCryptoProvider.secureCompare(signature, 'foo')); | ||
})); | ||
}); | ||
}); | ||
}); |
@@ -42,3 +42,3 @@ import { GetOptions, PostOptions, PutOptions, WorkOSOptions } from './common/interfaces'; | ||
emitWarning(warning: string): void; | ||
private handleFetchError; | ||
private handleHttpError; | ||
} |
@@ -26,4 +26,4 @@ "use strict"; | ||
const bad_request_exception_1 = require("./common/exceptions/bad-request.exception"); | ||
const fetch_client_1 = require("./common/utils/fetch-client"); | ||
const VERSION = '7.9.0'; | ||
const net_1 = require("./common/net"); | ||
const VERSION = '7.10.0'; | ||
const DEFAULT_HOSTNAME = 'api.workos.com'; | ||
@@ -68,3 +68,3 @@ class WorkOS { | ||
} | ||
this.client = new fetch_client_1.FetchClient(this.baseURL, Object.assign(Object.assign({}, options.config), { headers: Object.assign(Object.assign({}, (_a = options.config) === null || _a === void 0 ? void 0 : _a.headers), { Authorization: `Bearer ${this.key}`, 'User-Agent': userAgent }) })); | ||
this.client = (0, net_1.createHttpClient)(this.baseURL, Object.assign(Object.assign({}, options.config), { headers: Object.assign(Object.assign({}, (_a = options.config) === null || _a === void 0 ? void 0 : _a.headers), { Authorization: `Bearer ${this.key}`, 'User-Agent': userAgent }) }), options.fetchFn); | ||
} | ||
@@ -81,9 +81,10 @@ get version() { | ||
try { | ||
return yield this.client.post(path, entity, { | ||
const res = yield this.client.post(path, entity, { | ||
params: options.query, | ||
headers: requestHeaders, | ||
}); | ||
return { data: yield res.toJSON() }; | ||
} | ||
catch (error) { | ||
this.handleFetchError({ path, error }); | ||
this.handleHttpError({ path, error }); | ||
throw error; | ||
@@ -97,3 +98,3 @@ } | ||
const { accessToken } = options; | ||
return yield this.client.get(path, { | ||
const res = yield this.client.get(path, { | ||
params: options.query, | ||
@@ -104,5 +105,6 @@ headers: accessToken | ||
}); | ||
return { data: yield res.toJSON() }; | ||
} | ||
catch (error) { | ||
this.handleFetchError({ path, error }); | ||
this.handleHttpError({ path, error }); | ||
throw error; | ||
@@ -119,9 +121,10 @@ } | ||
try { | ||
return yield this.client.put(path, entity, { | ||
const res = yield this.client.put(path, entity, { | ||
params: options.query, | ||
headers: requestHeaders, | ||
}); | ||
return { data: yield res.toJSON() }; | ||
} | ||
catch (error) { | ||
this.handleFetchError({ path, error }); | ||
this.handleHttpError({ path, error }); | ||
throw error; | ||
@@ -139,3 +142,3 @@ } | ||
catch (error) { | ||
this.handleFetchError({ path, error }); | ||
this.handleHttpError({ path, error }); | ||
throw error; | ||
@@ -153,8 +156,11 @@ } | ||
} | ||
handleFetchError({ path, error }) { | ||
handleHttpError({ path, error }) { | ||
var _a; | ||
if (!(error instanceof net_1.HttpClientError)) { | ||
throw new Error(`Unexpected error: ${error}`); | ||
} | ||
const { response } = error; | ||
if (response) { | ||
const { status, data, headers } = response; | ||
const requestID = (_a = headers.get('X-Request-ID')) !== null && _a !== void 0 ? _a : ''; | ||
const requestID = (_a = headers['X-Request-ID']) !== null && _a !== void 0 ? _a : ''; | ||
const { code, error_description: errorDescription, error, errors, message, } = data; | ||
@@ -161,0 +167,0 @@ switch (status) { |
@@ -21,2 +21,3 @@ "use strict"; | ||
const rate_limit_exceeded_exception_1 = require("./common/exceptions/rate-limit-exceeded.exception"); | ||
const net_1 = require("./common/net"); | ||
describe('WorkOS', () => { | ||
@@ -99,6 +100,36 @@ beforeEach(() => jest_fetch_mock_1.default.resetMocks()); | ||
expect((0, test_utils_1.fetchHeaders)()).toMatchObject({ | ||
'User-Agent': `workos-node/${packageJson.version} fooApp: 1.0.0`, | ||
'User-Agent': `workos-node/${packageJson.version}/fetch fooApp: 1.0.0`, | ||
}); | ||
})); | ||
}); | ||
describe('when no `appInfo` option is provided', () => { | ||
it('adds the HTTP client name to the user-agent', () => __awaiter(void 0, void 0, void 0, function* () { | ||
(0, test_utils_1.fetchOnce)('{}'); | ||
const packageJson = JSON.parse(yield promises_1.default.readFile('package.json', 'utf8')); | ||
const workos = new workos_1.WorkOS('sk_test'); | ||
yield workos.post('/somewhere', {}); | ||
expect((0, test_utils_1.fetchHeaders)()).toMatchObject({ | ||
'User-Agent': `workos-node/${packageJson.version}/fetch`, | ||
}); | ||
})); | ||
}); | ||
describe('when no `appInfo` option is provided', () => { | ||
it('adds the HTTP client name to the user-agent', () => __awaiter(void 0, void 0, void 0, function* () { | ||
(0, test_utils_1.fetchOnce)('{}'); | ||
const packageJson = JSON.parse(yield promises_1.default.readFile('package.json', 'utf8')); | ||
const workos = new workos_1.WorkOS('sk_test'); | ||
yield workos.post('/somewhere', {}); | ||
expect((0, test_utils_1.fetchHeaders)()).toMatchObject({ | ||
'User-Agent': `workos-node/${packageJson.version}/fetch`, | ||
}); | ||
})); | ||
}); | ||
describe('when using an environment that supports fetch', () => { | ||
it('automatically uses the fetch HTTP client', () => { | ||
const workos = new workos_1.WorkOS('sk_test'); | ||
// Bracket notation gets past private visibility | ||
// tslint:disable-next-line | ||
expect(workos['client']).toBeInstanceOf(net_1.FetchHttpClient); | ||
}); | ||
}); | ||
}); | ||
@@ -182,10 +213,32 @@ describe('version', () => { | ||
describe('when the entity is null', () => { | ||
it('sends a null body', () => __awaiter(void 0, void 0, void 0, function* () { | ||
it('sends an empty string body', () => __awaiter(void 0, void 0, void 0, function* () { | ||
(0, test_utils_1.fetchOnce)(); | ||
const workos = new workos_1.WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); | ||
yield workos.post('/somewhere', null); | ||
expect((0, test_utils_1.fetchBody)({ raw: true })).toBeNull(); | ||
expect((0, test_utils_1.fetchBody)({ raw: true })).toBe(''); | ||
})); | ||
}); | ||
}); | ||
describe('when in an environment that does not support fetch', () => { | ||
const fetchFn = globalThis.fetch; | ||
beforeEach(() => { | ||
// @ts-ignore | ||
delete globalThis.fetch; | ||
}); | ||
afterEach(() => { | ||
globalThis.fetch = fetchFn; | ||
}); | ||
it('automatically uses the node HTTP client', () => { | ||
const workos = new workos_1.WorkOS('sk_test_key'); | ||
// tslint:disable-next-line | ||
expect(workos['client']).toBeInstanceOf(net_1.NodeHttpClient); | ||
}); | ||
it('uses a fetch function if provided', () => { | ||
const workos = new workos_1.WorkOS('sk_test_key', { | ||
fetchFn, | ||
}); | ||
// tslint:disable-next-line | ||
expect(workos['client']).toBeInstanceOf(net_1.FetchHttpClient); | ||
}); | ||
}); | ||
}); |
{ | ||
"version": "7.9.0", | ||
"version": "7.10.0", | ||
"name": "@workos-inc/node", | ||
@@ -16,5 +16,4 @@ "author": "WorkOS", | ||
"engines": { | ||
"node": ">=19" | ||
"node": ">=16" | ||
}, | ||
"engineStrict": true, | ||
"main": "lib/index.js", | ||
@@ -21,0 +20,0 @@ "typings": "lib/index.d.ts", |
@@ -12,2 +12,6 @@ # WorkOS Node.js Library | ||
## Requirements | ||
Node 16 or higher. | ||
## Installation | ||
@@ -14,0 +18,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
466174
461
10014
64
3