@dojocoding/whatsapp-sdk 0.8.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.
@@ -0,0 +1,167 @@
1
+ import { S as Storage } from './index-CDfzGvQJ.js';
2
+
3
+ type WhatsAppEvent = MessageEvent | StatusEvent | TemplateStatusEvent | TemplateQualityUpdateEvent | TemplateCategoryUpdateEvent | PhoneNumberQualityUpdateEvent | AccountAlertEvent | AccountReviewEvent | UnknownEvent;
4
+ interface BaseEvent {
5
+ /** WhatsApp Business Account id this event originated from. */
6
+ wabaId: string;
7
+ /** Phone number id when the event ties to a specific phone (messages, statuses, quality). */
8
+ phoneNumberId?: string;
9
+ /** E.164 display number when known. */
10
+ displayPhoneNumber?: string;
11
+ /** Event timestamp normalised to epoch milliseconds. */
12
+ timestamp: number;
13
+ }
14
+ type IncomingMessageKind = "text" | "image" | "video" | "audio" | "document" | "sticker" | "location" | "contacts" | "interactive_button_reply" | "interactive_list_reply" | "button" | "order" | "reaction" | "system" | "unsupported";
15
+ interface MessageEvent extends BaseEvent {
16
+ kind: "message";
17
+ /** wamid — Meta's unique message id. */
18
+ id: string;
19
+ from: string;
20
+ type: IncomingMessageKind;
21
+ /** The originating message wamid this is a reply to (when present). */
22
+ contextId?: string;
23
+ /**
24
+ * Arbitrary type-specific body. Kept as the raw Meta object on purpose;
25
+ * this lets consumers progressively narrow without locking the SDK to
26
+ * every possible inbound shape. Outbound shapes are handled by
27
+ * {@link WhatsAppMessage}.
28
+ */
29
+ body: Record<string, unknown>;
30
+ }
31
+ /**
32
+ * Documented status transitions Meta sends most often. The union widens
33
+ * to `string` because Meta has shipped new transitions in the past
34
+ * (e.g., `accepted`) without a major-version bump — typing this as a
35
+ * literal-only union would force consumers to upgrade in lockstep.
36
+ */
37
+ type DeliveryStatus = "sent" | "delivered" | "read" | "failed" | (string & {});
38
+ interface StatusEvent extends BaseEvent {
39
+ kind: "status";
40
+ /** wamid the status update applies to. */
41
+ id: string;
42
+ status: DeliveryStatus;
43
+ /** Recipient WA id. */
44
+ recipientId?: string;
45
+ conversationId?: string;
46
+ /** Pricing model ("CBP" vs "PMP" vs "regular") when Meta provides it. */
47
+ pricingCategory?: string;
48
+ /** Raw error envelope when status === "failed". */
49
+ errors?: ReadonlyArray<{
50
+ code?: number;
51
+ title?: string;
52
+ message?: string;
53
+ }>;
54
+ }
55
+ interface TemplateStatusEvent extends BaseEvent {
56
+ kind: "template_status";
57
+ templateId: string;
58
+ templateName?: string;
59
+ language?: string;
60
+ /** APPROVED | REJECTED | DISABLED | PENDING | PAUSED | FLAGGED */
61
+ event: string;
62
+ reason?: string;
63
+ }
64
+ interface TemplateQualityUpdateEvent extends BaseEvent {
65
+ kind: "template_quality";
66
+ templateId: string;
67
+ templateName?: string;
68
+ /** GREEN | YELLOW | RED */
69
+ newQualityScore: string;
70
+ previousQualityScore?: string;
71
+ }
72
+ interface TemplateCategoryUpdateEvent extends BaseEvent {
73
+ kind: "template_category";
74
+ templateId: string;
75
+ templateName?: string;
76
+ newCategory: string;
77
+ previousCategory?: string;
78
+ }
79
+ interface PhoneNumberQualityUpdateEvent extends BaseEvent {
80
+ kind: "phone_number_quality";
81
+ /** GREEN | YELLOW | RED */
82
+ newQualityScore: string;
83
+ }
84
+ interface AccountAlertEvent extends BaseEvent {
85
+ kind: "account_alert";
86
+ /** Severity / type Meta provides. */
87
+ alertSeverity?: string;
88
+ alertType?: string;
89
+ raw: unknown;
90
+ }
91
+ interface AccountReviewEvent extends BaseEvent {
92
+ kind: "account_review";
93
+ decision: string;
94
+ raw: unknown;
95
+ }
96
+ interface UnknownEvent extends BaseEvent {
97
+ kind: "unknown";
98
+ field: string;
99
+ value: unknown;
100
+ }
101
+
102
+ interface WebhookReceiverOptions {
103
+ appSecret: string;
104
+ verifyToken: string;
105
+ storage?: Storage;
106
+ dedupeTtlMs?: number;
107
+ /** Invoked once per handler error (in addition to the `error` event). */
108
+ onError?: (err: unknown, event: WhatsAppEvent | undefined) => void;
109
+ }
110
+ type EventKindMap = {
111
+ message: MessageEvent;
112
+ status: StatusEvent;
113
+ template_status: TemplateStatusEvent;
114
+ template_quality: TemplateQualityUpdateEvent;
115
+ template_category: TemplateCategoryUpdateEvent;
116
+ phone_number_quality: PhoneNumberQualityUpdateEvent;
117
+ account_alert: AccountAlertEvent;
118
+ account_review: AccountReviewEvent;
119
+ unknown: UnknownEvent;
120
+ };
121
+ type Handler<E> = (event: E) => void | Promise<void>;
122
+ type ErrorHandler = (err: unknown, event: WhatsAppEvent | undefined) => void | Promise<void>;
123
+ interface VerifyRequestInput {
124
+ mode: string | null | undefined;
125
+ verifyToken: string | null | undefined;
126
+ challenge: string | null | undefined;
127
+ }
128
+ type VerifyRequestResult = {
129
+ status: 200;
130
+ body: string;
131
+ } | {
132
+ status: 403;
133
+ };
134
+ type HandlePayloadResult = {
135
+ status: 200;
136
+ dispatchPromise: Promise<void>;
137
+ } | {
138
+ status: 401;
139
+ };
140
+ /**
141
+ * Framework-agnostic WhatsApp webhook receiver.
142
+ *
143
+ * Phase 8 wires this into Express via a sub-module adapter. Today,
144
+ * consumers can use it directly:
145
+ *
146
+ * const r = new WebhookReceiver({ appSecret, verifyToken });
147
+ * r.on("message", async (e) => { … });
148
+ * const { status, dispatchPromise } = await r.handlePayload(rawBody, sig, body);
149
+ * res.status(status).end();
150
+ * // dispatchPromise resolves once handlers complete; do not await
151
+ * // it inside the HTTP handler (Meta's 30s ack rule).
152
+ */
153
+ declare class WebhookReceiver {
154
+ #private;
155
+ constructor(options: WebhookReceiverOptions);
156
+ on<K extends keyof EventKindMap>(kind: K, handler: Handler<EventKindMap[K]>): this;
157
+ on(kind: "error", handler: ErrorHandler): this;
158
+ off<K extends keyof EventKindMap>(kind: K, handler: Handler<EventKindMap[K]>): this;
159
+ off(kind: "error", handler: ErrorHandler): this;
160
+ verify(rawBody: Buffer | Uint8Array | string, signatureHeader: string | null | undefined): Promise<boolean>;
161
+ handleVerifyRequest(input: VerifyRequestInput): VerifyRequestResult;
162
+ handlePayload(rawBody: Buffer | Uint8Array | string, signatureHeader: string | null | undefined, parsedBody: unknown): Promise<HandlePayloadResult>;
163
+ /** @internal — used by mock-mode (Phase 6) to inject synthetic events. */
164
+ _dispatchEvents(events: ReadonlyArray<WhatsAppEvent>): Promise<void>;
165
+ }
166
+
167
+ export { type AccountAlertEvent as A, type BaseEvent as B, type DeliveryStatus as D, type ErrorHandler as E, type HandlePayloadResult as H, type IncomingMessageKind as I, type MessageEvent as M, type PhoneNumberQualityUpdateEvent as P, type StatusEvent as S, type TemplateCategoryUpdateEvent as T, type UnknownEvent as U, type VerifyRequestInput as V, WebhookReceiver as W, type WhatsAppEvent as a, type AccountReviewEvent as b, type EventKindMap as c, type Handler as d, type TemplateQualityUpdateEvent as e, type TemplateStatusEvent as f, type VerifyRequestResult as g, type WebhookReceiverOptions as h };
@@ -0,0 +1,167 @@
1
+ import { S as Storage } from './index-CDfzGvQJ.cjs';
2
+
3
+ type WhatsAppEvent = MessageEvent | StatusEvent | TemplateStatusEvent | TemplateQualityUpdateEvent | TemplateCategoryUpdateEvent | PhoneNumberQualityUpdateEvent | AccountAlertEvent | AccountReviewEvent | UnknownEvent;
4
+ interface BaseEvent {
5
+ /** WhatsApp Business Account id this event originated from. */
6
+ wabaId: string;
7
+ /** Phone number id when the event ties to a specific phone (messages, statuses, quality). */
8
+ phoneNumberId?: string;
9
+ /** E.164 display number when known. */
10
+ displayPhoneNumber?: string;
11
+ /** Event timestamp normalised to epoch milliseconds. */
12
+ timestamp: number;
13
+ }
14
+ type IncomingMessageKind = "text" | "image" | "video" | "audio" | "document" | "sticker" | "location" | "contacts" | "interactive_button_reply" | "interactive_list_reply" | "button" | "order" | "reaction" | "system" | "unsupported";
15
+ interface MessageEvent extends BaseEvent {
16
+ kind: "message";
17
+ /** wamid — Meta's unique message id. */
18
+ id: string;
19
+ from: string;
20
+ type: IncomingMessageKind;
21
+ /** The originating message wamid this is a reply to (when present). */
22
+ contextId?: string;
23
+ /**
24
+ * Arbitrary type-specific body. Kept as the raw Meta object on purpose;
25
+ * this lets consumers progressively narrow without locking the SDK to
26
+ * every possible inbound shape. Outbound shapes are handled by
27
+ * {@link WhatsAppMessage}.
28
+ */
29
+ body: Record<string, unknown>;
30
+ }
31
+ /**
32
+ * Documented status transitions Meta sends most often. The union widens
33
+ * to `string` because Meta has shipped new transitions in the past
34
+ * (e.g., `accepted`) without a major-version bump — typing this as a
35
+ * literal-only union would force consumers to upgrade in lockstep.
36
+ */
37
+ type DeliveryStatus = "sent" | "delivered" | "read" | "failed" | (string & {});
38
+ interface StatusEvent extends BaseEvent {
39
+ kind: "status";
40
+ /** wamid the status update applies to. */
41
+ id: string;
42
+ status: DeliveryStatus;
43
+ /** Recipient WA id. */
44
+ recipientId?: string;
45
+ conversationId?: string;
46
+ /** Pricing model ("CBP" vs "PMP" vs "regular") when Meta provides it. */
47
+ pricingCategory?: string;
48
+ /** Raw error envelope when status === "failed". */
49
+ errors?: ReadonlyArray<{
50
+ code?: number;
51
+ title?: string;
52
+ message?: string;
53
+ }>;
54
+ }
55
+ interface TemplateStatusEvent extends BaseEvent {
56
+ kind: "template_status";
57
+ templateId: string;
58
+ templateName?: string;
59
+ language?: string;
60
+ /** APPROVED | REJECTED | DISABLED | PENDING | PAUSED | FLAGGED */
61
+ event: string;
62
+ reason?: string;
63
+ }
64
+ interface TemplateQualityUpdateEvent extends BaseEvent {
65
+ kind: "template_quality";
66
+ templateId: string;
67
+ templateName?: string;
68
+ /** GREEN | YELLOW | RED */
69
+ newQualityScore: string;
70
+ previousQualityScore?: string;
71
+ }
72
+ interface TemplateCategoryUpdateEvent extends BaseEvent {
73
+ kind: "template_category";
74
+ templateId: string;
75
+ templateName?: string;
76
+ newCategory: string;
77
+ previousCategory?: string;
78
+ }
79
+ interface PhoneNumberQualityUpdateEvent extends BaseEvent {
80
+ kind: "phone_number_quality";
81
+ /** GREEN | YELLOW | RED */
82
+ newQualityScore: string;
83
+ }
84
+ interface AccountAlertEvent extends BaseEvent {
85
+ kind: "account_alert";
86
+ /** Severity / type Meta provides. */
87
+ alertSeverity?: string;
88
+ alertType?: string;
89
+ raw: unknown;
90
+ }
91
+ interface AccountReviewEvent extends BaseEvent {
92
+ kind: "account_review";
93
+ decision: string;
94
+ raw: unknown;
95
+ }
96
+ interface UnknownEvent extends BaseEvent {
97
+ kind: "unknown";
98
+ field: string;
99
+ value: unknown;
100
+ }
101
+
102
+ interface WebhookReceiverOptions {
103
+ appSecret: string;
104
+ verifyToken: string;
105
+ storage?: Storage;
106
+ dedupeTtlMs?: number;
107
+ /** Invoked once per handler error (in addition to the `error` event). */
108
+ onError?: (err: unknown, event: WhatsAppEvent | undefined) => void;
109
+ }
110
+ type EventKindMap = {
111
+ message: MessageEvent;
112
+ status: StatusEvent;
113
+ template_status: TemplateStatusEvent;
114
+ template_quality: TemplateQualityUpdateEvent;
115
+ template_category: TemplateCategoryUpdateEvent;
116
+ phone_number_quality: PhoneNumberQualityUpdateEvent;
117
+ account_alert: AccountAlertEvent;
118
+ account_review: AccountReviewEvent;
119
+ unknown: UnknownEvent;
120
+ };
121
+ type Handler<E> = (event: E) => void | Promise<void>;
122
+ type ErrorHandler = (err: unknown, event: WhatsAppEvent | undefined) => void | Promise<void>;
123
+ interface VerifyRequestInput {
124
+ mode: string | null | undefined;
125
+ verifyToken: string | null | undefined;
126
+ challenge: string | null | undefined;
127
+ }
128
+ type VerifyRequestResult = {
129
+ status: 200;
130
+ body: string;
131
+ } | {
132
+ status: 403;
133
+ };
134
+ type HandlePayloadResult = {
135
+ status: 200;
136
+ dispatchPromise: Promise<void>;
137
+ } | {
138
+ status: 401;
139
+ };
140
+ /**
141
+ * Framework-agnostic WhatsApp webhook receiver.
142
+ *
143
+ * Phase 8 wires this into Express via a sub-module adapter. Today,
144
+ * consumers can use it directly:
145
+ *
146
+ * const r = new WebhookReceiver({ appSecret, verifyToken });
147
+ * r.on("message", async (e) => { … });
148
+ * const { status, dispatchPromise } = await r.handlePayload(rawBody, sig, body);
149
+ * res.status(status).end();
150
+ * // dispatchPromise resolves once handlers complete; do not await
151
+ * // it inside the HTTP handler (Meta's 30s ack rule).
152
+ */
153
+ declare class WebhookReceiver {
154
+ #private;
155
+ constructor(options: WebhookReceiverOptions);
156
+ on<K extends keyof EventKindMap>(kind: K, handler: Handler<EventKindMap[K]>): this;
157
+ on(kind: "error", handler: ErrorHandler): this;
158
+ off<K extends keyof EventKindMap>(kind: K, handler: Handler<EventKindMap[K]>): this;
159
+ off(kind: "error", handler: ErrorHandler): this;
160
+ verify(rawBody: Buffer | Uint8Array | string, signatureHeader: string | null | undefined): Promise<boolean>;
161
+ handleVerifyRequest(input: VerifyRequestInput): VerifyRequestResult;
162
+ handlePayload(rawBody: Buffer | Uint8Array | string, signatureHeader: string | null | undefined, parsedBody: unknown): Promise<HandlePayloadResult>;
163
+ /** @internal — used by mock-mode (Phase 6) to inject synthetic events. */
164
+ _dispatchEvents(events: ReadonlyArray<WhatsAppEvent>): Promise<void>;
165
+ }
166
+
167
+ export { type AccountAlertEvent as A, type BaseEvent as B, type DeliveryStatus as D, type ErrorHandler as E, type HandlePayloadResult as H, type IncomingMessageKind as I, type MessageEvent as M, type PhoneNumberQualityUpdateEvent as P, type StatusEvent as S, type TemplateCategoryUpdateEvent as T, type UnknownEvent as U, type VerifyRequestInput as V, WebhookReceiver as W, type WhatsAppEvent as a, type AccountReviewEvent as b, type EventKindMap as c, type Handler as d, type TemplateQualityUpdateEvent as e, type TemplateStatusEvent as f, type VerifyRequestResult as g, type WebhookReceiverOptions as h };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ // src/storage/postgres.ts
4
+ var POSTGRES_STORAGE_SCHEMA = `
5
+ CREATE TABLE IF NOT EXISTS whatsapp_storage (
6
+ key TEXT PRIMARY KEY,
7
+ value JSONB NOT NULL,
8
+ expires_at TIMESTAMPTZ NOT NULL
9
+ );
10
+ CREATE INDEX IF NOT EXISTS whatsapp_storage_expires_at_idx
11
+ ON whatsapp_storage (expires_at);
12
+ `.trim();
13
+ var INFINITY_TS = "infinity";
14
+ function createPostgresStorage(client, options = {}) {
15
+ const prefix = options.keyPrefix ?? "whatsapp:";
16
+ const table = options.table ?? "whatsapp_storage";
17
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
18
+ throw new TypeError(
19
+ `PostgresStorage: table name must be alphanumeric+underscore; got ${JSON.stringify(table)}.`
20
+ );
21
+ }
22
+ const k = (key) => prefix + key;
23
+ return {
24
+ async get(key) {
25
+ const result = await client.query(
26
+ `SELECT value FROM ${table} WHERE key = $1 AND expires_at > now()`,
27
+ [k(key)]
28
+ );
29
+ if (result.rows.length === 0) return void 0;
30
+ return result.rows[0].value;
31
+ },
32
+ async set(key, value, ttlMs) {
33
+ const expiresExpr = ttlMs > 0 ? `now() + ($3 || ' milliseconds')::interval` : `'${INFINITY_TS}'::timestamptz`;
34
+ const params = [k(key), JSON.stringify(value)];
35
+ if (ttlMs > 0) params.push(String(ttlMs));
36
+ await client.query(
37
+ `INSERT INTO ${table} (key, value, expires_at)
38
+ VALUES ($1, $2::jsonb, ${expiresExpr})
39
+ ON CONFLICT (key) DO UPDATE
40
+ SET value = excluded.value, expires_at = excluded.expires_at`,
41
+ params
42
+ );
43
+ },
44
+ async setIfAbsent(key, value, ttlMs) {
45
+ const expiresExpr = ttlMs > 0 ? `now() + ($3 || ' milliseconds')::interval` : `'${INFINITY_TS}'::timestamptz`;
46
+ const params = [k(key), JSON.stringify(value)];
47
+ if (ttlMs > 0) params.push(String(ttlMs));
48
+ const result = await client.query(
49
+ `INSERT INTO ${table} (key, value, expires_at)
50
+ VALUES ($1, $2::jsonb, ${expiresExpr})
51
+ ON CONFLICT (key) DO UPDATE
52
+ SET value = excluded.value, expires_at = excluded.expires_at
53
+ WHERE ${table}.expires_at <= now()
54
+ RETURNING key`,
55
+ params
56
+ );
57
+ return result.rows.length > 0;
58
+ },
59
+ async delete(key) {
60
+ await client.query(`DELETE FROM ${table} WHERE key = $1`, [k(key)]);
61
+ }
62
+ };
63
+ }
64
+
65
+ exports.POSTGRES_STORAGE_SCHEMA = POSTGRES_STORAGE_SCHEMA;
66
+ exports.createPostgresStorage = createPostgresStorage;
@@ -0,0 +1,38 @@
1
+ import { S as Storage } from '../index-CDfzGvQJ.cjs';
2
+
3
+ /**
4
+ * Minimal structural interface of a `pg`-shaped client. Anything
5
+ * that provides `query<R>(sql, params?): Promise<{ rows: R[] }>`
6
+ * works — production `pg.Pool`, `pg.Client`, or a test fake. The
7
+ * SDK does NOT import `pg` at runtime.
8
+ */
9
+ interface PgLike {
10
+ query<R = unknown>(sql: string, params?: ReadonlyArray<unknown>): Promise<{
11
+ rows: R[];
12
+ }>;
13
+ }
14
+ interface PostgresStorageOptions {
15
+ /** Prepended to every key. Defaults to `"whatsapp:"`. */
16
+ keyPrefix?: string;
17
+ /** Table name. Defaults to `"whatsapp_storage"`. */
18
+ table?: string;
19
+ }
20
+ /**
21
+ * DDL for the storage table. Consumers MUST run this once (via
22
+ * their own migration tool) before using `createPostgresStorage`.
23
+ * Idempotent; safe to run repeatedly.
24
+ *
25
+ * The default table name is `whatsapp_storage`. If you override
26
+ * the `table` option, edit this SQL accordingly.
27
+ */
28
+ declare const POSTGRES_STORAGE_SCHEMA: string;
29
+ /**
30
+ * Create a {@link Storage} backed by a `PgLike` client. The
31
+ * caller owns the connection (pool, TLS, retries). Expired rows
32
+ * are filtered on `get` (`WHERE expires_at > now()`) but not
33
+ * proactively deleted — schedule a `DELETE FROM <table> WHERE
34
+ * expires_at < now()` job if table growth becomes a concern.
35
+ */
36
+ declare function createPostgresStorage(client: PgLike, options?: PostgresStorageOptions): Storage;
37
+
38
+ export { POSTGRES_STORAGE_SCHEMA, type PgLike, type PostgresStorageOptions, createPostgresStorage };
@@ -0,0 +1,38 @@
1
+ import { S as Storage } from '../index-CDfzGvQJ.js';
2
+
3
+ /**
4
+ * Minimal structural interface of a `pg`-shaped client. Anything
5
+ * that provides `query<R>(sql, params?): Promise<{ rows: R[] }>`
6
+ * works — production `pg.Pool`, `pg.Client`, or a test fake. The
7
+ * SDK does NOT import `pg` at runtime.
8
+ */
9
+ interface PgLike {
10
+ query<R = unknown>(sql: string, params?: ReadonlyArray<unknown>): Promise<{
11
+ rows: R[];
12
+ }>;
13
+ }
14
+ interface PostgresStorageOptions {
15
+ /** Prepended to every key. Defaults to `"whatsapp:"`. */
16
+ keyPrefix?: string;
17
+ /** Table name. Defaults to `"whatsapp_storage"`. */
18
+ table?: string;
19
+ }
20
+ /**
21
+ * DDL for the storage table. Consumers MUST run this once (via
22
+ * their own migration tool) before using `createPostgresStorage`.
23
+ * Idempotent; safe to run repeatedly.
24
+ *
25
+ * The default table name is `whatsapp_storage`. If you override
26
+ * the `table` option, edit this SQL accordingly.
27
+ */
28
+ declare const POSTGRES_STORAGE_SCHEMA: string;
29
+ /**
30
+ * Create a {@link Storage} backed by a `PgLike` client. The
31
+ * caller owns the connection (pool, TLS, retries). Expired rows
32
+ * are filtered on `get` (`WHERE expires_at > now()`) but not
33
+ * proactively deleted — schedule a `DELETE FROM <table> WHERE
34
+ * expires_at < now()` job if table growth becomes a concern.
35
+ */
36
+ declare function createPostgresStorage(client: PgLike, options?: PostgresStorageOptions): Storage;
37
+
38
+ export { POSTGRES_STORAGE_SCHEMA, type PgLike, type PostgresStorageOptions, createPostgresStorage };
@@ -0,0 +1,63 @@
1
+ // src/storage/postgres.ts
2
+ var POSTGRES_STORAGE_SCHEMA = `
3
+ CREATE TABLE IF NOT EXISTS whatsapp_storage (
4
+ key TEXT PRIMARY KEY,
5
+ value JSONB NOT NULL,
6
+ expires_at TIMESTAMPTZ NOT NULL
7
+ );
8
+ CREATE INDEX IF NOT EXISTS whatsapp_storage_expires_at_idx
9
+ ON whatsapp_storage (expires_at);
10
+ `.trim();
11
+ var INFINITY_TS = "infinity";
12
+ function createPostgresStorage(client, options = {}) {
13
+ const prefix = options.keyPrefix ?? "whatsapp:";
14
+ const table = options.table ?? "whatsapp_storage";
15
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
16
+ throw new TypeError(
17
+ `PostgresStorage: table name must be alphanumeric+underscore; got ${JSON.stringify(table)}.`
18
+ );
19
+ }
20
+ const k = (key) => prefix + key;
21
+ return {
22
+ async get(key) {
23
+ const result = await client.query(
24
+ `SELECT value FROM ${table} WHERE key = $1 AND expires_at > now()`,
25
+ [k(key)]
26
+ );
27
+ if (result.rows.length === 0) return void 0;
28
+ return result.rows[0].value;
29
+ },
30
+ async set(key, value, ttlMs) {
31
+ const expiresExpr = ttlMs > 0 ? `now() + ($3 || ' milliseconds')::interval` : `'${INFINITY_TS}'::timestamptz`;
32
+ const params = [k(key), JSON.stringify(value)];
33
+ if (ttlMs > 0) params.push(String(ttlMs));
34
+ await client.query(
35
+ `INSERT INTO ${table} (key, value, expires_at)
36
+ VALUES ($1, $2::jsonb, ${expiresExpr})
37
+ ON CONFLICT (key) DO UPDATE
38
+ SET value = excluded.value, expires_at = excluded.expires_at`,
39
+ params
40
+ );
41
+ },
42
+ async setIfAbsent(key, value, ttlMs) {
43
+ const expiresExpr = ttlMs > 0 ? `now() + ($3 || ' milliseconds')::interval` : `'${INFINITY_TS}'::timestamptz`;
44
+ const params = [k(key), JSON.stringify(value)];
45
+ if (ttlMs > 0) params.push(String(ttlMs));
46
+ const result = await client.query(
47
+ `INSERT INTO ${table} (key, value, expires_at)
48
+ VALUES ($1, $2::jsonb, ${expiresExpr})
49
+ ON CONFLICT (key) DO UPDATE
50
+ SET value = excluded.value, expires_at = excluded.expires_at
51
+ WHERE ${table}.expires_at <= now()
52
+ RETURNING key`,
53
+ params
54
+ );
55
+ return result.rows.length > 0;
56
+ },
57
+ async delete(key) {
58
+ await client.query(`DELETE FROM ${table} WHERE key = $1`, [k(key)]);
59
+ }
60
+ };
61
+ }
62
+
63
+ export { POSTGRES_STORAGE_SCHEMA, createPostgresStorage };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ // src/storage/redis.ts
4
+ function createRedisStorage(client, options = {}) {
5
+ const prefix = options.keyPrefix ?? "whatsapp:";
6
+ const k = (key) => prefix + key;
7
+ return {
8
+ async get(key) {
9
+ const raw = await client.get(k(key));
10
+ if (raw === null) return void 0;
11
+ return JSON.parse(raw);
12
+ },
13
+ async set(key, value, ttlMs) {
14
+ const serialized = JSON.stringify(value);
15
+ if (ttlMs > 0) {
16
+ await client.set(k(key), serialized, "PX", ttlMs);
17
+ } else {
18
+ await client.set(k(key), serialized);
19
+ }
20
+ },
21
+ async setIfAbsent(key, value, ttlMs) {
22
+ const serialized = JSON.stringify(value);
23
+ const result = ttlMs > 0 ? await client.set(k(key), serialized, "PX", ttlMs, "NX") : await client.set(k(key), serialized, "NX");
24
+ return result === "OK";
25
+ },
26
+ async delete(key) {
27
+ await client.del(k(key));
28
+ }
29
+ };
30
+ }
31
+
32
+ exports.createRedisStorage = createRedisStorage;
@@ -0,0 +1,38 @@
1
+ import { S as Storage } from '../index-CDfzGvQJ.cjs';
2
+
3
+ /**
4
+ * Minimal structural interface of an `ioredis`-shaped client.
5
+ * Anything that provides these three methods works — production
6
+ * `ioredis`, the `node-redis` v4 legacy mode, or a test fake. The
7
+ * SDK does NOT import `ioredis` at runtime.
8
+ */
9
+ interface RedisLike {
10
+ get(key: string): Promise<string | null>;
11
+ /**
12
+ * `ioredis`'s variadic `SET`. Accepts the value followed by an
13
+ * arbitrary list of `["PX", ttlMs, "NX"]` etc. Returns `"OK"`
14
+ * on success, `null` when `NX` rejected.
15
+ */
16
+ set(key: string, value: string, ...args: Array<string | number>): Promise<string | null>;
17
+ del(...keys: string[]): Promise<number>;
18
+ }
19
+ interface RedisStorageOptions {
20
+ /**
21
+ * Prepended to every key. Lets multiple consumers share one
22
+ * Redis instance. Defaults to `"whatsapp:"`.
23
+ */
24
+ keyPrefix?: string;
25
+ }
26
+ /**
27
+ * Create a {@link Storage} backed by a `RedisLike` client. The
28
+ * client is owned by the consumer (connection pooling, TLS, auth,
29
+ * retries are upstream concerns).
30
+ *
31
+ * Values are JSON-encoded; TTL is enforced by Redis itself via
32
+ * the `PX` argument on `SET`. `ttlMs <= 0` stores forever (no
33
+ * `PX` argument). `setIfAbsent` uses `SET NX` and is atomic by
34
+ * Redis semantics.
35
+ */
36
+ declare function createRedisStorage(client: RedisLike, options?: RedisStorageOptions): Storage;
37
+
38
+ export { type RedisLike, type RedisStorageOptions, createRedisStorage };
@@ -0,0 +1,38 @@
1
+ import { S as Storage } from '../index-CDfzGvQJ.js';
2
+
3
+ /**
4
+ * Minimal structural interface of an `ioredis`-shaped client.
5
+ * Anything that provides these three methods works — production
6
+ * `ioredis`, the `node-redis` v4 legacy mode, or a test fake. The
7
+ * SDK does NOT import `ioredis` at runtime.
8
+ */
9
+ interface RedisLike {
10
+ get(key: string): Promise<string | null>;
11
+ /**
12
+ * `ioredis`'s variadic `SET`. Accepts the value followed by an
13
+ * arbitrary list of `["PX", ttlMs, "NX"]` etc. Returns `"OK"`
14
+ * on success, `null` when `NX` rejected.
15
+ */
16
+ set(key: string, value: string, ...args: Array<string | number>): Promise<string | null>;
17
+ del(...keys: string[]): Promise<number>;
18
+ }
19
+ interface RedisStorageOptions {
20
+ /**
21
+ * Prepended to every key. Lets multiple consumers share one
22
+ * Redis instance. Defaults to `"whatsapp:"`.
23
+ */
24
+ keyPrefix?: string;
25
+ }
26
+ /**
27
+ * Create a {@link Storage} backed by a `RedisLike` client. The
28
+ * client is owned by the consumer (connection pooling, TLS, auth,
29
+ * retries are upstream concerns).
30
+ *
31
+ * Values are JSON-encoded; TTL is enforced by Redis itself via
32
+ * the `PX` argument on `SET`. `ttlMs <= 0` stores forever (no
33
+ * `PX` argument). `setIfAbsent` uses `SET NX` and is atomic by
34
+ * Redis semantics.
35
+ */
36
+ declare function createRedisStorage(client: RedisLike, options?: RedisStorageOptions): Storage;
37
+
38
+ export { type RedisLike, type RedisStorageOptions, createRedisStorage };
@@ -0,0 +1,30 @@
1
+ // src/storage/redis.ts
2
+ function createRedisStorage(client, options = {}) {
3
+ const prefix = options.keyPrefix ?? "whatsapp:";
4
+ const k = (key) => prefix + key;
5
+ return {
6
+ async get(key) {
7
+ const raw = await client.get(k(key));
8
+ if (raw === null) return void 0;
9
+ return JSON.parse(raw);
10
+ },
11
+ async set(key, value, ttlMs) {
12
+ const serialized = JSON.stringify(value);
13
+ if (ttlMs > 0) {
14
+ await client.set(k(key), serialized, "PX", ttlMs);
15
+ } else {
16
+ await client.set(k(key), serialized);
17
+ }
18
+ },
19
+ async setIfAbsent(key, value, ttlMs) {
20
+ const serialized = JSON.stringify(value);
21
+ const result = ttlMs > 0 ? await client.set(k(key), serialized, "PX", ttlMs, "NX") : await client.set(k(key), serialized, "NX");
22
+ return result === "OK";
23
+ },
24
+ async delete(key) {
25
+ await client.del(k(key));
26
+ }
27
+ };
28
+ }
29
+
30
+ export { createRedisStorage };