@spokpay/chat 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.
@@ -0,0 +1,209 @@
1
+ import type { SpokPayChatOptions, CreateConversationOptions, Conversation, GetConversationOptions, SendMessageOptions, Message, ListMessagesOptions, CloseConversationOptions, GetUploadUrlOptions, VerifyWebhookOptions } from "./types.js";
2
+ /**
3
+ * SpokPay Chat SDK client.
4
+ *
5
+ * Provides methods for creating conversations, sending messages, and
6
+ * receiving staff replies via webhooks. Zero dependencies — uses native
7
+ * `fetch` and Web Crypto API.
8
+ *
9
+ * @example Basic setup
10
+ * ```ts
11
+ * import { SpokPayChat } from "@spokpay/chat";
12
+ *
13
+ * const chat = new SpokPayChat({
14
+ * apiKey: "spk_live_abc123...",
15
+ * baseUrl: "https://your-deployment.convex.site",
16
+ * });
17
+ * ```
18
+ *
19
+ * @example Create a conversation and send a message
20
+ * ```ts
21
+ * const conv = await chat.conversations.create({
22
+ * type: "order_support",
23
+ * customer: { externalId: "cust_123", name: "João" },
24
+ * subject: "Issue with my order",
25
+ * });
26
+ *
27
+ * await chat.conversations.sendMessage({
28
+ * conversationId: conv.id,
29
+ * content: "When will my order arrive?",
30
+ * });
31
+ * ```
32
+ *
33
+ * @example Receive staff replies via webhook
34
+ * ```ts
35
+ * // In your webhook handler
36
+ * const body = await req.text();
37
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
38
+ * payload: body,
39
+ * signature: req.headers.get("x-spokpay-signature")!,
40
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
41
+ * });
42
+ *
43
+ * if (isValid) {
44
+ * const event = JSON.parse(body);
45
+ * if (event.type === "message.created") {
46
+ * // Staff replied — show the message to the customer
47
+ * displayMessage(event.data.content, event.data.authorDisplayName);
48
+ * }
49
+ * }
50
+ * ```
51
+ */
52
+ export declare class SpokPayChat {
53
+ private readonly apiKey;
54
+ private readonly baseUrl;
55
+ /**
56
+ * Client for managing conversations and messages.
57
+ */
58
+ readonly conversations: ConversationsClient;
59
+ constructor(options: SpokPayChatOptions);
60
+ /**
61
+ * Verify a webhook signature to ensure the request came from SpokPay.
62
+ *
63
+ * Uses HMAC-SHA256 with constant-time comparison to prevent timing attacks.
64
+ * Works in all JavaScript runtimes: Node.js 18+, Deno, Cloudflare Workers,
65
+ * Vercel Edge, and Convex.
66
+ *
67
+ * @param options - The raw payload, signature header, and your webhook secret.
68
+ * @returns `true` if the signature is valid, `false` otherwise.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
73
+ * payload: rawBody,
74
+ * signature: req.headers.get("x-spokpay-signature")!,
75
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
76
+ * });
77
+ * ```
78
+ */
79
+ static verifyWebhookSignature(options: VerifyWebhookOptions): Promise<boolean>;
80
+ }
81
+ declare class ConversationsClient {
82
+ private readonly apiKey;
83
+ private readonly baseUrl;
84
+ constructor(apiKey: string, baseUrl: string);
85
+ /**
86
+ * Create a new conversation.
87
+ *
88
+ * @param options - Conversation details (type, customer, subject, etc.).
89
+ * @returns The created conversation.
90
+ * @throws {@link SpokPayChatApiError} if the API returns an error.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const conv = await chat.conversations.create({
95
+ * type: "order_support",
96
+ * externalId: "ticket_123",
97
+ * customer: { externalId: "cust_abc", name: "João" },
98
+ * subject: "Order issue",
99
+ * metadata: { orderId: "order_456" },
100
+ * });
101
+ * ```
102
+ */
103
+ create(options: CreateConversationOptions): Promise<Conversation>;
104
+ /**
105
+ * Get a conversation by SpokPay ID or your external ID.
106
+ *
107
+ * @param options - Lookup criteria (SpokPay ID or external ID).
108
+ * @returns The conversation object.
109
+ * @throws {@link SpokPayChatApiError} with status 404 if not found.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * const conv = await chat.conversations.get({ externalId: "ticket_123" });
114
+ * ```
115
+ */
116
+ get(options: GetConversationOptions): Promise<Conversation>;
117
+ /**
118
+ * Send a message from a customer.
119
+ *
120
+ * @param options - Message details (conversation, content, author info).
121
+ * @returns The created message.
122
+ * @throws {@link SpokPayChatApiError} if the conversation is closed or not found.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const msg = await chat.conversations.sendMessage({
127
+ * conversationId: conv.id,
128
+ * content: "When will my order arrive?",
129
+ * authorExternalId: "cust_abc",
130
+ * authorDisplayName: "João",
131
+ * });
132
+ * ```
133
+ */
134
+ sendMessage(options: SendMessageOptions): Promise<{
135
+ id: string;
136
+ conversationId: string;
137
+ content: string;
138
+ createdAt: number;
139
+ }>;
140
+ /**
141
+ * List messages in a conversation.
142
+ *
143
+ * @param options - Conversation ID/externalId and pagination options.
144
+ * @returns Array of messages, newest first.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * const { messages } = await chat.conversations.listMessages({
149
+ * conversationId: conv.id,
150
+ * limit: 20,
151
+ * });
152
+ * ```
153
+ */
154
+ listMessages(options: ListMessagesOptions): Promise<{
155
+ messages: Message[];
156
+ }>;
157
+ /**
158
+ * Close a conversation.
159
+ *
160
+ * @param options - The conversation ID to close.
161
+ * @returns Confirmation with the closed conversation ID and status.
162
+ * @throws {@link SpokPayChatApiError} if the conversation is already closed or not found.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * await chat.conversations.close({ id: conv.id });
167
+ * ```
168
+ */
169
+ close(options: CloseConversationOptions): Promise<{
170
+ id: string;
171
+ status: "closed";
172
+ }>;
173
+ /**
174
+ * Get a storage upload URL for file attachments.
175
+ *
176
+ * Upload your file to the returned URL with a PUT request, then include
177
+ * the resulting `storageId` in your message attachments.
178
+ *
179
+ * @param options - The conversation ID.
180
+ * @returns An upload URL.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const { uploadUrl } = await chat.conversations.getUploadUrl({
185
+ * conversationId: conv.id,
186
+ * });
187
+ *
188
+ * // Upload the file
189
+ * const res = await fetch(uploadUrl, {
190
+ * method: "POST",
191
+ * headers: { "Content-Type": file.type },
192
+ * body: file,
193
+ * });
194
+ * const { storageId } = await res.json();
195
+ *
196
+ * // Send message with attachment
197
+ * await chat.conversations.sendMessage({
198
+ * conversationId: conv.id,
199
+ * content: "Here is the screenshot",
200
+ * attachments: [{ storageId, filename: "screenshot.png", contentType: "image/png" }],
201
+ * });
202
+ * ```
203
+ */
204
+ getUploadUrl(options: GetUploadUrlOptions): Promise<{
205
+ uploadUrl: string;
206
+ }>;
207
+ private request;
208
+ }
209
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,300 @@
1
+ import { SpokPayChatApiError } from "./types.js";
2
+ const DEFAULT_BASE_URL = "https://your-convex-deployment.convex.site";
3
+ /**
4
+ * SpokPay Chat SDK client.
5
+ *
6
+ * Provides methods for creating conversations, sending messages, and
7
+ * receiving staff replies via webhooks. Zero dependencies — uses native
8
+ * `fetch` and Web Crypto API.
9
+ *
10
+ * @example Basic setup
11
+ * ```ts
12
+ * import { SpokPayChat } from "@spokpay/chat";
13
+ *
14
+ * const chat = new SpokPayChat({
15
+ * apiKey: "spk_live_abc123...",
16
+ * baseUrl: "https://your-deployment.convex.site",
17
+ * });
18
+ * ```
19
+ *
20
+ * @example Create a conversation and send a message
21
+ * ```ts
22
+ * const conv = await chat.conversations.create({
23
+ * type: "order_support",
24
+ * customer: { externalId: "cust_123", name: "João" },
25
+ * subject: "Issue with my order",
26
+ * });
27
+ *
28
+ * await chat.conversations.sendMessage({
29
+ * conversationId: conv.id,
30
+ * content: "When will my order arrive?",
31
+ * });
32
+ * ```
33
+ *
34
+ * @example Receive staff replies via webhook
35
+ * ```ts
36
+ * // In your webhook handler
37
+ * const body = await req.text();
38
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
39
+ * payload: body,
40
+ * signature: req.headers.get("x-spokpay-signature")!,
41
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
42
+ * });
43
+ *
44
+ * if (isValid) {
45
+ * const event = JSON.parse(body);
46
+ * if (event.type === "message.created") {
47
+ * // Staff replied — show the message to the customer
48
+ * displayMessage(event.data.content, event.data.authorDisplayName);
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ export class SpokPayChat {
54
+ apiKey;
55
+ baseUrl;
56
+ /**
57
+ * Client for managing conversations and messages.
58
+ */
59
+ conversations;
60
+ constructor(options) {
61
+ this.apiKey = options.apiKey;
62
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
63
+ this.conversations = new ConversationsClient(this.apiKey, this.baseUrl);
64
+ }
65
+ /**
66
+ * Verify a webhook signature to ensure the request came from SpokPay.
67
+ *
68
+ * Uses HMAC-SHA256 with constant-time comparison to prevent timing attacks.
69
+ * Works in all JavaScript runtimes: Node.js 18+, Deno, Cloudflare Workers,
70
+ * Vercel Edge, and Convex.
71
+ *
72
+ * @param options - The raw payload, signature header, and your webhook secret.
73
+ * @returns `true` if the signature is valid, `false` otherwise.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
78
+ * payload: rawBody,
79
+ * signature: req.headers.get("x-spokpay-signature")!,
80
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
81
+ * });
82
+ * ```
83
+ */
84
+ static async verifyWebhookSignature(options) {
85
+ const { payload, signature, secret } = options;
86
+ if (!signature.startsWith("sha256="))
87
+ return false;
88
+ const expectedHex = signature.slice(7);
89
+ const encoder = new TextEncoder();
90
+ const key = await globalThis.crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
91
+ const signatureBuffer = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(payload));
92
+ const computedHex = Array.from(new Uint8Array(signatureBuffer))
93
+ .map((b) => b.toString(16).padStart(2, "0"))
94
+ .join("");
95
+ // Constant-time comparison
96
+ if (computedHex.length !== expectedHex.length)
97
+ return false;
98
+ let result = 0;
99
+ for (let i = 0; i < computedHex.length; i++) {
100
+ result |= computedHex.charCodeAt(i) ^ expectedHex.charCodeAt(i);
101
+ }
102
+ return result === 0;
103
+ }
104
+ }
105
+ class ConversationsClient {
106
+ apiKey;
107
+ baseUrl;
108
+ constructor(apiKey, baseUrl) {
109
+ this.apiKey = apiKey;
110
+ this.baseUrl = baseUrl;
111
+ }
112
+ /**
113
+ * Create a new conversation.
114
+ *
115
+ * @param options - Conversation details (type, customer, subject, etc.).
116
+ * @returns The created conversation.
117
+ * @throws {@link SpokPayChatApiError} if the API returns an error.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const conv = await chat.conversations.create({
122
+ * type: "order_support",
123
+ * externalId: "ticket_123",
124
+ * customer: { externalId: "cust_abc", name: "João" },
125
+ * subject: "Order issue",
126
+ * metadata: { orderId: "order_456" },
127
+ * });
128
+ * ```
129
+ */
130
+ async create(options) {
131
+ const body = {
132
+ type: options.type,
133
+ };
134
+ if (options.externalId)
135
+ body.externalId = options.externalId;
136
+ if (options.subject)
137
+ body.subject = options.subject;
138
+ if (options.chargeId)
139
+ body.chargeId = options.chargeId;
140
+ if (options.customer)
141
+ body.customer = options.customer;
142
+ if (options.metadata)
143
+ body.metadata = options.metadata;
144
+ return this.request("/v1/conversations", body);
145
+ }
146
+ /**
147
+ * Get a conversation by SpokPay ID or your external ID.
148
+ *
149
+ * @param options - Lookup criteria (SpokPay ID or external ID).
150
+ * @returns The conversation object.
151
+ * @throws {@link SpokPayChatApiError} with status 404 if not found.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * const conv = await chat.conversations.get({ externalId: "ticket_123" });
156
+ * ```
157
+ */
158
+ async get(options) {
159
+ if (!options.id && !options.externalId) {
160
+ throw new Error("Either id or externalId is required");
161
+ }
162
+ const body = {};
163
+ if (options.id)
164
+ body.id = options.id;
165
+ if (options.externalId)
166
+ body.externalId = options.externalId;
167
+ return this.request("/v1/conversations/get", body);
168
+ }
169
+ /**
170
+ * Send a message from a customer.
171
+ *
172
+ * @param options - Message details (conversation, content, author info).
173
+ * @returns The created message.
174
+ * @throws {@link SpokPayChatApiError} if the conversation is closed or not found.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * const msg = await chat.conversations.sendMessage({
179
+ * conversationId: conv.id,
180
+ * content: "When will my order arrive?",
181
+ * authorExternalId: "cust_abc",
182
+ * authorDisplayName: "João",
183
+ * });
184
+ * ```
185
+ */
186
+ async sendMessage(options) {
187
+ if (!options.conversationId && !options.externalId) {
188
+ throw new Error("Either conversationId or externalId is required");
189
+ }
190
+ const body = {
191
+ content: options.content,
192
+ };
193
+ if (options.conversationId)
194
+ body.conversationId = options.conversationId;
195
+ if (options.externalId)
196
+ body.externalId = options.externalId;
197
+ if (options.authorExternalId)
198
+ body.authorExternalId = options.authorExternalId;
199
+ if (options.authorDisplayName)
200
+ body.authorDisplayName = options.authorDisplayName;
201
+ if (options.attachments)
202
+ body.attachments = options.attachments;
203
+ return this.request("/v1/conversations/messages", body);
204
+ }
205
+ /**
206
+ * List messages in a conversation.
207
+ *
208
+ * @param options - Conversation ID/externalId and pagination options.
209
+ * @returns Array of messages, newest first.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * const { messages } = await chat.conversations.listMessages({
214
+ * conversationId: conv.id,
215
+ * limit: 20,
216
+ * });
217
+ * ```
218
+ */
219
+ async listMessages(options) {
220
+ if (!options.conversationId && !options.externalId) {
221
+ throw new Error("Either conversationId or externalId is required");
222
+ }
223
+ const body = {};
224
+ if (options.conversationId)
225
+ body.conversationId = options.conversationId;
226
+ if (options.externalId)
227
+ body.externalId = options.externalId;
228
+ if (options.limit !== undefined)
229
+ body.limit = options.limit;
230
+ if (options.cursor !== undefined)
231
+ body.cursor = options.cursor;
232
+ return this.request("/v1/conversations/messages/list", body);
233
+ }
234
+ /**
235
+ * Close a conversation.
236
+ *
237
+ * @param options - The conversation ID to close.
238
+ * @returns Confirmation with the closed conversation ID and status.
239
+ * @throws {@link SpokPayChatApiError} if the conversation is already closed or not found.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * await chat.conversations.close({ id: conv.id });
244
+ * ```
245
+ */
246
+ async close(options) {
247
+ return this.request("/v1/conversations/close", { id: options.id });
248
+ }
249
+ /**
250
+ * Get a storage upload URL for file attachments.
251
+ *
252
+ * Upload your file to the returned URL with a PUT request, then include
253
+ * the resulting `storageId` in your message attachments.
254
+ *
255
+ * @param options - The conversation ID.
256
+ * @returns An upload URL.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const { uploadUrl } = await chat.conversations.getUploadUrl({
261
+ * conversationId: conv.id,
262
+ * });
263
+ *
264
+ * // Upload the file
265
+ * const res = await fetch(uploadUrl, {
266
+ * method: "POST",
267
+ * headers: { "Content-Type": file.type },
268
+ * body: file,
269
+ * });
270
+ * const { storageId } = await res.json();
271
+ *
272
+ * // Send message with attachment
273
+ * await chat.conversations.sendMessage({
274
+ * conversationId: conv.id,
275
+ * content: "Here is the screenshot",
276
+ * attachments: [{ storageId, filename: "screenshot.png", contentType: "image/png" }],
277
+ * });
278
+ * ```
279
+ */
280
+ async getUploadUrl(options) {
281
+ return this.request("/v1/conversations/upload-url", {
282
+ conversationId: options.conversationId,
283
+ });
284
+ }
285
+ async request(path, body) {
286
+ const response = await fetch(`${this.baseUrl}${path}`, {
287
+ method: "POST",
288
+ headers: {
289
+ "Content-Type": "application/json",
290
+ Authorization: `Bearer ${this.apiKey}`,
291
+ },
292
+ body: JSON.stringify(body),
293
+ });
294
+ const data = await response.json();
295
+ if (!response.ok) {
296
+ throw new SpokPayChatApiError(data.error ?? "Request failed", response.status, data);
297
+ }
298
+ return data;
299
+ }
300
+ }
@@ -0,0 +1,3 @@
1
+ export { SpokPayChat } from "./client.js";
2
+ export { SpokPayChatApiError } from "./types.js";
3
+ export type { SpokPayChatOptions, Conversation, CreateConversationOptions, GetConversationOptions, Message, SendMessageOptions, MessageAttachment, ListMessagesOptions, CloseConversationOptions, GetUploadUrlOptions, ChatWebhookPayload, WebhookMessageData, WebhookConversationData, VerifyWebhookOptions, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { SpokPayChat } from "./client.js";
2
+ export { SpokPayChatApiError } from "./types.js";
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Configuration options for the {@link SpokPayChat} client.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { SpokPayChat } from "@spokpay/chat";
7
+ *
8
+ * const chat = new SpokPayChat({
9
+ * apiKey: "spk_live_abc123...",
10
+ * baseUrl: "https://your-deployment.convex.site",
11
+ * });
12
+ * ```
13
+ */
14
+ export interface SpokPayChatOptions {
15
+ /**
16
+ * Your SpokPay API key.
17
+ * Generate keys from the Plugin page in your SpokPay store dashboard.
18
+ */
19
+ apiKey: string;
20
+ /**
21
+ * The base URL of your SpokPay Convex deployment.
22
+ * This is the HTTP Actions URL from your Convex dashboard (ends in `.convex.site`).
23
+ *
24
+ * @example "https://your-deployment.convex.site"
25
+ */
26
+ baseUrl?: string;
27
+ }
28
+ /**
29
+ * A conversation object returned by the SpokPay API.
30
+ */
31
+ export interface Conversation {
32
+ /** Unique SpokPay conversation ID. */
33
+ id: string;
34
+ /** The type of conversation (e.g., `"order_support"`, `"billing"`, `"general"`). */
35
+ type: string;
36
+ /** Current status of the conversation. */
37
+ status: "open" | "closed";
38
+ /** The storefront that created this conversation. */
39
+ storefront: "discord" | "sdk" | "dashboard";
40
+ /** Your external ID, if provided when creating the conversation. */
41
+ externalId?: string;
42
+ /** Conversation subject or title. */
43
+ subject?: string;
44
+ /** Arbitrary metadata as a JSON string, if provided. */
45
+ metadata?: string;
46
+ /** Number of messages in the conversation. */
47
+ messageCount: number;
48
+ /** Unix timestamp (ms) of the last activity. */
49
+ lastActivityAt: number;
50
+ /** Unix timestamp (ms) when the conversation was created. */
51
+ createdAt: number;
52
+ /** Unix timestamp (ms) when the conversation was closed, if closed. */
53
+ closedAt?: number;
54
+ }
55
+ /**
56
+ * Options for creating a new conversation.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const conv = await chat.conversations.create({
61
+ * type: "order_support",
62
+ * externalId: "ticket_123",
63
+ * subject: "Issue with my order",
64
+ * customer: { externalId: "cust_abc", name: "João" },
65
+ * metadata: { orderId: "order_456" },
66
+ * });
67
+ * ```
68
+ */
69
+ export interface CreateConversationOptions {
70
+ /** The type of conversation. Use any string that categorizes this conversation. */
71
+ type: string;
72
+ /**
73
+ * Your own unique identifier for this conversation.
74
+ * Must be unique per store. Useful for correlating with your system.
75
+ */
76
+ externalId?: string;
77
+ /** Conversation subject or title. */
78
+ subject?: string;
79
+ /** SpokPay charge ID to link to this conversation. */
80
+ chargeId?: string;
81
+ /** Customer information. If `externalId` is provided, the customer will be created or linked. */
82
+ customer?: {
83
+ /** Your own identifier for this customer. */
84
+ externalId?: string;
85
+ /** Customer's display name. */
86
+ name?: string;
87
+ /** Customer's email address. */
88
+ email?: string;
89
+ };
90
+ /**
91
+ * Arbitrary metadata to attach to the conversation.
92
+ * Must be less than 4KB when serialized.
93
+ */
94
+ metadata?: Record<string, unknown>;
95
+ }
96
+ /**
97
+ * Options for retrieving a conversation.
98
+ * Provide either `id` or `externalId`.
99
+ */
100
+ export interface GetConversationOptions {
101
+ /** SpokPay conversation ID. */
102
+ id?: string;
103
+ /** Your external ID provided when the conversation was created. */
104
+ externalId?: string;
105
+ }
106
+ /**
107
+ * A message in a conversation.
108
+ */
109
+ export interface Message {
110
+ /** Unique message ID. */
111
+ id: string;
112
+ /** The conversation this message belongs to. */
113
+ conversationId: string;
114
+ /** Message text content. */
115
+ content: string;
116
+ /** Who sent this message. */
117
+ authorType: "customer" | "staff" | "bot" | "ai";
118
+ /** Display name of the author. */
119
+ authorDisplayName?: string;
120
+ /** Your external ID for the author (for customer messages). */
121
+ authorExternalId?: string;
122
+ /** Whether this is a system-generated message. */
123
+ isSystemMessage: boolean;
124
+ /** Unix timestamp (ms) when the message was sent. */
125
+ createdAt: number;
126
+ /** Unix timestamp (ms) when the message was last edited. */
127
+ editedAt?: number;
128
+ }
129
+ /**
130
+ * Options for sending a message in a conversation.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * const msg = await chat.conversations.sendMessage({
135
+ * conversationId: conv.id,
136
+ * content: "When will my order arrive?",
137
+ * authorExternalId: "cust_abc",
138
+ * authorDisplayName: "João",
139
+ * });
140
+ * ```
141
+ */
142
+ export interface SendMessageOptions {
143
+ /** SpokPay conversation ID. */
144
+ conversationId?: string;
145
+ /** Your external ID for the conversation (alternative to conversationId). */
146
+ externalId?: string;
147
+ /** Message text content. */
148
+ content: string;
149
+ /** Your external ID for the message author. */
150
+ authorExternalId?: string;
151
+ /** Display name for the message author. */
152
+ authorDisplayName?: string;
153
+ /** File attachments. Use `getUploadUrl()` to upload files first. */
154
+ attachments?: MessageAttachment[];
155
+ }
156
+ /**
157
+ * A file attachment on a message.
158
+ */
159
+ export interface MessageAttachment {
160
+ /** Convex storage ID (from uploading via the upload URL). */
161
+ storageId?: string;
162
+ /** Direct URL to the file. */
163
+ url?: string;
164
+ /** Filename. */
165
+ filename: string;
166
+ /** MIME content type. */
167
+ contentType?: string;
168
+ /** File size in bytes. */
169
+ size?: number;
170
+ }
171
+ /**
172
+ * Options for listing messages in a conversation.
173
+ */
174
+ export interface ListMessagesOptions {
175
+ /** SpokPay conversation ID. */
176
+ conversationId?: string;
177
+ /** Your external ID for the conversation (alternative to conversationId). */
178
+ externalId?: string;
179
+ /** Maximum number of messages to return. Defaults to 50. */
180
+ limit?: number;
181
+ /** Cursor for pagination — pass `createdAt` of the last message to get older messages. */
182
+ cursor?: number;
183
+ }
184
+ /**
185
+ * Options for closing a conversation.
186
+ */
187
+ export interface CloseConversationOptions {
188
+ /** SpokPay conversation ID. */
189
+ id: string;
190
+ }
191
+ /**
192
+ * Options for getting an upload URL.
193
+ */
194
+ export interface GetUploadUrlOptions {
195
+ /** SpokPay conversation ID. */
196
+ conversationId: string;
197
+ }
198
+ /**
199
+ * Webhook event payload sent by SpokPay for chat events.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * const event: ChatWebhookPayload = JSON.parse(body);
204
+ *
205
+ * if (event.type === "message.created") {
206
+ * // A staff member replied to the conversation
207
+ * console.log(event.data.content);
208
+ * console.log(event.data.authorType); // "staff"
209
+ * }
210
+ *
211
+ * if (event.type === "conversation.closed") {
212
+ * // The conversation was closed by staff
213
+ * console.log(event.data.id);
214
+ * }
215
+ * ```
216
+ */
217
+ export interface ChatWebhookPayload {
218
+ /**
219
+ * The event type.
220
+ *
221
+ * - `"message.created"` — A new message was sent (staff reply).
222
+ * - `"message.updated"` — A message was edited.
223
+ * - `"message.deleted"` — A message was deleted.
224
+ * - `"conversation.closed"` — The conversation was closed.
225
+ */
226
+ type: "message.created" | "message.updated" | "message.deleted" | "conversation.closed";
227
+ /** Event data. Shape depends on the event type. */
228
+ data: WebhookMessageData | WebhookConversationData;
229
+ /** Unix timestamp (ms) when the event was created. */
230
+ createdAt: number;
231
+ }
232
+ /** Data included in message webhook events. */
233
+ export interface WebhookMessageData {
234
+ id: string;
235
+ conversationId: string;
236
+ content: string;
237
+ authorType: string;
238
+ authorDisplayName?: string;
239
+ authorExternalId?: string;
240
+ attachments: Array<{
241
+ filename: string;
242
+ contentType?: string;
243
+ size?: number;
244
+ url?: string;
245
+ }>;
246
+ createdAt: number;
247
+ }
248
+ /** Data included in conversation webhook events. */
249
+ export interface WebhookConversationData {
250
+ id: string;
251
+ type: string;
252
+ status: string;
253
+ externalId?: string;
254
+ subject?: string;
255
+ metadata?: string;
256
+ createdAt: number;
257
+ closedAt?: number;
258
+ }
259
+ /**
260
+ * Options for verifying a webhook signature.
261
+ *
262
+ * @see {@link SpokPayChat.verifyWebhookSignature}
263
+ */
264
+ export interface VerifyWebhookOptions {
265
+ /** The raw request body as a string (do NOT parse it before verifying). */
266
+ payload: string;
267
+ /** The `x-spokpay-signature` header value from the webhook request. */
268
+ signature: string;
269
+ /** Your webhook endpoint secret from the SpokPay dashboard. */
270
+ secret: string;
271
+ }
272
+ /**
273
+ * Error thrown when the SpokPay API returns a non-2xx response.
274
+ */
275
+ export declare class SpokPayChatApiError extends Error {
276
+ /** HTTP status code from the API response. */
277
+ readonly statusCode: number;
278
+ /** Raw response body from the API. */
279
+ readonly body: unknown;
280
+ constructor(message: string, statusCode: number, body?: unknown);
281
+ }
package/dist/types.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Error thrown when the SpokPay API returns a non-2xx response.
3
+ */
4
+ export class SpokPayChatApiError extends Error {
5
+ /** HTTP status code from the API response. */
6
+ statusCode;
7
+ /** Raw response body from the API. */
8
+ body;
9
+ constructor(message, statusCode, body) {
10
+ super(message);
11
+ this.name = "SpokPayChatApiError";
12
+ this.statusCode = statusCode;
13
+ this.body = body;
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@spokpay/chat",
3
+ "version": "0.1.0",
4
+ "description": "SpokPay Chat SDK — add live chat conversations to your app",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsc --watch"
20
+ },
21
+ "keywords": [
22
+ "spokpay",
23
+ "chat",
24
+ "conversations",
25
+ "support",
26
+ "sdk"
27
+ ],
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "typescript": "^5.9.0"
31
+ }
32
+ }