Skip to content

Commit

Permalink
Merge branch 'next--merge-conflict' into generated--merge-conflict
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertCraigie committed May 3, 2024
2 parents 6f1e429 + b57da29 commit 575226d
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.1.2"
".": "2.4.0"
}
97 changes: 97 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,102 @@
# Changelog

## 2.4.0 (2024-05-02)

Full Changelog: [v2.3.0...v2.4.0](https://github.com/orbcorp/orb-node/compare/v2.3.0...v2.4.0)

### Features

* **api:** add effective_date field ([#163](https://github.com/orbcorp/orb-node/issues/163)) ([7a5d9af](https://github.com/orbcorp/orb-node/commit/7a5d9afe4f578cfc7f1e8a03767f75bad3579751))
* **api:** add param to backfill create ([#174](https://github.com/orbcorp/orb-node/issues/174)) ([9379644](https://github.com/orbcorp/orb-node/commit/93796449184a9b4823a83ead1fa88b3557459588))
* **api:** add subscription update endpoint ([#168](https://github.com/orbcorp/orb-node/issues/168)) ([d7ee5ba](https://github.com/orbcorp/orb-node/commit/d7ee5bae793278692eb62159152dd5ad1f015bc9))
* **api:** add the shared model PaginationMetadata ([#164](https://github.com/orbcorp/orb-node/issues/164)) ([85956fe](https://github.com/orbcorp/orb-node/commit/85956fea41477392b725778e7d8364616236cad9))
* **api:** price evaluation endpoint generally available ([#169](https://github.com/orbcorp/orb-node/issues/169)) ([c1e2b83](https://github.com/orbcorp/orb-node/commit/c1e2b837cad4c1786ca553b0ea9fbabfc4623880))
* **api:** updates ([#154](https://github.com/orbcorp/orb-node/issues/154)) ([0f7c989](https://github.com/orbcorp/orb-node/commit/0f7c989eac9d033b1c8a03420efed405d3af4b88))
* **api:** updates ([#159](https://github.com/orbcorp/orb-node/issues/159)) ([ca602ea](https://github.com/orbcorp/orb-node/commit/ca602eaa69dcf615f4785c41a0c078b9a20af029))
* **api:** updates ([#179](https://github.com/orbcorp/orb-node/issues/179)) ([86b9015](https://github.com/orbcorp/orb-node/commit/86b9015532a7c165a605f77938c4bbadee24d452))


### Bug Fixes

* **api:** add shared model BillingCycleRelativeDate ([#167](https://github.com/orbcorp/orb-node/issues/167)) ([bb6764e](https://github.com/orbcorp/orb-node/commit/bb6764e1eed1bce9f1629e853de6a08eef0166cd))
* **api:** some path params were incorrectly typed as nullable ([#166](https://github.com/orbcorp/orb-node/issues/166)) ([bafd0a9](https://github.com/orbcorp/orb-node/commit/bafd0a9e007ad993fc129dd18836583074cd6b86))
* use UTC timezone for verifying webhook signature ([5eb9d2c](https://github.com/orbcorp/orb-node/commit/5eb9d2c50f5312d4ef82bc18bb94f95d1120b906)), closes [#156](https://github.com/orbcorp/orb-node/issues/156)


### Chores

* **internal:** add link to openapi spec ([#176](https://github.com/orbcorp/orb-node/issues/176)) ([fd77e6a](https://github.com/orbcorp/orb-node/commit/fd77e6a266b2ba7a82821552afa227eb26d69443))
* **internal:** add scripts/test and scripts/mock ([#173](https://github.com/orbcorp/orb-node/issues/173)) ([495558b](https://github.com/orbcorp/orb-node/commit/495558b5f58d1729e3cc059e1c0acdaa039640af))
* **internal:** add scripts/test, scripts/mock and add ci job ([#177](https://github.com/orbcorp/orb-node/issues/177)) ([4440933](https://github.com/orbcorp/orb-node/commit/4440933b3f3d51740d8fcfd4ab188856b09967e9))
* **internal:** formatting ([#161](https://github.com/orbcorp/orb-node/issues/161)) ([b4ae917](https://github.com/orbcorp/orb-node/commit/b4ae9171619c0a23f368c92cc3e4b415859ac6af))
* **internal:** forward arguments in scripts/test ([#178](https://github.com/orbcorp/orb-node/issues/178)) ([0564bfe](https://github.com/orbcorp/orb-node/commit/0564bfe383da0efbe642ad5801c0ef9484f9f36c))
* **internal:** move client class to separate file ([#180](https://github.com/orbcorp/orb-node/issues/180)) ([9f56e13](https://github.com/orbcorp/orb-node/commit/9f56e13cce2e88d6a4a95a73215bd348a2685a9e))
* **internal:** refactor scripts ([#175](https://github.com/orbcorp/orb-node/issues/175)) ([76f4954](https://github.com/orbcorp/orb-node/commit/76f49549e07628aea9747f2190766c6e966c3d2e))
* **internal:** update gitignore ([#160](https://github.com/orbcorp/orb-node/issues/160)) ([bbd39e8](https://github.com/orbcorp/orb-node/commit/bbd39e830f21ef19b95dc7cd3ff06450906c360e))
* **internal:** use @swc/jest for running tests ([#165](https://github.com/orbcorp/orb-node/issues/165)) ([50ad2b0](https://github.com/orbcorp/orb-node/commit/50ad2b09e4d415050660adb28b4549c55a0730f1))
* **internal:** use actions/checkout@v4 for codeflow ([#171](https://github.com/orbcorp/orb-node/issues/171)) ([dcf2157](https://github.com/orbcorp/orb-node/commit/dcf21575dda07240093966cb560585f3eb08643c))
* use debug instead of console log for webhook signature logging ([590cbf1](https://github.com/orbcorp/orb-node/commit/590cbf17e37a7eb071d6d31295b3f9cc9bb46579))


### Build System

* configure UTF-8 locale in devcontainer ([#162](https://github.com/orbcorp/orb-node/issues/162)) ([b1b8a5a](https://github.com/orbcorp/orb-node/commit/b1b8a5a1a586305e71183cd0c02f24a70513f3b1))

## 2.3.0 (2024-04-24)

Full Changelog: [v2.2.0...v2.3.0](https://github.com/orbcorp/orb-node/compare/v2.2.0...v2.3.0)

### Features

* **api:** add effective_date field ([#163](https://github.com/orbcorp/orb-node/issues/163)) ([96a98bd](https://github.com/orbcorp/orb-node/commit/96a98bd315ceb6f2239dee6267bcf0b3a7e84a39))
* **api:** add subscription update endpoint ([#168](https://github.com/orbcorp/orb-node/issues/168)) ([09b5798](https://github.com/orbcorp/orb-node/commit/09b5798769187bcb18c92210c8b336b169f72343))
* **api:** add the shared model PaginationMetadata ([#164](https://github.com/orbcorp/orb-node/issues/164)) ([15f5af3](https://github.com/orbcorp/orb-node/commit/15f5af31b39a66ef8a626af50d09abf3202afc8f))
* **api:** price evaluation endpoint generally available ([#169](https://github.com/orbcorp/orb-node/issues/169)) ([492a728](https://github.com/orbcorp/orb-node/commit/492a72833150267df706054e5d8c1ce998b27999))
* **api:** updates ([#154](https://github.com/orbcorp/orb-node/issues/154)) ([01aef6d](https://github.com/orbcorp/orb-node/commit/01aef6d41d2fd0f6174eb6116ac2a670cfc96882))
* **api:** updates ([#159](https://github.com/orbcorp/orb-node/issues/159)) ([5b6287c](https://github.com/orbcorp/orb-node/commit/5b6287c047ed1a5eab7e498690838062c233a1a7))


### Bug Fixes

* **api:** add shared model BillingCycleRelativeDate ([#167](https://github.com/orbcorp/orb-node/issues/167)) ([8677375](https://github.com/orbcorp/orb-node/commit/867737537ddecdfcaaf33bb0bd3fe29532f265c4))
* **api:** some path params were incorrectly typed as nullable ([#166](https://github.com/orbcorp/orb-node/issues/166)) ([eeb81d4](https://github.com/orbcorp/orb-node/commit/eeb81d4ade556528cf564b850b0d1b932fbfd999))
* use UTC timezone for verifying webhook signature ([9a0b87a](https://github.com/orbcorp/orb-node/commit/9a0b87a704a84608383c66cb5acb73f55aa97b3c)), closes [#156](https://github.com/orbcorp/orb-node/issues/156)


### Chores

* **internal:** formatting ([#161](https://github.com/orbcorp/orb-node/issues/161)) ([db765d8](https://github.com/orbcorp/orb-node/commit/db765d86efc540e6301fc15912360ba66218c673))
* **internal:** update gitignore ([#160](https://github.com/orbcorp/orb-node/issues/160)) ([7a2c39f](https://github.com/orbcorp/orb-node/commit/7a2c39f468ea6aba7133d6fdfd28c632c119e145))
* **internal:** use @swc/jest for running tests ([#165](https://github.com/orbcorp/orb-node/issues/165)) ([c744e70](https://github.com/orbcorp/orb-node/commit/c744e70bf74385171fea39d73c9611f792661a3c))
* use debug instead of console log for webhook signature logging ([590cbf1](https://github.com/orbcorp/orb-node/commit/590cbf17e37a7eb071d6d31295b3f9cc9bb46579))


### Build System

* configure UTF-8 locale in devcontainer ([#162](https://github.com/orbcorp/orb-node/issues/162)) ([aa09c7d](https://github.com/orbcorp/orb-node/commit/aa09c7dfc261b6e05e8e944f23a55463f80d13da))

## 2.2.0 (2024-04-08)

Full Changelog: [v2.1.2...v2.2.0](https://github.com/orbcorp/orb-node/compare/v2.1.2...v2.2.0)

### Features

* add webhooks verification helpers ([2beaa01](https://github.com/orbcorp/orb-node/commit/2beaa019f1f4e58ee6fde3cab29f500964dc3bc1))
* **api:** add `invoice_source` to invoice ([#153](https://github.com/orbcorp/orb-node/issues/153)) ([8a9b15e](https://github.com/orbcorp/orb-node/commit/8a9b15e6560c1f82888bf4f7b09536ac8d3fee01))
* **api:** add tiered package with minimum price ([#150](https://github.com/orbcorp/orb-node/issues/150)) ([6c53952](https://github.com/orbcorp/orb-node/commit/6c53952ea49a49b1565df1133f132cb0fef30215))
* **api:** remove accidental null ([#146](https://github.com/orbcorp/orb-node/issues/146)) ([e66a755](https://github.com/orbcorp/orb-node/commit/e66a7556055d4f3638245f338769a8565cdddddc))
* **api:** timeframe_end and timeframe_start accept null ([#148](https://github.com/orbcorp/orb-node/issues/148)) ([bab2d25](https://github.com/orbcorp/orb-node/commit/bab2d25a0e37deb6058ed9890560f5bcc6dc53cb))
* **api:** updates ([#141](https://github.com/orbcorp/orb-node/issues/141)) ([c14e608](https://github.com/orbcorp/orb-node/commit/c14e60853f9ec7a3954c83ee8bb9275ee97e5797))
* **client:** add webhook secret argument ([#143](https://github.com/orbcorp/orb-node/issues/143)) ([46c215a](https://github.com/orbcorp/orb-node/commit/46c215aac68387a5c43c2bc0fb620486369dcbf5))


### Chores

* **deps:** bump yarn to v1.22.22 ([#151](https://github.com/orbcorp/orb-node/issues/151)) ([4ab1e0e](https://github.com/orbcorp/orb-node/commit/4ab1e0e4e2b5e9e62c2d27c99aedc35bc8e3c00f))
* **deps:** remove unused dependency digest-fetch ([#149](https://github.com/orbcorp/orb-node/issues/149)) ([567dbb4](https://github.com/orbcorp/orb-node/commit/567dbb4edd942bb04eb952c8dd19e8a8af8ff24e))
* **docs:** revise currency description ([#152](https://github.com/orbcorp/orb-node/issues/152)) ([1508146](https://github.com/orbcorp/orb-node/commit/15081469303ea1b6327b2c5b3ea38650c76b8ee8))
* **internal:** bump dependencies ([#145](https://github.com/orbcorp/orb-node/issues/145)) ([94acc13](https://github.com/orbcorp/orb-node/commit/94acc135e32597d32cce614aa4c25758600838ca))

## 2.1.2 (2024-03-25)

Full Changelog: [v2.1.1...v2.1.2](https://github.com/orbcorp/orb-node/compare/v2.1.1...v2.1.2)
Expand Down
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,35 @@ while (page.hasNextPage()) {
}
```

## Webhook Verification

We provide helper methods for verifying that a webhook request came from Orb, and not a malicious third party.

You can use `orb.webhooks.verifySignature(body: string, headers, secret?) -> void` or `orb.webhooks.unwrap(body: string, headers, secret?) -> Payload`,
both of which will raise an error if the signature is invalid.

Note that the "body" parameter must be the raw JSON string sent from the server (do not parse and re-stringify it).
The `.unwrap()` method will automatically parse this JSON for you into a typed `Payload`.

For example:

```ts
// with Express:
app.use('/webhooks/orb', bodyParser.text({ type: '*/*' }), function (req, res) {
const payload = orb.webhooks.unwrap(req.body, req.headers, process.env['ORB_WEBHOOK_SECRET']); // env var or client arg used by default; explicit here.
console.log(payload);
res.json({ ok: true });
});

// with Next.js (app router):
export default async function POST(req) {
const body = await req.text(); // if you're using the pages router, you will need this trick: https://vancelucas.com/blog/how-to-access-raw-body-data-with-next-js/
const payload = orb.webhooks.unwrap(body, req.headers, process.env['ORB_WEBHOOK_SECRET']); // env var or client arg used by default; explicit here.
console.log(payload);
return NextResponse.json({ ok: true });
}
```

## Advanced Usage

### Accessing raw Response data (e.g., headers)
Expand Down Expand Up @@ -208,7 +237,7 @@ await client.post('/some/path', {
});
```

#### Undocumented request params
#### Undocumented params

To make requests using undocumented parameters, you may use `// @ts-expect-error` on the undocumented
parameter. This library doesn't validate at runtime that the request matches the type, so any extra values you
Expand All @@ -229,7 +258,7 @@ extra param in the body.
If you want to explicitly send an extra argument, you can do so with the `query`, `body`, and `headers` request
options.

#### Undocumented response properties
#### Undocumented properties

To access undocumented response properties, you may access the response object with `// @ts-expect-error` on
the response object, or cast the response object to the requisite type. Like the request params, we do not
Expand Down
2 changes: 1 addition & 1 deletion api.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,4 @@ Methods:
- <code title="post /subscriptions/{subscription_id}/unschedule_cancellation">client.subscriptions.<a href="./src/resources/subscriptions.ts">unscheduleCancellation</a>(subscriptionId) -> Subscription</code>
- <code title="post /subscriptions/{subscription_id}/unschedule_fixed_fee_quantity_updates">client.subscriptions.<a href="./src/resources/subscriptions.ts">unscheduleFixedFeeQuantityUpdates</a>(subscriptionId, { ...params }) -> Subscription</code>
- <code title="post /subscriptions/{subscription_id}/unschedule_pending_plan_changes">client.subscriptions.<a href="./src/resources/subscriptions.ts">unschedulePendingPlanChanges</a>(subscriptionId) -> Subscription</code>
- <code title="post /subscriptions/{subscription_id}/update_fixed_fee_quantity">client.subscriptions.<a href="./src/resources/subscriptions.ts">updateFixedFeeQuantity</a>(subscriptionId, { ...params }) -> Subscription</code>
- <code title="post /subscriptions/{subscription_id}/update_fixed_fee_quantity">client.subscriptions.<a href="./src/resources/subscriptions.ts">updateFixedFeeQuantity</a>(subscriptionId, { ...params }) -> Subscription</code>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "orb-billing",
"version": "2.1.2",
"version": "2.4.0",
"description": "The official TypeScript library for the Orb API",
"author": "Orb <[email protected]>",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export class Orb extends Core.APIClient {
plans: API.Plans = new API.Plans(this);
prices: API.Prices = new API.Prices(this);
subscriptions: API.Subscriptions = new API.Subscriptions(this);
webhooks: API.Webhooks = new API.Webhooks(this);

protected override defaultQuery(): Core.DefaultQuery | undefined {
return this._options.defaultQuery;
Expand Down
1 change: 1 addition & 0 deletions src/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ export {
SubscriptionFetchScheduleResponsesPage,
} from './subscriptions';
export { TopLevelPingResponse, TopLevel } from './top-level';
export { Webhooks } from './webhooks';
142 changes: 142 additions & 0 deletions src/resources/webhooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// File generated from our OpenAPI spec by Stainless.

import { APIResource } from 'orb-billing/resource';
import { createHmac } from 'crypto';
import { debug, getRequiredHeader, HeadersLike } from 'orb-billing/core';

export class Webhooks extends APIResource {
/**
* Validates that the given payload was sent by Orb and parses the payload.
*
* An error will be raised if the webhook payload was not sent by Orb.
*/
unwrap(
payload: string,
headers: HeadersLike,
secret: string | undefined | null = this._client.webhookSecret,
): Object {
this.verifySignature(payload, headers, secret);
return JSON.parse(payload);
}

private parseSecret(secret: string | null | undefined): Uint8Array {
if (!secret) {
throw new Error(
"The webhook secret must either be set using the env var, ORB_WEBHOOK_SECRET, on the client class, Orb({ webhookSecret: '123' }), or passed to this function",
);
}

const buf = Buffer.from(secret, 'utf-8');
if (buf.toString('utf-8') !== secret) {
throw new Error(`Given secret is not valid`);
}

return new Uint8Array(buf);
}

private signPayload(payload: string, { timestamp, secret }: { timestamp: string; secret: Uint8Array }) {
const encoder = new TextEncoder();
const toSign = encoder.encode(`v1:${timestamp}:${payload}`);

const hmac = createHmac('sha256', secret);
hmac.update(toSign);

return `v1=${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 Orb.
*
* An error will be raised if the webhook payload was not sent by Orb.
*/
verifySignature(
body: string,
headers: HeadersLike,
secret: string | undefined | null = this._client.webhookSecret,
): void {
const whsecret = this.parseSecret(secret);

const msgTimestamp = getRequiredHeader(headers, 'X-Orb-Timestamp');
const msgSignature = getRequiredHeader(headers, 'X-Orb-Signature');

const nowSeconds = Math.floor(Date.now() / 1000);
// The timestamp header does not include a timezone (it is UTC by default)
const timezoneSuffix = msgTimestamp.includes('Z') || msgTimestamp.includes('+') ? '' : 'Z'
const timestamp = new Date(msgTimestamp + timezoneSuffix);
const timestampSeconds = Math.floor(timestamp.getTime() / 1000);
if (isNaN(timestampSeconds)) {
throw new Error('Invalid timestamp header');
}

const webhookToleranceInSeconds = 5 * 60; // 5 minutes
if (nowSeconds - timestampSeconds > webhookToleranceInSeconds) {
throw new Error('Webhook timestamp is too old');
}

if (timestampSeconds > nowSeconds + webhookToleranceInSeconds) {
console.warn({ timestampSeconds, nowSeconds, webhookToleranceInSeconds });
throw new Error('Webhook timestamp is too new');
}

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 computedSignature = this.signPayload(body, { timestamp: msgTimestamp, secret: whsecret });
const expectedSignature = computedSignature.split('=')[1];

const passedSignatures = msgSignature.split(' ');

const encoder = new globalThis.TextEncoder();
for (const versionedSignature of passedSignatures) {
const [version, signature] = versionedSignature.split('=');
debug('verifySignature', { version, signature, expectedSignature, computedSignature });

if (version !== 'v1') {
continue;
}

if (this.timingSafeEqual(encoder.encode(signature), encoder.encode(expectedSignature))) {
// valid!
return;
}
}

throw new Error('None of the given webhook signatures match the expected signature');
}
}
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '2.1.2'; // x-release-please-version
export const VERSION = '2.4.0'; // x-release-please-version
Loading

0 comments on commit 575226d

Please sign in to comment.