whisp-js 0.0.0-stage → 0.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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +296 -2
  3. package/dist/cjs/client.d.ts +164 -0
  4. package/dist/cjs/client.js +460 -0
  5. package/dist/cjs/constants.d.ts +43 -0
  6. package/dist/cjs/constants.js +59 -0
  7. package/dist/cjs/errors.d.ts +96 -0
  8. package/dist/cjs/errors.js +115 -0
  9. package/dist/cjs/index.d.ts +19 -0
  10. package/dist/cjs/index.js +39 -0
  11. package/dist/cjs/listener.d.ts +93 -0
  12. package/dist/cjs/listener.js +468 -0
  13. package/dist/cjs/media.d.ts +40 -0
  14. package/dist/cjs/media.js +181 -0
  15. package/dist/cjs/package.json +3 -0
  16. package/dist/cjs/refs.d.ts +4 -0
  17. package/dist/cjs/refs.js +38 -0
  18. package/dist/cjs/runtime.d.ts +24 -0
  19. package/dist/cjs/runtime.js +93 -0
  20. package/dist/cjs/sse.d.ts +32 -0
  21. package/dist/cjs/sse.js +105 -0
  22. package/dist/cjs/transport.d.ts +61 -0
  23. package/dist/cjs/transport.js +181 -0
  24. package/dist/cjs/types.d.ts +516 -0
  25. package/dist/cjs/types.js +14 -0
  26. package/dist/cjs/version.d.ts +2 -0
  27. package/dist/cjs/version.js +5 -0
  28. package/dist/cjs/webhook.d.ts +27 -0
  29. package/dist/cjs/webhook.js +128 -0
  30. package/dist/esm/client.d.ts +164 -0
  31. package/dist/esm/client.js +456 -0
  32. package/dist/esm/constants.d.ts +43 -0
  33. package/dist/esm/constants.js +56 -0
  34. package/dist/esm/errors.d.ts +96 -0
  35. package/dist/esm/errors.js +107 -0
  36. package/dist/esm/index.d.ts +19 -0
  37. package/dist/esm/index.js +16 -0
  38. package/dist/esm/listener.d.ts +93 -0
  39. package/dist/esm/listener.js +463 -0
  40. package/dist/esm/media.d.ts +40 -0
  41. package/dist/esm/media.js +175 -0
  42. package/dist/esm/refs.d.ts +4 -0
  43. package/dist/esm/refs.js +33 -0
  44. package/dist/esm/runtime.d.ts +24 -0
  45. package/dist/esm/runtime.js +84 -0
  46. package/dist/esm/sse.d.ts +32 -0
  47. package/dist/esm/sse.js +100 -0
  48. package/dist/esm/transport.d.ts +61 -0
  49. package/dist/esm/transport.js +172 -0
  50. package/dist/esm/types.d.ts +516 -0
  51. package/dist/esm/types.js +13 -0
  52. package/dist/esm/version.d.ts +2 -0
  53. package/dist/esm/version.js +2 -0
  54. package/dist/esm/webhook.d.ts +27 -0
  55. package/dist/esm/webhook.js +123 -0
  56. package/package.json +37 -5
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * The SDK's errors (phase2-design 12.3, 12.5). Every one is a `WhispError`,
4
+ * so one `instanceof` check catches them all and `code` says what happened:
5
+ *
6
+ * - `WhispError`: the API answered with an error (`status` is the HTTP
7
+ * status, `code` the API's stable code), or a local check failed before
8
+ * anything was sent (`status` is null).
9
+ * - `WhispConnectionError`: no HTTP answer after every retry (network error
10
+ * or timeout).
11
+ * - `WhispSignatureError`: `verifyWebhook` refused a request.
12
+ *
13
+ * Messages are English and safe to log: they never contain the token or a
14
+ * webhook secret.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.localError = exports.readApiError = exports.WhispSignatureError = exports.WhispConnectionError = exports.WhispError = exports.SDK_ERROR_CODES = void 0;
18
+ /** Codes the SDK makes itself. Everything else comes from the API as it sent it. */
19
+ exports.SDK_ERROR_CODES = {
20
+ /** No `token` was passed and WHISP_API_TOKEN is not set. Nothing was sent. */
21
+ TOKEN_MISSING: 'TOKEN_MISSING',
22
+ /** The token isn't shaped like a project token (`wsp_live_…`). Nothing was sent. The API uses the same code. */
23
+ TOKEN_INVALID: 'TOKEN_INVALID',
24
+ /** The SDK was created in a browser without `dangerouslyAllowBrowser`. The API uses the same code. */
25
+ TOKEN_IN_BROWSER: 'TOKEN_IN_BROWSER',
26
+ /** A `phone` (or `number(...)`) that is neither a phone in international form nor a `num_` id. */
27
+ PHONE_INVALID: 'PHONE_INVALID',
28
+ /** A read that needs one number (`chats`, `chat`, `chatMessages`, `check`) on a pool or whole-project instance. */
29
+ NUMBER_REQUIRED: 'NUMBER_REQUIRED',
30
+ /** An option or argument the SDK can tell is wrong without asking the API (`baseUrl`, `timeoutMs`, ...). */
31
+ OPTIONS_INVALID: 'OPTIONS_INVALID',
32
+ /** A file to send that can't be uploaded as given (no bytes, no type, too big). Nothing was sent. */
33
+ MEDIA_INVALID: 'MEDIA_INVALID',
34
+ /** The presigned upload (the PUT to storage) failed. */
35
+ UPLOAD_FAILED: 'UPLOAD_FAILED',
36
+ /** The signed file link answered an error (`downloadMedia`). */
37
+ MEDIA_DOWNLOAD_FAILED: 'MEDIA_DOWNLOAD_FAILED',
38
+ /** A 2xx whose body wasn't what the API documents, or a redirect the SDK refused to follow. */
39
+ INVALID_RESPONSE: 'INVALID_RESPONSE',
40
+ /** No HTTP answer: DNS, connection refused or reset, TLS. */
41
+ NETWORK_ERROR: 'NETWORK_ERROR',
42
+ /** An attempt ran past `timeoutMs`, or the event stream sent nothing for 45 s. */
43
+ TIMEOUT: 'TIMEOUT',
44
+ /** `verifyWebhook` got no secret (an unset env variable, usually). */
45
+ SECRET_MISSING: 'SECRET_MISSING',
46
+ /** `verifyWebhook` got a secret that isn't a webhook signing secret (`wsp_whs_…`). */
47
+ SECRET_INVALID: 'SECRET_INVALID',
48
+ /** `verifyWebhook` refused the request; `reason` says why. */
49
+ SIGNATURE_INVALID: 'SIGNATURE_INVALID',
50
+ /** This runtime has no Web Crypto (`crypto.subtle`), which `verifyWebhook` needs. */
51
+ CRYPTO_UNAVAILABLE: 'CRYPTO_UNAVAILABLE',
52
+ };
53
+ class WhispError extends Error {
54
+ name = 'WhispError';
55
+ /** Stable: `NUMBER_NOT_IN_PROJECT`, `RATE_LIMITED`, `TOKEN_MISSING`, ... */
56
+ code;
57
+ /** The HTTP status when the API answered; null when nothing was answered. */
58
+ status;
59
+ /** The values the code names (`number`, `total`, `reason`, ...), or null. */
60
+ params;
61
+ requestId;
62
+ /** Seconds the API asked to wait (`Retry-After`), or null. */
63
+ retryAfter;
64
+ constructor(init) {
65
+ super(init.message, init.cause === undefined ? undefined : { cause: init.cause });
66
+ this.code = init.code;
67
+ this.status = init.status ?? null;
68
+ this.params = init.params ?? null;
69
+ this.requestId = init.requestId ?? null;
70
+ this.retryAfter = init.retryAfter ?? null;
71
+ }
72
+ }
73
+ exports.WhispError = WhispError;
74
+ /** No HTTP answer after every retry. `cause` is the runtime's own error. */
75
+ class WhispConnectionError extends WhispError {
76
+ name = 'WhispConnectionError';
77
+ constructor(code, message, cause) {
78
+ super({ code, message, cause });
79
+ }
80
+ }
81
+ exports.WhispConnectionError = WhispConnectionError;
82
+ const SIGNATURE_MESSAGES = {
83
+ missing: 'The request has no X-Whisp-Signature header',
84
+ malformed: 'The X-Whisp-Signature header or the body is not in the expected format',
85
+ timestamp: "The signature's timestamp is outside the tolerance (an old or replayed request, or a wrong clock)",
86
+ mismatch: 'No signature matches the body with this secret (a changed body, another secret, or a parsed body)',
87
+ };
88
+ class WhispSignatureError extends WhispError {
89
+ name = 'WhispSignatureError';
90
+ reason;
91
+ constructor(reason, message = SIGNATURE_MESSAGES[reason]) {
92
+ super({ code: exports.SDK_ERROR_CODES.SIGNATURE_INVALID, message, params: { reason } });
93
+ this.reason = reason;
94
+ }
95
+ }
96
+ exports.WhispSignatureError = WhispSignatureError;
97
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
98
+ /** `{ code, message, params? }` out of an API body (5.12), or null when it isn't one. */
99
+ const readApiError = (body) => {
100
+ if (!isRecord(body) || typeof body.code !== 'string' || typeof body.message !== 'string')
101
+ return null;
102
+ let params = null;
103
+ if (isRecord(body.params)) {
104
+ params = {};
105
+ for (const [key, value] of Object.entries(body.params)) {
106
+ if (typeof value === 'string' || typeof value === 'number')
107
+ params[key] = value;
108
+ }
109
+ }
110
+ return { code: body.code, message: body.message, params };
111
+ };
112
+ exports.readApiError = readApiError;
113
+ /** A local check failed; nothing was sent. */
114
+ const localError = (code, message, params) => new WhispError({ code, message, params: params ?? null });
115
+ exports.localError = localError;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The Whisp SDK (phase2-design 12): send WhatsApp messages from your
3
+ * project's numbers, read their chats, listen to their events, and check
4
+ * webhook signatures, from Node 22.12+ or Bun. Zero runtime dependencies:
5
+ * `fetch`, Web Streams and Web Crypto only.
6
+ *
7
+ * import { Whisp } from 'whisp-js'
8
+ * const whisp = new Whisp({ phone: '972501234567' }) // WHISP_API_TOKEN from the environment
9
+ * await whisp.send('972521112233', 'Hello from Whisp')
10
+ * whisp.on('message', msg => console.log(msg.from, msg.text))
11
+ */
12
+ export { DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, type DownloadOptions, type MessagesQuery, type Page, type PageResult, type SendFileOptions, type SendOptions, type VerifyResult, Whisp, type WhispOptions, } from './client.js';
13
+ export { API_TOKEN_ENV, API_URL_ENV, DEFAULT_BASE_URL, EVENT_CURSOR_PREFIX, PUBLIC_EVENT_TYPES, SDK_PACKAGE_NAME, SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, TOKEN_PREFIX, USER_AGENT_PREFIX, WEBHOOK_SECRET_PREFIX, } from './constants.js';
14
+ export { type ErrorParams, SDK_ERROR_CODES, type SdkErrorCode, type SignatureFailureReason, WhispConnectionError, WhispError, WhispSignatureError, } from './errors.js';
15
+ export type { AnyListenerEvent, EventHandlers, EventMeta, ListenerEvent, ListenerName, StreamClose, StreamGap, } from './listener.js';
16
+ export type { MediaFile, MediaSource, MediaUrl } from './media.js';
17
+ export type * from './types.js';
18
+ export { SDK_VERSION } from './version.js';
19
+ export { type VerifyWebhookInput, verifyWebhook } from './webhook.js';
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyWebhook = exports.SDK_VERSION = exports.WhispSignatureError = exports.WhispError = exports.WhispConnectionError = exports.SDK_ERROR_CODES = exports.WEBHOOK_SECRET_PREFIX = exports.USER_AGENT_PREFIX = exports.TOKEN_PREFIX = exports.SIGNATURE_TOLERANCE_SECONDS = exports.SIGNATURE_HEADER = exports.SDK_PACKAGE_NAME = exports.PUBLIC_EVENT_TYPES = exports.EVENT_CURSOR_PREFIX = exports.DEFAULT_BASE_URL = exports.API_URL_ENV = exports.API_TOKEN_ENV = exports.Whisp = exports.DEFAULT_TIMEOUT_MS = exports.DEFAULT_MAX_RETRIES = void 0;
4
+ /**
5
+ * The Whisp SDK (phase2-design 12): send WhatsApp messages from your
6
+ * project's numbers, read their chats, listen to their events, and check
7
+ * webhook signatures, from Node 22.12+ or Bun. Zero runtime dependencies:
8
+ * `fetch`, Web Streams and Web Crypto only.
9
+ *
10
+ * import { Whisp } from 'whisp-js'
11
+ * const whisp = new Whisp({ phone: '972501234567' }) // WHISP_API_TOKEN from the environment
12
+ * await whisp.send('972521112233', 'Hello from Whisp')
13
+ * whisp.on('message', msg => console.log(msg.from, msg.text))
14
+ */
15
+ var client_ts_1 = require("./client.js");
16
+ Object.defineProperty(exports, "DEFAULT_MAX_RETRIES", { enumerable: true, get: function () { return client_ts_1.DEFAULT_MAX_RETRIES; } });
17
+ Object.defineProperty(exports, "DEFAULT_TIMEOUT_MS", { enumerable: true, get: function () { return client_ts_1.DEFAULT_TIMEOUT_MS; } });
18
+ Object.defineProperty(exports, "Whisp", { enumerable: true, get: function () { return client_ts_1.Whisp; } });
19
+ var constants_ts_1 = require("./constants.js");
20
+ Object.defineProperty(exports, "API_TOKEN_ENV", { enumerable: true, get: function () { return constants_ts_1.API_TOKEN_ENV; } });
21
+ Object.defineProperty(exports, "API_URL_ENV", { enumerable: true, get: function () { return constants_ts_1.API_URL_ENV; } });
22
+ Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return constants_ts_1.DEFAULT_BASE_URL; } });
23
+ Object.defineProperty(exports, "EVENT_CURSOR_PREFIX", { enumerable: true, get: function () { return constants_ts_1.EVENT_CURSOR_PREFIX; } });
24
+ Object.defineProperty(exports, "PUBLIC_EVENT_TYPES", { enumerable: true, get: function () { return constants_ts_1.PUBLIC_EVENT_TYPES; } });
25
+ Object.defineProperty(exports, "SDK_PACKAGE_NAME", { enumerable: true, get: function () { return constants_ts_1.SDK_PACKAGE_NAME; } });
26
+ Object.defineProperty(exports, "SIGNATURE_HEADER", { enumerable: true, get: function () { return constants_ts_1.SIGNATURE_HEADER; } });
27
+ Object.defineProperty(exports, "SIGNATURE_TOLERANCE_SECONDS", { enumerable: true, get: function () { return constants_ts_1.SIGNATURE_TOLERANCE_SECONDS; } });
28
+ Object.defineProperty(exports, "TOKEN_PREFIX", { enumerable: true, get: function () { return constants_ts_1.TOKEN_PREFIX; } });
29
+ Object.defineProperty(exports, "USER_AGENT_PREFIX", { enumerable: true, get: function () { return constants_ts_1.USER_AGENT_PREFIX; } });
30
+ Object.defineProperty(exports, "WEBHOOK_SECRET_PREFIX", { enumerable: true, get: function () { return constants_ts_1.WEBHOOK_SECRET_PREFIX; } });
31
+ var errors_ts_1 = require("./errors.js");
32
+ Object.defineProperty(exports, "SDK_ERROR_CODES", { enumerable: true, get: function () { return errors_ts_1.SDK_ERROR_CODES; } });
33
+ Object.defineProperty(exports, "WhispConnectionError", { enumerable: true, get: function () { return errors_ts_1.WhispConnectionError; } });
34
+ Object.defineProperty(exports, "WhispError", { enumerable: true, get: function () { return errors_ts_1.WhispError; } });
35
+ Object.defineProperty(exports, "WhispSignatureError", { enumerable: true, get: function () { return errors_ts_1.WhispSignatureError; } });
36
+ var version_ts_1 = require("./version.js");
37
+ Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_ts_1.SDK_VERSION; } });
38
+ var webhook_ts_1 = require("./webhook.js");
39
+ Object.defineProperty(exports, "verifyWebhook", { enumerable: true, get: function () { return webhook_ts_1.verifyWebhook; } });
@@ -0,0 +1,93 @@
1
+ import { type Timers } from './runtime.js';
2
+ import { type FetchLike } from './transport.js';
3
+ import type { CursorRejection, EventData, PublicEvent, PublicEventType, StreamReady } from './types.js';
4
+ /** Wait before reconnect attempt N (0-based); the last value repeats. */
5
+ export declare const RECONNECT_DELAYS_MS: readonly [1000, 2000, 4000, 8000, 15000];
6
+ /** No byte for this long and the connection is dropped (the server pings every 20 s). */
7
+ export declare const WATCHDOG_MS = 45000;
8
+ /** The wait after a 429 (TOO_MANY_STREAMS or RATE_LIMITED), unless `Retry-After` asks for longer (up to a minute). */
9
+ export declare const RATE_LIMITED_WAIT_MS = 30000;
10
+ /** Where the event came from, for logging and dedupe: `id` is the dedupe key, `cursor` the resume point. */
11
+ export interface EventMeta<T extends PublicEventType = PublicEventType> {
12
+ /** The event id (`evt_…`): the same event can arrive twice across a restart, never with another id. */
13
+ id: string;
14
+ type: T;
15
+ /** When the change committed. */
16
+ createdAt: string;
17
+ /** Save it and pass it as `cursor` to resume after this event. */
18
+ cursor: string;
19
+ }
20
+ /**
21
+ * What a handler gets (12.4): the event's `data` with the envelope folded in,
22
+ * so `msg.from`, `msg.text` and `msg.number` read directly, and
23
+ * `e.number`, `e.status` for `number.status`. `number` is the Whisp number's
24
+ * phone, `numberId` its id.
25
+ */
26
+ export type ListenerEvent<T extends PublicEventType> = Omit<EventData[T], 'number' | 'numberId' | 'event'> & {
27
+ number: string;
28
+ numberId: string;
29
+ event: EventMeta<T>;
30
+ };
31
+ /** Any of the twelve; `event.event.type` says which. */
32
+ export type AnyListenerEvent = {
33
+ [T in PublicEventType]: ListenerEvent<T>;
34
+ }[PublicEventType];
35
+ /** `'gap'`: a resume was asked and the server couldn't honour it; reconcile with reads. */
36
+ export interface StreamGap {
37
+ reason: CursorRejection;
38
+ /** The cursor that was sent. */
39
+ cursor: string;
40
+ }
41
+ /** `'close'`: a connection ended. `reconnect` is false once the listener has stopped. */
42
+ export interface StreamClose {
43
+ reconnect: boolean;
44
+ /** Why it ended, when it wasn't `close()`. */
45
+ error?: Error;
46
+ }
47
+ export type Handler<A> = (arg: A) => unknown;
48
+ /** Every name `on()` takes and what its handler receives. */
49
+ export type EventHandlers = {
50
+ [T in PublicEventType]: Handler<ListenerEvent<T>>;
51
+ } & {
52
+ /** `message.received`. */
53
+ message: Handler<ListenerEvent<'message.received'>>;
54
+ /** Every one of the twelve types. */
55
+ '*': Handler<AnyListenerEvent>;
56
+ /** A connection reached `ready`. */
57
+ open: Handler<StreamReady>;
58
+ close: Handler<StreamClose>;
59
+ gap: Handler<StreamGap>;
60
+ /** A handler threw (with the event it was given), the server sent `error`, or the listener stopped for good. */
61
+ error: (error: Error, event?: AnyListenerEvent) => unknown;
62
+ };
63
+ export type ListenerName = keyof EventHandlers;
64
+ export interface StreamTarget {
65
+ baseUrl: string;
66
+ token: string;
67
+ fetch: FetchLike;
68
+ userAgent: string;
69
+ }
70
+ export interface ListenerConfig {
71
+ /** The connection's target, resolved per connection; throws a WhispError (TOKEN_MISSING) to stop. */
72
+ resolve: () => StreamTarget;
73
+ /** The instance's numbers (`numbers=`), or null for the whole project. */
74
+ numbers: readonly string[] | null;
75
+ cursor: string | null;
76
+ /** How long to wait for the answer's headers. */
77
+ connectTimeoutMs: number;
78
+ timers: Timers;
79
+ /** Where an error goes when nobody listens to `'error'`. */
80
+ report: (error: Error) => void;
81
+ }
82
+ /** The handler's argument: `data` plus the envelope (12.4). */
83
+ export declare const toListenerEvent: (envelope: PublicEvent, cursor: string) => AnyListenerEvent;
84
+ export declare class Listener {
85
+ #private;
86
+ constructor(config: ListenerConfig);
87
+ get cursor(): string | null;
88
+ get running(): boolean;
89
+ add(name: string, handler: Handler<never>): void;
90
+ remove(name: string, handler: Handler<never>): void;
91
+ /** Ends the stream and its reconnects, and forgets every handler; a later `on()` starts again from the cursor. */
92
+ close(): void;
93
+ }