@isnap/sdk 0.0.0-next.3

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 (93) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/dist/client.d.ts +50 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +242 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/config.d.ts +43 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +55 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/errors.d.ts +53 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +100 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/generated/openapi.d.ts +7963 -0
  17. package/dist/idempotency.d.ts +2 -0
  18. package/dist/idempotency.d.ts.map +1 -0
  19. package/dist/idempotency.js +4 -0
  20. package/dist/idempotency.js.map +1 -0
  21. package/dist/index.d.ts +28 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +14 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/internal/sleep.d.ts +10 -0
  26. package/dist/internal/sleep.d.ts.map +1 -0
  27. package/dist/internal/sleep.js +37 -0
  28. package/dist/internal/sleep.js.map +1 -0
  29. package/dist/pagination.d.ts +10 -0
  30. package/dist/pagination.d.ts.map +1 -0
  31. package/dist/pagination.js +16 -0
  32. package/dist/pagination.js.map +1 -0
  33. package/dist/resources/attachments.d.ts +45 -0
  34. package/dist/resources/attachments.d.ts.map +1 -0
  35. package/dist/resources/attachments.js +120 -0
  36. package/dist/resources/attachments.js.map +1 -0
  37. package/dist/resources/base.d.ts +6 -0
  38. package/dist/resources/base.d.ts.map +1 -0
  39. package/dist/resources/base.js +7 -0
  40. package/dist/resources/base.js.map +1 -0
  41. package/dist/resources/byod.d.ts +15 -0
  42. package/dist/resources/byod.d.ts.map +1 -0
  43. package/dist/resources/byod.js +13 -0
  44. package/dist/resources/byod.js.map +1 -0
  45. package/dist/resources/chats.d.ts +29 -0
  46. package/dist/resources/chats.d.ts.map +1 -0
  47. package/dist/resources/chats.js +28 -0
  48. package/dist/resources/chats.js.map +1 -0
  49. package/dist/resources/lines.d.ts +70 -0
  50. package/dist/resources/lines.d.ts.map +1 -0
  51. package/dist/resources/lines.js +86 -0
  52. package/dist/resources/lines.js.map +1 -0
  53. package/dist/resources/lookup.d.ts +15 -0
  54. package/dist/resources/lookup.d.ts.map +1 -0
  55. package/dist/resources/lookup.js +13 -0
  56. package/dist/resources/lookup.js.map +1 -0
  57. package/dist/resources/messages.d.ts +42 -0
  58. package/dist/resources/messages.d.ts.map +1 -0
  59. package/dist/resources/messages.js +41 -0
  60. package/dist/resources/messages.js.map +1 -0
  61. package/dist/resources/trial.d.ts +28 -0
  62. package/dist/resources/trial.d.ts.map +1 -0
  63. package/dist/resources/trial.js +31 -0
  64. package/dist/resources/trial.js.map +1 -0
  65. package/dist/resources/webhooks.d.ts +58 -0
  66. package/dist/resources/webhooks.d.ts.map +1 -0
  67. package/dist/resources/webhooks.js +65 -0
  68. package/dist/resources/webhooks.js.map +1 -0
  69. package/dist/retry.d.ts +14 -0
  70. package/dist/retry.d.ts.map +1 -0
  71. package/dist/retry.js +47 -0
  72. package/dist/retry.js.map +1 -0
  73. package/dist/types.d.ts +33 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/types.js +2 -0
  76. package/dist/types.js.map +1 -0
  77. package/dist/version.d.ts +2 -0
  78. package/dist/version.d.ts.map +1 -0
  79. package/dist/version.js +4 -0
  80. package/dist/version.js.map +1 -0
  81. package/dist/webhooks/parse.d.ts +15 -0
  82. package/dist/webhooks/parse.d.ts.map +1 -0
  83. package/dist/webhooks/parse.js +47 -0
  84. package/dist/webhooks/parse.js.map +1 -0
  85. package/dist/webhooks/types.d.ts +80 -0
  86. package/dist/webhooks/types.d.ts.map +1 -0
  87. package/dist/webhooks/types.js +17 -0
  88. package/dist/webhooks/types.js.map +1 -0
  89. package/dist/webhooks/verify.d.ts +41 -0
  90. package/dist/webhooks/verify.d.ts.map +1 -0
  91. package/dist/webhooks/verify.js +110 -0
  92. package/dist/webhooks/verify.js.map +1 -0
  93. package/package.json +65 -0
@@ -0,0 +1,47 @@
1
+ import { WebhookSignatureError } from '../errors.js';
2
+ /**
3
+ * Parse a raw webhook delivery body into a typed envelope. Throws
4
+ * `WebhookSignatureError` if the body is not valid JSON or doesn't carry
5
+ * the §4.1 envelope shape (`event_id`, `event_type`, `data`). Use
6
+ * `verifyWebhook` for the full HMAC + replay-window verification — this
7
+ * is the parsing-only half for cases where verification happened
8
+ * out-of-band (e.g. behind a trusted gateway).
9
+ *
10
+ * **Consumers MUST dedupe by `event.event_id`** — webhook delivery is
11
+ * at-least-once, so retries replay the same envelope. Without dedup,
12
+ * processing the same event twice produces duplicate side-effects.
13
+ */
14
+ export function parseWebhookEvent(rawBody) {
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(rawBody);
18
+ }
19
+ catch (err) {
20
+ throw new WebhookSignatureError(`Webhook body is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
21
+ }
22
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
23
+ throw new WebhookSignatureError('Webhook body must be a JSON object');
24
+ }
25
+ const obj = parsed;
26
+ if (typeof obj.api_version !== 'string' || obj.api_version.length === 0) {
27
+ throw new WebhookSignatureError('Webhook envelope missing api_version');
28
+ }
29
+ if (typeof obj.event_id !== 'string' || obj.event_id.length === 0) {
30
+ throw new WebhookSignatureError('Webhook envelope missing event_id');
31
+ }
32
+ if (typeof obj.event_type !== 'string' || obj.event_type.length === 0) {
33
+ throw new WebhookSignatureError('Webhook envelope missing event_type');
34
+ }
35
+ if (typeof obj.created_at !== 'string' || obj.created_at.length === 0) {
36
+ throw new WebhookSignatureError('Webhook envelope missing created_at');
37
+ }
38
+ // `data` must be a plain object — arrays satisfy `typeof === 'object'`
39
+ // but the contract §4.1 envelope is `{ ...fields, data: { message: ... } }`,
40
+ // never an array. Rejecting at the boundary keeps the
41
+ // `Record<string, unknown>` declared type aligned with runtime.
42
+ if (typeof obj.data !== 'object' || obj.data === null || Array.isArray(obj.data)) {
43
+ throw new WebhookSignatureError('Webhook envelope missing data');
44
+ }
45
+ return obj;
46
+ }
47
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/webhooks/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAGpD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,qBAAqB,CAC7B,mCAAmC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACtF,CAAA;IACH,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,qBAAqB,CAAC,oCAAoC,CAAC,CAAA;IACvE,CAAC;IACD,MAAM,GAAG,GAAG,MAAiC,CAAA;IAC7C,IAAI,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,qBAAqB,CAAC,sCAAsC,CAAC,CAAA;IACzE,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,qBAAqB,CAAC,mCAAmC,CAAC,CAAA;IACtE,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,qBAAqB,CAAC,qCAAqC,CAAC,CAAA;IACxE,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,qBAAqB,CAAC,qCAAqC,CAAC,CAAA;IACxE,CAAC;IACD,uEAAuE;IACvE,6EAA6E;IAC7E,sDAAsD;IACtD,gEAAgE;IAChE,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACjF,MAAM,IAAI,qBAAqB,CAAC,+BAA+B,CAAC,CAAA;IAClE,CAAC;IACD,OAAO,GAAkC,CAAA;AAC3C,CAAC"}
@@ -0,0 +1,80 @@
1
+ import type { Message, Reaction } from '../resources/messages.js';
2
+ /**
3
+ * Common envelope for every webhook event. Per contract §4.1: every
4
+ * delivery body is shaped `{ api_version, event_id, event_type,
5
+ * created_at, trace_id?, data }`. `data` is a per-event-type payload —
6
+ * the type guards below narrow it to a typed shape.
7
+ */
8
+ export interface WebhookEventBase {
9
+ api_version: string;
10
+ event_id: string;
11
+ event_type: string;
12
+ created_at: string;
13
+ trace_id?: string;
14
+ data: Record<string, unknown>;
15
+ }
16
+ export type MessageEventType = 'message.queued' | 'message.scheduled' | 'message.sent' | 'message.delivered' | 'message.failed' | 'message.received' | 'message.read' | 'message.cancelled';
17
+ export interface MessageEvent extends WebhookEventBase {
18
+ event_type: `message.${string}`;
19
+ data: {
20
+ message: Message;
21
+ };
22
+ }
23
+ export type ReactionEventType = 'reaction.added' | 'reaction.removed';
24
+ export interface ReactionEvent extends WebhookEventBase {
25
+ event_type: `reaction.${string}`;
26
+ data: {
27
+ reaction: Reaction;
28
+ };
29
+ }
30
+ export type LineEventType = 'line.connected' | 'line.offline' | 'line.degraded' | 'line.disconnected' | 'line.activated' | 'line.suspended' | 'line.apple_id_flagged' | 'line.quota_warning' | 'line.quota_exceeded' | 'line.permanently_blocked';
31
+ /**
32
+ * `data` is intentionally typed as `Record<string, unknown>` until the
33
+ * contract pins per-event-type field shapes. Today's payloads vary by
34
+ * event (e.g. `line.apple_id_flagged` carries `flag_reason`,
35
+ * `line.quota_warning` carries `bucket` + `usage`), and surfacing a
36
+ * union with optional fields would let TypeScript autocomplete suggest
37
+ * keys that aren't present at runtime. Tighten when the contract is
38
+ * amended to specify the per-event shapes.
39
+ */
40
+ export interface LineEvent extends WebhookEventBase {
41
+ event_type: `line.${string}`;
42
+ data: Record<string, unknown>;
43
+ }
44
+ export type TypingEventType = 'typing.started' | 'typing.stopped';
45
+ /**
46
+ * `data` is intentionally `Record<string, unknown>` — the partner-facing
47
+ * shape is not yet documented in the contract. See `LineEvent` for the
48
+ * same rationale.
49
+ */
50
+ export interface TypingEvent extends WebhookEventBase {
51
+ event_type: `typing.${string}`;
52
+ data: Record<string, unknown>;
53
+ }
54
+ export type TrialEventType = 'trial.linked' | 'trial.dormant' | 'trial.reactivated' | 'trial.converted';
55
+ /** See `LineEvent` for why `data` is loose. */
56
+ export interface TrialEvent extends WebhookEventBase {
57
+ event_type: `trial.${string}`;
58
+ data: Record<string, unknown>;
59
+ }
60
+ /**
61
+ * Discriminated union over the typed event families plus `WebhookEventBase`
62
+ * as the catch-all for event types not yet known to this SDK version.
63
+ * Use a type guard (`isMessageEvent`, `isReactionEvent`, ...) to narrow
64
+ * inside an event handler:
65
+ *
66
+ * ```ts
67
+ * function handle(event: WebhookEvent) {
68
+ * if (isMessageEvent(event)) {
69
+ * console.log(event.data.message.id) // typed as Message
70
+ * }
71
+ * }
72
+ * ```
73
+ */
74
+ export type WebhookEvent = MessageEvent | ReactionEvent | LineEvent | TypingEvent | TrialEvent | WebhookEventBase;
75
+ export declare function isMessageEvent(event: WebhookEventBase): event is MessageEvent;
76
+ export declare function isReactionEvent(event: WebhookEventBase): event is ReactionEvent;
77
+ export declare function isLineEvent(event: WebhookEventBase): event is LineEvent;
78
+ export declare function isTypingEvent(event: WebhookEventBase): event is TypingEvent;
79
+ export declare function isTrialEvent(event: WebhookEventBase): event is TrialEvent;
80
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/webhooks/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAEjE;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAeD,MAAM,MAAM,gBAAgB,GACxB,gBAAgB,GAChB,mBAAmB,GACnB,cAAc,GACd,mBAAmB,GACnB,gBAAgB,GAChB,kBAAkB,GAClB,cAAc,GACd,mBAAmB,CAAA;AAEvB,MAAM,WAAW,YAAa,SAAQ,gBAAgB;IACpD,UAAU,EAAE,WAAW,MAAM,EAAE,CAAA;IAC/B,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAA;CAC3B;AAID,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,kBAAkB,CAAA;AAErE,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,UAAU,EAAE,YAAY,MAAM,EAAE,CAAA;IAChC,IAAI,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAA;KAAE,CAAA;CAC7B;AAID,MAAM,MAAM,aAAa,GACrB,gBAAgB,GAChB,cAAc,GACd,eAAe,GACf,mBAAmB,GACnB,gBAAgB,GAChB,gBAAgB,GAChB,uBAAuB,GACvB,oBAAoB,GACpB,qBAAqB,GACrB,0BAA0B,CAAA;AAE9B;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAU,SAAQ,gBAAgB;IACjD,UAAU,EAAE,QAAQ,MAAM,EAAE,CAAA;IAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAID,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,gBAAgB,CAAA;AAEjE;;;;GAIG;AACH,MAAM,WAAW,WAAY,SAAQ,gBAAgB;IACnD,UAAU,EAAE,UAAU,MAAM,EAAE,CAAA;IAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAID,MAAM,MAAM,cAAc,GACtB,cAAc,GACd,eAAe,GACf,mBAAmB,GACnB,iBAAiB,CAAA;AAErB,+CAA+C;AAC/C,MAAM,WAAW,UAAW,SAAQ,gBAAgB;IAClD,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAID;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,GACpB,YAAY,GACZ,aAAa,GACb,SAAS,GACT,WAAW,GACX,UAAU,GACV,gBAAgB,CAAA;AAIpB,wBAAgB,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,KAAK,IAAI,YAAY,CAE7E;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,gBAAgB,GAAG,KAAK,IAAI,aAAa,CAE/E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,KAAK,IAAI,SAAS,CAEvE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,KAAK,IAAI,WAAW,CAE3E;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,gBAAgB,GAAG,KAAK,IAAI,UAAU,CAEzE"}
@@ -0,0 +1,17 @@
1
+ /* ─────────────────────────── Type guards ─────────────────────────────── */
2
+ export function isMessageEvent(event) {
3
+ return event.event_type.startsWith('message.');
4
+ }
5
+ export function isReactionEvent(event) {
6
+ return event.event_type.startsWith('reaction.');
7
+ }
8
+ export function isLineEvent(event) {
9
+ return event.event_type.startsWith('line.');
10
+ }
11
+ export function isTypingEvent(event) {
12
+ return event.event_type.startsWith('typing.');
13
+ }
14
+ export function isTrialEvent(event) {
15
+ return event.event_type.startsWith('trial.');
16
+ }
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/webhooks/types.ts"],"names":[],"mappings":"AAsIA,6EAA6E;AAE7E,MAAM,UAAU,cAAc,CAAC,KAAuB;IACpD,OAAO,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,CAAA;AAChD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAuB;IACrD,OAAO,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;AACjD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,OAAO,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAuB;IACnD,OAAO,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;AAC/C,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAuB;IAClD,OAAO,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;AAC9C,CAAC"}
@@ -0,0 +1,41 @@
1
+ import type { WebhookEventBase } from './types.js';
2
+ export interface VerifyWebhookOptions {
3
+ /**
4
+ * Maximum drift in seconds between the request timestamp and the
5
+ * verifier's clock. Defaults to 300 (5 minutes) per contract §4.4.
6
+ * Set higher only if your server is reverse-proxied through a queue
7
+ * that may stall messages.
8
+ */
9
+ tolerance?: number;
10
+ /**
11
+ * Override the "now" clock — exposed primarily so tests can pin a
12
+ * deterministic time. Returns unix seconds.
13
+ */
14
+ now?: () => number;
15
+ }
16
+ /**
17
+ * Verify an incoming webhook delivery using the iSnap signing scheme:
18
+ *
19
+ * - `X-iSnap-Signature` header carries the hex-encoded
20
+ * `HMAC-SHA256(secret, "<timestamp>.<rawBody>")`
21
+ * - `X-iSnap-Timestamp` header carries the unix-second issuance time
22
+ *
23
+ * Throws `WebhookSignatureError` on missing headers, malformed
24
+ * timestamp, expired timestamp (outside `tolerance`), or signature
25
+ * mismatch. Returns the parsed event envelope on success.
26
+ *
27
+ * `secret` may be a single secret string OR an array — pass `[newSecret,
28
+ * oldSecret]` during the 5-minute rotate-secret grace window so both
29
+ * still validate. The verifier tries each in order and returns on the
30
+ * first match.
31
+ *
32
+ * Headers may be a `Record<string, string>` (e.g. Express's `req.headers`)
33
+ * or a `Headers` instance — both are read case-insensitively.
34
+ *
35
+ * **Consumers MUST dedupe by `event.event_id`** — retries replay the same
36
+ * envelope (same `event_id`, same body, same signature, refreshed
37
+ * timestamp). Without consumer-side dedup, an at-least-once delivery
38
+ * model produces duplicate side-effects on the partner system.
39
+ */
40
+ export declare function verifyWebhook(rawBody: string, headers: Record<string, string | string[] | undefined> | Headers, secret: string | readonly string[], options?: VerifyWebhookOptions): WebhookEventBase;
41
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/webhooks/verify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAIlD,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,OAAO,EAChE,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAClC,OAAO,GAAE,oBAAyB,GACjC,gBAAgB,CA+DlB"}
@@ -0,0 +1,110 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { WebhookSignatureError } from '../errors.js';
3
+ import { parseWebhookEvent } from './parse.js';
4
+ const DEFAULT_TOLERANCE_SECONDS = 300;
5
+ /**
6
+ * Verify an incoming webhook delivery using the iSnap signing scheme:
7
+ *
8
+ * - `X-iSnap-Signature` header carries the hex-encoded
9
+ * `HMAC-SHA256(secret, "<timestamp>.<rawBody>")`
10
+ * - `X-iSnap-Timestamp` header carries the unix-second issuance time
11
+ *
12
+ * Throws `WebhookSignatureError` on missing headers, malformed
13
+ * timestamp, expired timestamp (outside `tolerance`), or signature
14
+ * mismatch. Returns the parsed event envelope on success.
15
+ *
16
+ * `secret` may be a single secret string OR an array — pass `[newSecret,
17
+ * oldSecret]` during the 5-minute rotate-secret grace window so both
18
+ * still validate. The verifier tries each in order and returns on the
19
+ * first match.
20
+ *
21
+ * Headers may be a `Record<string, string>` (e.g. Express's `req.headers`)
22
+ * or a `Headers` instance — both are read case-insensitively.
23
+ *
24
+ * **Consumers MUST dedupe by `event.event_id`** — retries replay the same
25
+ * envelope (same `event_id`, same body, same signature, refreshed
26
+ * timestamp). Without consumer-side dedup, an at-least-once delivery
27
+ * model produces duplicate side-effects on the partner system.
28
+ */
29
+ export function verifyWebhook(rawBody, headers, secret, options = {}) {
30
+ const sigHeader = getHeader(headers, 'x-isnap-signature');
31
+ if (!sigHeader) {
32
+ throw new WebhookSignatureError('Missing X-iSnap-Signature header');
33
+ }
34
+ const tsHeader = getHeader(headers, 'x-isnap-timestamp');
35
+ if (!tsHeader) {
36
+ throw new WebhookSignatureError('Missing X-iSnap-Timestamp header');
37
+ }
38
+ // Strict integer-only check — `Number.parseInt` would happily accept
39
+ // `"1714478400foo"` and silently truncate. The HMAC compare downstream
40
+ // catches the resulting payload mismatch, but rejecting earlier here
41
+ // surfaces a clearer error and avoids one unnecessary HMAC computation.
42
+ if (!/^\d+$/.test(tsHeader)) {
43
+ throw new WebhookSignatureError(`Malformed X-iSnap-Timestamp: ${tsHeader}`);
44
+ }
45
+ const timestamp = Number(tsHeader);
46
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
47
+ throw new WebhookSignatureError(`Malformed X-iSnap-Timestamp: ${tsHeader}`);
48
+ }
49
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
50
+ // Validate before use — `Math.abs(...) > NaN` is always false (so the
51
+ // replay-window check would silently pass), `Math.abs(...) > -1` is
52
+ // always true (every timestamp rejected), and `Infinity` makes every
53
+ // timestamp pass. Reject the bad inputs explicitly so misuse fails
54
+ // loudly at the verify call site instead of hiding a security gap.
55
+ if (!Number.isFinite(tolerance) || tolerance < 0) {
56
+ throw new WebhookSignatureError(`tolerance must be a finite non-negative number (got ${String(tolerance)})`);
57
+ }
58
+ const nowSec = (options.now ?? defaultNow)();
59
+ if (Math.abs(nowSec - timestamp) > tolerance) {
60
+ throw new WebhookSignatureError(`Webhook timestamp is outside the ${tolerance}s tolerance window (drift: ${nowSec - timestamp}s)`);
61
+ }
62
+ const candidates = typeof secret === 'string' ? [secret] : secret;
63
+ if (candidates.length === 0) {
64
+ throw new WebhookSignatureError('No webhook secret(s) provided');
65
+ }
66
+ const provided = decodeHex(sigHeader);
67
+ if (!provided) {
68
+ throw new WebhookSignatureError('X-iSnap-Signature is not valid hex');
69
+ }
70
+ const signedPayload = `${timestamp}.${rawBody}`;
71
+ // Multi-secret loop returns early on first match — this is a minor
72
+ // timing side-channel during the rotate-secret grace window (an
73
+ // attacker who controls request timing could distinguish "matched first
74
+ // candidate" from "matched second"). Practical exploit value is near
75
+ // zero: knowing which secret matched does not help forge signatures
76
+ // for either secret. Worth revisiting if real partners run rotation
77
+ // through hostile networks.
78
+ for (const candidate of candidates) {
79
+ const expected = createHmac('sha256', candidate).update(signedPayload).digest();
80
+ if (provided.length === expected.length && timingSafeEqual(provided, expected)) {
81
+ return parseWebhookEvent(rawBody);
82
+ }
83
+ }
84
+ throw new WebhookSignatureError('Webhook signature did not match any provided secret');
85
+ }
86
+ function getHeader(headers, name) {
87
+ if (typeof Headers !== 'undefined' && headers instanceof Headers) {
88
+ return headers.get(name);
89
+ }
90
+ const lower = name.toLowerCase();
91
+ for (const [key, value] of Object.entries(headers)) {
92
+ if (key.toLowerCase() !== lower)
93
+ continue;
94
+ if (Array.isArray(value))
95
+ return value[0] ?? null;
96
+ if (typeof value === 'string')
97
+ return value;
98
+ return null;
99
+ }
100
+ return null;
101
+ }
102
+ function decodeHex(hex) {
103
+ if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length % 2 !== 0)
104
+ return null;
105
+ return Buffer.from(hex, 'hex');
106
+ }
107
+ function defaultNow() {
108
+ return Math.floor(Date.now() / 1000);
109
+ }
110
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/webhooks/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAG9C,MAAM,yBAAyB,GAAG,GAAG,CAAA;AAiBrC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAe,EACf,OAAgE,EAChE,MAAkC,EAClC,UAAgC,EAAE;IAElC,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAA;IACzD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,qBAAqB,CAAC,kCAAkC,CAAC,CAAA;IACrE,CAAC;IACD,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAA;IACxD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,qBAAqB,CAAC,kCAAkC,CAAC,CAAA;IACrE,CAAC;IACD,qEAAqE;IACrE,uEAAuE;IACvE,qEAAqE;IACrE,wEAAwE;IACxE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,qBAAqB,CAAC,gCAAgC,QAAQ,EAAE,CAAC,CAAA;IAC7E,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAA;IAClC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,qBAAqB,CAAC,gCAAgC,QAAQ,EAAE,CAAC,CAAA;IAC7E,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,yBAAyB,CAAA;IAChE,sEAAsE;IACtE,oEAAoE;IACpE,qEAAqE;IACrE,mEAAmE;IACnE,mEAAmE;IACnE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,qBAAqB,CAC7B,uDAAuD,MAAM,CAAC,SAAS,CAAC,GAAG,CAC5E,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,UAAU,CAAC,EAAE,CAAA;IAC5C,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,EAAE,CAAC;QAC7C,MAAM,IAAI,qBAAqB,CAC7B,oCAAoC,SAAS,8BAA8B,MAAM,GAAG,SAAS,IAAI,CAClG,CAAA;IACH,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IACjE,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,qBAAqB,CAAC,+BAA+B,CAAC,CAAA;IAClE,CAAC;IAED,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,CAAA;IACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,qBAAqB,CAAC,oCAAoC,CAAC,CAAA;IACvE,CAAC;IACD,MAAM,aAAa,GAAG,GAAG,SAAS,IAAI,OAAO,EAAE,CAAA;IAC/C,mEAAmE;IACnE,gEAAgE;IAChE,wEAAwE;IACxE,qEAAqE;IACrE,oEAAoE;IACpE,oEAAoE;IACpE,4BAA4B;IAC5B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAM,EAAE,CAAA;QAC/E,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC/E,OAAO,iBAAiB,CAAC,OAAO,CAAC,CAAA;QACnC,CAAC;IACH,CAAC;IACD,MAAM,IAAI,qBAAqB,CAAC,qDAAqD,CAAC,CAAA;AACxF,CAAC;AAED,SAAS,SAAS,CAChB,OAAgE,EAChE,IAAY;IAEZ,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,YAAY,OAAO,EAAE,CAAC;QACjE,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;IAChC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,KAAK;YAAE,SAAQ;QACzC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;QACjD,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAA;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,SAAS,CAAC,GAAW;IAC5B,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACpE,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;AAChC,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;AACtC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@isnap/sdk",
3
+ "version": "0.0.0-next.3",
4
+ "description": "Official TypeScript SDK for the iSnap P2P telephony API",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "LICENSE"
20
+ ],
21
+ "keywords": [
22
+ "isnap",
23
+ "imessage",
24
+ "sms",
25
+ "rcs",
26
+ "p2p",
27
+ "telephony",
28
+ "messaging",
29
+ "sdk",
30
+ "typescript"
31
+ ],
32
+ "homepage": "https://github.com/WhatSnap/iSnap-App/tree/main/packages/sdk#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/WhatSnap/iSnap-App/issues"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/WhatSnap/iSnap-App.git",
39
+ "directory": "packages/sdk"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.build.json && bun scripts/copy-generated.ts",
43
+ "lint": "eslint src/",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "generate": "openapi-typescript ../backend/openapi.json -o src/generated/openapi.d.ts",
48
+ "prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "devDependencies": {
57
+ "@eslint/js": "^10.0.1",
58
+ "@types/node": "^22.0.0",
59
+ "eslint": "^9.0.0",
60
+ "openapi-typescript": "^7.13.0",
61
+ "typescript": "^5.7.0",
62
+ "typescript-eslint": "^8.57.1",
63
+ "vitest": "^3.0.0"
64
+ }
65
+ }