@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.
Files changed (54) hide show
  1. package/README.md +87 -26
  2. package/dist/account.d.ts +30 -0
  3. package/dist/account.d.ts.map +1 -0
  4. package/dist/account.js +25 -0
  5. package/dist/account.js.map +1 -0
  6. package/dist/client.d.ts +9 -16
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +5 -30
  9. package/dist/client.js.map +1 -1
  10. package/dist/domains.d.ts +1 -1
  11. package/dist/domains.d.ts.map +1 -1
  12. package/dist/domains.js +1 -1
  13. package/dist/domains.js.map +1 -1
  14. package/dist/error.d.ts +5 -2
  15. package/dist/error.d.ts.map +1 -1
  16. package/dist/error.js +13 -0
  17. package/dist/error.js.map +1 -1
  18. package/dist/index.d.ts +6 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/options.d.ts +20 -0
  23. package/dist/options.d.ts.map +1 -0
  24. package/dist/options.js +34 -0
  25. package/dist/options.js.map +1 -0
  26. package/dist/servers.d.ts +64 -0
  27. package/dist/servers.d.ts.map +1 -0
  28. package/dist/servers.js +133 -0
  29. package/dist/servers.js.map +1 -0
  30. package/dist/types.d.ts +40 -3
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/dist/webhook-verify.d.ts +7 -5
  35. package/dist/webhook-verify.d.ts.map +1 -1
  36. package/dist/webhook-verify.js +38 -17
  37. package/dist/webhook-verify.js.map +1 -1
  38. package/dist/webhooks.d.ts +13 -1
  39. package/dist/webhooks.d.ts.map +1 -1
  40. package/dist/webhooks.js +26 -0
  41. package/dist/webhooks.js.map +1 -1
  42. package/package.json +4 -4
  43. package/skills/sendora/SKILL.md +19 -9
  44. package/src/account.ts +40 -0
  45. package/src/client.ts +14 -43
  46. package/src/domains.ts +1 -1
  47. package/src/error.ts +23 -1
  48. package/src/index.ts +6 -1
  49. package/src/options.ts +51 -0
  50. package/src/servers.ts +166 -0
  51. package/src/types.ts +47 -3
  52. package/src/version.ts +1 -1
  53. package/src/webhook-verify.ts +39 -18
  54. package/src/webhooks.ts +29 -0
@@ -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
- * the hex an HMAC-SHA256 with your secret over `<t>.<raw body>`; the
38
- * timestamp is inside the signed text, so a captured request cannot be
39
- * replayed after the tolerance. Throws `SendoraError` with
40
- * `invalid_signature` or `stale_signature`.
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
- if (!(await crypto.subtle.verify('HMAC', key, signature.mac, signed))) {
76
- throw invalidSignature('The signature does not match the body.');
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
- mac: Uint8Array<ArrayBuffer>;
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
- const parts = new Map<string, string>();
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 > 0) {
92
- parts.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
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
- const timestamp = Number(parts.get('t'));
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
- const mac = new Uint8Array(hex.length / 2);
101
- for (let i = 0; i < mac.length; i += 1) {
102
- mac[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
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 { timestamp, mac };
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`,