@sendora/sdk 1.2.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -26
- package/dist/account.d.ts +30 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +25 -0
- package/dist/account.js.map +1 -0
- package/dist/client.d.ts +9 -16
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -30
- package/dist/client.js.map +1 -1
- package/dist/domains.d.ts +1 -1
- package/dist/domains.d.ts.map +1 -1
- package/dist/domains.js +1 -1
- package/dist/domains.js.map +1 -1
- package/dist/error.d.ts +5 -2
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +13 -0
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +20 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +34 -0
- package/dist/options.js.map +1 -0
- package/dist/servers.d.ts +64 -0
- package/dist/servers.d.ts.map +1 -0
- package/dist/servers.js +133 -0
- package/dist/servers.js.map +1 -0
- package/dist/types.d.ts +40 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhook-verify.d.ts +7 -5
- package/dist/webhook-verify.d.ts.map +1 -1
- package/dist/webhook-verify.js +38 -17
- package/dist/webhook-verify.js.map +1 -1
- package/dist/webhooks.d.ts +13 -1
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +26 -0
- package/dist/webhooks.js.map +1 -1
- package/package.json +4 -4
- package/skills/sendora/SKILL.md +19 -9
- package/src/account.ts +40 -0
- package/src/client.ts +14 -43
- package/src/domains.ts +1 -1
- package/src/error.ts +23 -1
- package/src/index.ts +6 -1
- package/src/options.ts +51 -0
- package/src/servers.ts +166 -0
- package/src/types.ts +47 -3
- package/src/version.ts +1 -1
- package/src/webhook-verify.ts +39 -18
- package/src/webhooks.ts +29 -0
package/src/webhook-verify.ts
CHANGED
|
@@ -33,11 +33,13 @@ const decoder = new TextDecoder();
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Checks a webhook request came from Sendora and answers its event, typed
|
|
36
|
-
* by `event`. The header is `Sendora-Signature: t=<unix seconds>,v1=<hex
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* by `event`. The header is `Sendora-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>]`,
|
|
37
|
+
* one hex per live secret of the webhook, each an HMAC-SHA256 with that
|
|
38
|
+
* secret over `<t>.<raw body>`; the one your secret produces is enough, so
|
|
39
|
+
* a secret being rolled in verifies as well as the old one. The timestamp
|
|
40
|
+
* is inside the signed text, so a captured request cannot be replayed
|
|
41
|
+
* after the tolerance. Throws `SendoraError` with `invalid_signature` or
|
|
42
|
+
* `stale_signature`.
|
|
41
43
|
*
|
|
42
44
|
* @example
|
|
43
45
|
* const event = await verifyWebhook({
|
|
@@ -72,36 +74,55 @@ export async function verifyWebhook(input: VerifyWebhookInput): Promise<WebhookE
|
|
|
72
74
|
false,
|
|
73
75
|
['verify'],
|
|
74
76
|
);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
// Every signature is checked, so the time taken says nothing about which one matched.
|
|
78
|
+
let matched = false;
|
|
79
|
+
for (const mac of signature.macs) {
|
|
80
|
+
matched = (await crypto.subtle.verify('HMAC', key, mac, signed)) || matched;
|
|
81
|
+
}
|
|
82
|
+
if (!matched) {
|
|
83
|
+
throw invalidSignature('No signature in the header matches the body under this secret.');
|
|
77
84
|
}
|
|
78
85
|
return parseEvent(decoder.decode(body));
|
|
79
86
|
}
|
|
80
87
|
|
|
88
|
+
/** The timestamp and every `v1` value, one per live secret of the webhook. */
|
|
81
89
|
function parseSignature(header: string | null | undefined): {
|
|
82
90
|
timestamp: number;
|
|
83
|
-
|
|
91
|
+
macs: Uint8Array<ArrayBuffer>[];
|
|
84
92
|
} {
|
|
85
93
|
if (typeof header !== 'string' || header === '') {
|
|
86
94
|
throw invalidSignature('The request carries no Sendora-Signature header.');
|
|
87
95
|
}
|
|
88
|
-
|
|
96
|
+
let timestamp: number | undefined;
|
|
97
|
+
const macs: Uint8Array<ArrayBuffer>[] = [];
|
|
89
98
|
for (const part of header.split(',')) {
|
|
90
99
|
const separator = part.indexOf('=');
|
|
91
|
-
if (separator
|
|
92
|
-
|
|
100
|
+
if (separator <= 0) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const name = part.slice(0, separator).trim();
|
|
104
|
+
const value = part.slice(separator + 1).trim();
|
|
105
|
+
if (name === 't') {
|
|
106
|
+
timestamp = Number(value);
|
|
107
|
+
} else if (name === 'v1') {
|
|
108
|
+
if (!MAC_HEX.test(value)) {
|
|
109
|
+
throw invalidSignature('The Sendora-Signature header is malformed.');
|
|
110
|
+
}
|
|
111
|
+
macs.push(bytesOf(value));
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
|
-
|
|
96
|
-
const hex = parts.get('v1');
|
|
97
|
-
if (!Number.isInteger(timestamp) || hex === undefined || !MAC_HEX.test(hex)) {
|
|
114
|
+
if (timestamp === undefined || !Number.isInteger(timestamp) || macs.length === 0) {
|
|
98
115
|
throw invalidSignature('The Sendora-Signature header is malformed.');
|
|
99
116
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
return { timestamp, macs };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function bytesOf(hex: string): Uint8Array<ArrayBuffer> {
|
|
121
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
122
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
123
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
103
124
|
}
|
|
104
|
-
return
|
|
125
|
+
return bytes;
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
function parseEvent(text: string): WebhookEvent {
|
package/src/webhooks.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { paginate } from './pagination.ts';
|
|
|
2
2
|
import type { Transport } from './transport.ts';
|
|
3
3
|
import type {
|
|
4
4
|
CreatedWebhook,
|
|
5
|
+
CreatedWebhookSecret,
|
|
5
6
|
CreateWebhookRequest,
|
|
6
7
|
Delivery,
|
|
7
8
|
DeliveryPage,
|
|
@@ -74,6 +75,34 @@ export class WebhooksResource {
|
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Adds a second live secret and answers its value once. Every delivery
|
|
80
|
+
* then carries one signature per live secret, the newest first, so
|
|
81
|
+
* switch your receiver to the new secret and delete the old one; a third
|
|
82
|
+
* is refused with `secret_limit`.
|
|
83
|
+
*
|
|
84
|
+
*
|
|
85
|
+
* const { secret, secretId } = await sendora.webhooks.createSecret(webhookId);
|
|
86
|
+
*/
|
|
87
|
+
createSecret(webhookId: string, options: RequestOptions = {}): Promise<CreatedWebhookSecret> {
|
|
88
|
+
return this.#transport.request<CreatedWebhookSecret>({
|
|
89
|
+
method: 'POST',
|
|
90
|
+
path: `/v1/webhooks/${encodeURIComponent(webhookId)}/secrets`,
|
|
91
|
+
idempotent: false,
|
|
92
|
+
signal: options.signal,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Deletes a secret; deliveries are no longer signed with it. The last live secret is refused (`last_secret`). */
|
|
97
|
+
deleteSecret(webhookId: string, secretId: string, options: RequestOptions = {}): Promise<void> {
|
|
98
|
+
return this.#transport.request<undefined>({
|
|
99
|
+
method: 'DELETE',
|
|
100
|
+
path: `/v1/webhooks/${encodeURIComponent(webhookId)}/secrets/${encodeURIComponent(secretId)}`,
|
|
101
|
+
idempotent: true,
|
|
102
|
+
signal: options.signal,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
77
106
|
/**
|
|
78
107
|
* One page of the events handed to the webhook, newest first, with the
|
|
79
108
|
* outcome of the last attempt; `status` narrows to `pending`,
|