@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.
- package/CHANGELOG.md +402 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/adapters/express/index.cjs +114 -0
- package/dist/adapters/express/index.d.cts +42 -0
- package/dist/adapters/express/index.d.ts +42 -0
- package/dist/adapters/express/index.js +108 -0
- package/dist/adapters/hono/index.cjs +52 -0
- package/dist/adapters/hono/index.d.cts +38 -0
- package/dist/adapters/hono/index.d.ts +38 -0
- package/dist/adapters/hono/index.js +50 -0
- package/dist/adapters/web/index.cjs +46 -0
- package/dist/adapters/web/index.d.cts +40 -0
- package/dist/adapters/web/index.d.ts +40 -0
- package/dist/adapters/web/index.js +44 -0
- package/dist/index-CDfzGvQJ.d.cts +42 -0
- package/dist/index-CDfzGvQJ.d.ts +42 -0
- package/dist/index.cjs +2242 -0
- package/dist/index.d.cts +1262 -0
- package/dist/index.d.ts +1262 -0
- package/dist/index.js +2183 -0
- package/dist/receiver-C_yfwg6g.d.ts +167 -0
- package/dist/receiver-DWJm571Z.d.cts +167 -0
- package/dist/storage/postgres.cjs +66 -0
- package/dist/storage/postgres.d.cts +38 -0
- package/dist/storage/postgres.d.ts +38 -0
- package/dist/storage/postgres.js +63 -0
- package/dist/storage/redis.cjs +32 -0
- package/dist/storage/redis.d.cts +38 -0
- package/dist/storage/redis.d.ts +38 -0
- package/dist/storage/redis.js +30 -0
- package/package.json +181 -0
|
@@ -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 };
|