From 6f8a02b51f35382d7c86f990916b4efe6751a203 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Thu, 6 Jun 2024 17:56:55 +0000 Subject: [PATCH] feat(api): update via SDK Studio --- api.md | 7 -- src/index.ts | 3 - src/resources/index.ts | 1 - src/resources/webhooks.ts | 120 --------------------------- tests/api-resources/webhooks.test.ts | 96 --------------------- 5 files changed, 227 deletions(-) delete mode 100644 src/resources/webhooks.ts delete mode 100644 tests/api-resources/webhooks.test.ts diff --git a/api.md b/api.md index 9537af2..c4aa917 100644 --- a/api.md +++ b/api.md @@ -165,13 +165,6 @@ Methods: - client.dashboards.getEmbeddableURL({ ...params }) -> DashboardGetEmbeddableURLResponse -# Webhooks - -Methods: - -- client.webhooks.unwrap(payload, headers, secret) -> Object -- client.webhooks.verifySignature(body, headers, secret) -> void - # Usage Types: diff --git a/src/index.ts b/src/index.ts index 91424b3..4a1ed63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -135,7 +135,6 @@ export class Metronome extends Core.APIClient { creditTypes: API.CreditTypes = new API.CreditTypes(this); customers: API.Customers = new API.Customers(this); dashboards: API.Dashboards = new API.Dashboards(this); - webhooks: API.Webhooks = new API.Webhooks(this); usage: API.Usage = new API.Usage(this); auditLogs: API.AuditLogs = new API.AuditLogs(this); customFields: API.CustomFields = new API.CustomFields(this); @@ -277,8 +276,6 @@ export namespace Metronome { export import DashboardGetEmbeddableURLResponse = API.DashboardGetEmbeddableURLResponse; export import DashboardGetEmbeddableURLParams = API.DashboardGetEmbeddableURLParams; - export import Webhooks = API.Webhooks; - export import Usage = API.Usage; export import UsageListResponse = API.UsageListResponse; export import UsageListWithGroupsResponse = API.UsageListWithGroupsResponse; diff --git a/src/resources/index.ts b/src/resources/index.ts index 0515938..bcda748 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -81,4 +81,3 @@ export { UsageListWithGroupsParams, Usage, } from './usage'; -export { Webhooks } from './webhooks'; diff --git a/src/resources/webhooks.ts b/src/resources/webhooks.ts deleted file mode 100644 index 621114b..0000000 --- a/src/resources/webhooks.ts +++ /dev/null @@ -1,120 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../resource'; -import { createHmac } from 'crypto'; -import { getRequiredHeader, HeadersLike } from '../core'; - -export class Webhooks extends APIResource { - /** - * Validates that the given payload was sent by Metronome and parses the payload. - */ - unwrap( - payload: string, - headers: HeadersLike, - secret: string | undefined | null = this._client.webhookSecret, - ): Object { - this.verifySignature(payload, headers, secret); - return JSON.parse(payload); - } - - private validateSecret(secret: string | null | undefined): asserts secret is string { - if (typeof secret !== 'string') { - throw new Error( - "The webhook secret must either be set using the env var, METRONOME_WEBHOOK_SECRET, on the client class, Metronome({ webhook_secret: '123' }), or passed to this function", - ); - } - - return; - } - - private signPayload(payload: string, { date, secret }: { date: string; secret: string }) { - const encoder = new TextEncoder(); - const toSign = encoder.encode(`${date}\n${payload}`); - - const hmac = createHmac('sha256', secret); - hmac.update(toSign); - - return hmac.digest('hex'); - } - - /** Make an assertion, if not `true`, then throw. */ - private assert(expr: unknown, msg = ''): asserts expr { - if (!expr) { - throw new Error(msg); - } - } - - /** Compare to array buffers or data views in a way that timing based attacks - * cannot gain information about the platform. */ - private timingSafeEqual( - a: ArrayBufferView | ArrayBufferLike | DataView, - b: ArrayBufferView | ArrayBufferLike | DataView, - ): boolean { - if (a.byteLength !== b.byteLength) { - return false; - } - if (!(a instanceof DataView)) { - a = new DataView(ArrayBuffer.isView(a) ? a.buffer : a); - } - if (!(b instanceof DataView)) { - b = new DataView(ArrayBuffer.isView(b) ? b.buffer : b); - } - this.assert(a instanceof DataView); - this.assert(b instanceof DataView); - const length = a.byteLength; - let out = 0; - let i = -1; - while (++i < length) { - out |= a.getUint8(i) ^ b.getUint8(i); - } - return out === 0; - } - - /** - * Validates whether or not the webhook payload was sent by Metronome. - * - * An error will be raised if the webhook payload was not sent by Metronome. - */ - verifySignature( - body: string, - headers: HeadersLike, - secret: string | undefined | null = this._client.webhookSecret, - ): void { - this.validateSecret(secret); - - const msgDate = getRequiredHeader(headers, 'Date'); - const msgSignature = getRequiredHeader(headers, 'Metronome-Webhook-Signature'); - - const now = Date.now(); - const timestampSeconds = Math.floor(new Date(msgDate).valueOf()); - - if (isNaN(timestampSeconds)) { - throw new Error(`Invalid timestamp header: ${msgDate}`); - } - - if (typeof body !== 'string') { - throw new Error( - 'Webhook body must be passed as the raw JSON string sent from the server (do not parse it first).', - ); - } - - const webhook_tolerance_in_seconds = 5 * 60; // 5 minutes - if (now - timestampSeconds > webhook_tolerance_in_seconds) { - throw new Error('Webhook timestamp is too old'); - } - - if (timestampSeconds > now + webhook_tolerance_in_seconds) { - throw new Error('Webhook timestamp is too new'); - } - - const expectedSignature = this.signPayload(body, { date: msgDate, secret }); - const encoder = new globalThis.TextEncoder(); - - if (this.timingSafeEqual(encoder.encode(msgSignature), encoder.encode(expectedSignature))) { - // valid! - return; - } - - throw new Error('The given webhook signature does not match the expected signature'); - } -} diff --git a/tests/api-resources/webhooks.test.ts b/tests/api-resources/webhooks.test.ts deleted file mode 100644 index 9ac3666..0000000 --- a/tests/api-resources/webhooks.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import Metronome from 'metronome'; - -const metronome = new Metronome({ - bearerToken: 'My Bearer Token', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource webhooks', () => { - const requestBody = `{ - "id": "b2c9e307-624e-4e7d-a5a4-1b74107d78c4", - "type": "widget_created", - "properties": { - "customer_id": "5f794d50-085a-4db6-8d15-286e518b7225", - "widget_id": "0891458d-b6f0-4fdd-a41e-380aae1a1e38" - } - }`; - const payload = Buffer.from(requestBody).toString('utf8'); - const signature = 'b82652fa2246cf1d8a27e591f155c865f68b46c19b9213fd9c052f2419b4742b'; - const date = 'Mon, 02 Jan 2006 22:04:05 GMT'; - const headers = { - Date: date, - 'Metronome-Webhook-Signature': signature, - }; - const secret = 'correct-horse-battery-staple'; - const fakeNow = new Date(date).valueOf(); - - beforeEach(() => { - jest.spyOn(global.Date, 'now').mockImplementation(() => fakeNow); - }); - - afterEach(() => { - // restore the spy created with spyOn - jest.restoreAllMocks(); - }); - - describe('unwrap', () => { - it('deserializes the payload object', () => { - metronome.webhooks.unwrap(payload, headers, secret); - }); - }); - - it('should pass for valid signature', () => { - metronome.webhooks.verifySignature(payload, headers, secret); - }); - - it('should throw for timestamp outside threshold', () => { - jest.spyOn(global.Date, 'now').mockImplementation(() => fakeNow + 360000); // 6 minutes - expect(() => - metronome.webhooks.verifySignature(payload, headers, secret), - ).toThrowErrorMatchingInlineSnapshot(`"Webhook timestamp is too old"`); - - jest.spyOn(global.Date, 'now').mockImplementation(() => fakeNow - 360000); // 6 minutes - expect(() => - metronome.webhooks.verifySignature(payload, headers, secret), - ).toThrowErrorMatchingInlineSnapshot(`"Webhook timestamp is too new"`); - }); - - it('should throw an error for invalid secret format', () => { - expect(() => { - metronome.webhooks.verifySignature(payload, headers, 'invalid secret'); - }).toThrowErrorMatchingInlineSnapshot( - `"The given webhook signature does not match the expected signature"`, - ); - }); - - it('should throw for invalid signature', () => { - expect(() => - metronome.webhooks.verifySignature(payload, headers, Buffer.from('foo').toString('base64')), - ).toThrowErrorMatchingInlineSnapshot( - `"The given webhook signature does not match the expected signature"`, - ); - }); - - it('should throw for invalid timestamp', () => { - expect(() => - metronome.webhooks.verifySignature( - payload, - { - ...headers, - Date: 'nan', - }, - secret, - ), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid timestamp header: nan"`); - }); - - it('should throw if payload is not a string', () => { - expect(() => - metronome.webhooks.verifySignature({ payload: 'not a string' } as any, headers, secret), - ).toThrowErrorMatchingInlineSnapshot( - `"Webhook body must be passed as the raw JSON string sent from the server (do not parse it first)."`, - ); - }); -});