@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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/client.d.ts +50 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +242 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +55 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +53 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +100 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/openapi.d.ts +7963 -0
- package/dist/idempotency.d.ts +2 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +4 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/sleep.d.ts +10 -0
- package/dist/internal/sleep.d.ts.map +1 -0
- package/dist/internal/sleep.js +37 -0
- package/dist/internal/sleep.js.map +1 -0
- package/dist/pagination.d.ts +10 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +16 -0
- package/dist/pagination.js.map +1 -0
- package/dist/resources/attachments.d.ts +45 -0
- package/dist/resources/attachments.d.ts.map +1 -0
- package/dist/resources/attachments.js +120 -0
- package/dist/resources/attachments.js.map +1 -0
- package/dist/resources/base.d.ts +6 -0
- package/dist/resources/base.d.ts.map +1 -0
- package/dist/resources/base.js +7 -0
- package/dist/resources/base.js.map +1 -0
- package/dist/resources/byod.d.ts +15 -0
- package/dist/resources/byod.d.ts.map +1 -0
- package/dist/resources/byod.js +13 -0
- package/dist/resources/byod.js.map +1 -0
- package/dist/resources/chats.d.ts +29 -0
- package/dist/resources/chats.d.ts.map +1 -0
- package/dist/resources/chats.js +28 -0
- package/dist/resources/chats.js.map +1 -0
- package/dist/resources/lines.d.ts +70 -0
- package/dist/resources/lines.d.ts.map +1 -0
- package/dist/resources/lines.js +86 -0
- package/dist/resources/lines.js.map +1 -0
- package/dist/resources/lookup.d.ts +15 -0
- package/dist/resources/lookup.d.ts.map +1 -0
- package/dist/resources/lookup.js +13 -0
- package/dist/resources/lookup.js.map +1 -0
- package/dist/resources/messages.d.ts +42 -0
- package/dist/resources/messages.d.ts.map +1 -0
- package/dist/resources/messages.js +41 -0
- package/dist/resources/messages.js.map +1 -0
- package/dist/resources/trial.d.ts +28 -0
- package/dist/resources/trial.d.ts.map +1 -0
- package/dist/resources/trial.js +31 -0
- package/dist/resources/trial.js.map +1 -0
- package/dist/resources/webhooks.d.ts +58 -0
- package/dist/resources/webhooks.d.ts.map +1 -0
- package/dist/resources/webhooks.js +65 -0
- package/dist/resources/webhooks.js.map +1 -0
- package/dist/retry.d.ts +14 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +47 -0
- package/dist/retry.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/webhooks/parse.d.ts +15 -0
- package/dist/webhooks/parse.d.ts.map +1 -0
- package/dist/webhooks/parse.js +47 -0
- package/dist/webhooks/parse.js.map +1 -0
- package/dist/webhooks/types.d.ts +80 -0
- package/dist/webhooks/types.d.ts.map +1 -0
- package/dist/webhooks/types.js +17 -0
- package/dist/webhooks/types.js.map +1 -0
- package/dist/webhooks/verify.d.ts +41 -0
- package/dist/webhooks/verify.d.ts.map +1 -0
- package/dist/webhooks/verify.js +110 -0
- package/dist/webhooks/verify.js.map +1 -0
- 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":"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"}
|
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
|
+
}
|