@spokpay/chat 0.1.0 → 0.2.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/dist/client.d.ts CHANGED
@@ -30,22 +30,41 @@ import type { SpokPayChatOptions, CreateConversationOptions, Conversation, GetCo
30
30
  * });
31
31
  * ```
32
32
  *
33
- * @example Receive staff replies via webhook
33
+ * @example Convex integration reactive chat with webhooks
34
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!,
35
+ * // convex/http.ts Receive SpokPay chat webhooks
36
+ * import { SpokPayChat, type ChatWebhookPayload } from "@spokpay/chat";
37
+ *
38
+ * http.route({
39
+ * path: "/webhooks/spokpay-chat",
40
+ * method: "POST",
41
+ * handler: httpAction(async (ctx, req) => {
42
+ * const body = await req.text();
43
+ * const signature = req.headers.get("x-spokpay-signature")!;
44
+ *
45
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
46
+ * payload: body,
47
+ * signature,
48
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
49
+ * });
50
+ * if (!isValid) return new Response("Invalid signature", { status: 401 });
51
+ *
52
+ * const event: ChatWebhookPayload = JSON.parse(body);
53
+ * if (event.type === "message.created") {
54
+ * await ctx.runMutation(internal.chat.handleStaffReply, {
55
+ * conversationId: event.data.conversationId,
56
+ * content: event.data.content,
57
+ * authorName: event.data.authorDisplayName,
58
+ * });
59
+ * }
60
+ * return new Response("OK");
61
+ * }),
41
62
  * });
42
63
  *
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
- * }
64
+ * // React component — auto-updates when webhook triggers the mutation
65
+ * function ChatPage({ conversationId }: { conversationId: string }) {
66
+ * const messages = useQuery(api.chat.listMessages, { conversationId });
67
+ * // messages reactively updates when staff replies via the webhook
49
68
  * }
50
69
  * ```
51
70
  */
@@ -54,8 +73,45 @@ export declare class SpokPayChat {
54
73
  private readonly baseUrl;
55
74
  /**
56
75
  * Client for managing conversations and messages.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * // Create a conversation
80
+ * const conv = await chat.conversations.create({
81
+ * type: "order_support",
82
+ * customer: { externalId: "cust_123", name: "João" },
83
+ * subject: "Help with my order",
84
+ * });
85
+ *
86
+ * // Send a message
87
+ * await chat.conversations.sendMessage({
88
+ * conversationId: conv.id,
89
+ * content: "When will my order arrive?",
90
+ * });
91
+ *
92
+ * // List messages
93
+ * const { messages } = await chat.conversations.listMessages({
94
+ * conversationId: conv.id,
95
+ * });
96
+ *
97
+ * // Close when resolved
98
+ * await chat.conversations.close({ id: conv.id });
99
+ * ```
57
100
  */
58
101
  readonly conversations: ConversationsClient;
102
+ /**
103
+ * Creates a new SpokPay Chat client.
104
+ *
105
+ * @param options - Client configuration (API key and base URL).
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const chat = new SpokPayChat({
110
+ * apiKey: process.env.SPOKPAY_API_KEY!,
111
+ * baseUrl: process.env.SPOKPAY_BASE_URL!,
112
+ * });
113
+ * ```
114
+ */
59
115
  constructor(options: SpokPayChatOptions);
60
116
  /**
61
117
  * Verify a webhook signature to ensure the request came from SpokPay.
@@ -64,15 +120,40 @@ export declare class SpokPayChat {
64
120
  * Works in all JavaScript runtimes: Node.js 18+, Deno, Cloudflare Workers,
65
121
  * Vercel Edge, and Convex.
66
122
  *
123
+ * **Important:** Always verify signatures before processing webhook payloads.
124
+ * Use the raw request body string — do not parse it before verification.
125
+ *
67
126
  * @param options - The raw payload, signature header, and your webhook secret.
68
127
  * @returns `true` if the signature is valid, `false` otherwise.
69
128
  *
70
- * @example
129
+ * @example Node.js / Express
130
+ * ```ts
131
+ * app.post("/webhooks/spokpay-chat", express.raw({ type: "*\/*" }), async (req, res) => {
132
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
133
+ * payload: req.body.toString(),
134
+ * signature: req.headers["x-spokpay-signature"] as string,
135
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
136
+ * });
137
+ *
138
+ * if (!isValid) return res.status(401).send("Invalid signature");
139
+ *
140
+ * const event = JSON.parse(req.body.toString());
141
+ * // Handle event...
142
+ * res.sendStatus(200);
143
+ * });
144
+ * ```
145
+ *
146
+ * @example Convex httpAction
71
147
  * ```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!,
148
+ * const handler = httpAction(async (ctx, req) => {
149
+ * const body = await req.text();
150
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
151
+ * payload: body,
152
+ * signature: req.headers.get("x-spokpay-signature")!,
153
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
154
+ * });
155
+ * if (!isValid) return new Response("Invalid", { status: 401 });
156
+ * // Process the webhook...
76
157
  * });
77
158
  * ```
78
159
  */
package/dist/client.js CHANGED
@@ -31,22 +31,41 @@ const DEFAULT_BASE_URL = "https://your-convex-deployment.convex.site";
31
31
  * });
32
32
  * ```
33
33
  *
34
- * @example Receive staff replies via webhook
34
+ * @example Convex integration reactive chat with webhooks
35
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!,
36
+ * // convex/http.ts Receive SpokPay chat webhooks
37
+ * import { SpokPayChat, type ChatWebhookPayload } from "@spokpay/chat";
38
+ *
39
+ * http.route({
40
+ * path: "/webhooks/spokpay-chat",
41
+ * method: "POST",
42
+ * handler: httpAction(async (ctx, req) => {
43
+ * const body = await req.text();
44
+ * const signature = req.headers.get("x-spokpay-signature")!;
45
+ *
46
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
47
+ * payload: body,
48
+ * signature,
49
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
50
+ * });
51
+ * if (!isValid) return new Response("Invalid signature", { status: 401 });
52
+ *
53
+ * const event: ChatWebhookPayload = JSON.parse(body);
54
+ * if (event.type === "message.created") {
55
+ * await ctx.runMutation(internal.chat.handleStaffReply, {
56
+ * conversationId: event.data.conversationId,
57
+ * content: event.data.content,
58
+ * authorName: event.data.authorDisplayName,
59
+ * });
60
+ * }
61
+ * return new Response("OK");
62
+ * }),
42
63
  * });
43
64
  *
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
- * }
65
+ * // React component — auto-updates when webhook triggers the mutation
66
+ * function ChatPage({ conversationId }: { conversationId: string }) {
67
+ * const messages = useQuery(api.chat.listMessages, { conversationId });
68
+ * // messages reactively updates when staff replies via the webhook
50
69
  * }
51
70
  * ```
52
71
  */
@@ -55,8 +74,45 @@ export class SpokPayChat {
55
74
  baseUrl;
56
75
  /**
57
76
  * Client for managing conversations and messages.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // Create a conversation
81
+ * const conv = await chat.conversations.create({
82
+ * type: "order_support",
83
+ * customer: { externalId: "cust_123", name: "João" },
84
+ * subject: "Help with my order",
85
+ * });
86
+ *
87
+ * // Send a message
88
+ * await chat.conversations.sendMessage({
89
+ * conversationId: conv.id,
90
+ * content: "When will my order arrive?",
91
+ * });
92
+ *
93
+ * // List messages
94
+ * const { messages } = await chat.conversations.listMessages({
95
+ * conversationId: conv.id,
96
+ * });
97
+ *
98
+ * // Close when resolved
99
+ * await chat.conversations.close({ id: conv.id });
100
+ * ```
58
101
  */
59
102
  conversations;
103
+ /**
104
+ * Creates a new SpokPay Chat client.
105
+ *
106
+ * @param options - Client configuration (API key and base URL).
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const chat = new SpokPayChat({
111
+ * apiKey: process.env.SPOKPAY_API_KEY!,
112
+ * baseUrl: process.env.SPOKPAY_BASE_URL!,
113
+ * });
114
+ * ```
115
+ */
60
116
  constructor(options) {
61
117
  this.apiKey = options.apiKey;
62
118
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -69,15 +125,40 @@ export class SpokPayChat {
69
125
  * Works in all JavaScript runtimes: Node.js 18+, Deno, Cloudflare Workers,
70
126
  * Vercel Edge, and Convex.
71
127
  *
128
+ * **Important:** Always verify signatures before processing webhook payloads.
129
+ * Use the raw request body string — do not parse it before verification.
130
+ *
72
131
  * @param options - The raw payload, signature header, and your webhook secret.
73
132
  * @returns `true` if the signature is valid, `false` otherwise.
74
133
  *
75
- * @example
134
+ * @example Node.js / Express
135
+ * ```ts
136
+ * app.post("/webhooks/spokpay-chat", express.raw({ type: "*\/*" }), async (req, res) => {
137
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
138
+ * payload: req.body.toString(),
139
+ * signature: req.headers["x-spokpay-signature"] as string,
140
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
141
+ * });
142
+ *
143
+ * if (!isValid) return res.status(401).send("Invalid signature");
144
+ *
145
+ * const event = JSON.parse(req.body.toString());
146
+ * // Handle event...
147
+ * res.sendStatus(200);
148
+ * });
149
+ * ```
150
+ *
151
+ * @example Convex httpAction
76
152
  * ```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!,
153
+ * const handler = httpAction(async (ctx, req) => {
154
+ * const body = await req.text();
155
+ * const isValid = await SpokPayChat.verifyWebhookSignature({
156
+ * payload: body,
157
+ * signature: req.headers.get("x-spokpay-signature")!,
158
+ * secret: process.env.SPOKPAY_WEBHOOK_SECRET!,
159
+ * });
160
+ * if (!isValid) return new Response("Invalid", { status: 401 });
161
+ * // Process the webhook...
81
162
  * });
82
163
  * ```
83
164
  */
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { SpokPayChat } from "./client.js";
2
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";
3
+ export type { SpokPayChatOptions, Conversation, CreateConversationOptions, GetConversationOptions, Message, SendMessageOptions, MessageAttachment, ListMessagesOptions, CloseConversationOptions, GetUploadUrlOptions, ChatWebhookPayload, WebhookMessageData, WebhookConversationData, WebhookItemDeliveredData, WebhookItemRefundedData, VerifyWebhookOptions, } from "./types.js";
package/dist/types.d.ts CHANGED
@@ -1,19 +1,33 @@
1
1
  /**
2
2
  * Configuration options for the {@link SpokPayChat} client.
3
3
  *
4
+ * The API key prefix determines the environment:
5
+ * - `spk_live_*` — Production (real conversations routed to staff)
6
+ * - `spk_test_*` — Development/Sandbox (test conversations, no notifications)
7
+ *
4
8
  * @example
5
9
  * ```ts
6
10
  * import { SpokPayChat } from "@spokpay/chat";
7
11
  *
12
+ * // Production
8
13
  * const chat = new SpokPayChat({
9
14
  * apiKey: "spk_live_abc123...",
10
15
  * baseUrl: "https://your-deployment.convex.site",
11
16
  * });
17
+ *
18
+ * // Sandbox / Testing
19
+ * const chatTest = new SpokPayChat({
20
+ * apiKey: "spk_test_abc123...",
21
+ * baseUrl: "https://your-deployment.convex.site",
22
+ * });
12
23
  * ```
13
24
  */
14
25
  export interface SpokPayChatOptions {
15
26
  /**
16
27
  * Your SpokPay API key.
28
+ * - `spk_live_*` keys target **production** (real conversations routed to staff).
29
+ * - `spk_test_*` keys target **SpokPay sandbox** (test conversations, no notifications).
30
+ *
17
31
  * Generate keys from the Plugin page in your SpokPay store dashboard.
18
32
  */
19
33
  apiKey: string;
@@ -222,40 +236,163 @@ export interface ChatWebhookPayload {
222
236
  * - `"message.updated"` — A message was edited.
223
237
  * - `"message.deleted"` — A message was deleted.
224
238
  * - `"conversation.closed"` — The conversation was closed.
239
+ * - `"item.delivered"` — An order item was delivered by staff.
240
+ * - `"item.refunded"` — An order item was refunded by staff.
225
241
  */
226
- type: "message.created" | "message.updated" | "message.deleted" | "conversation.closed";
242
+ type: "message.created" | "message.updated" | "message.deleted" | "conversation.closed" | "item.delivered" | "item.refunded";
227
243
  /** Event data. Shape depends on the event type. */
228
- data: WebhookMessageData | WebhookConversationData;
244
+ data: WebhookMessageData | WebhookConversationData | WebhookItemDeliveredData | WebhookItemRefundedData;
229
245
  /** Unix timestamp (ms) when the event was created. */
230
246
  createdAt: number;
231
247
  }
232
- /** Data included in message webhook events. */
248
+ /**
249
+ * Data included in message webhook events (`message.created`, `message.updated`, `message.deleted`).
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * const event: ChatWebhookPayload = JSON.parse(body);
254
+ * if (event.type === "message.created") {
255
+ * const msg = event.data as WebhookMessageData;
256
+ * console.log(`${msg.authorDisplayName}: ${msg.content}`);
257
+ * }
258
+ * ```
259
+ */
233
260
  export interface WebhookMessageData {
261
+ /** Unique message ID. */
234
262
  id: string;
263
+ /** The conversation this message belongs to. */
235
264
  conversationId: string;
265
+ /** Message text content. */
236
266
  content: string;
267
+ /** Who sent this message (e.g., `"staff"`, `"ai"`, `"bot"`). */
237
268
  authorType: string;
269
+ /** Display name of the author. */
238
270
  authorDisplayName?: string;
271
+ /** Your external ID for the author, if provided. */
239
272
  authorExternalId?: string;
273
+ /** File attachments included with the message. */
240
274
  attachments: Array<{
275
+ /** Filename. */
241
276
  filename: string;
277
+ /** MIME content type. */
242
278
  contentType?: string;
279
+ /** File size in bytes. */
243
280
  size?: number;
281
+ /** Direct URL to the file. */
244
282
  url?: string;
245
283
  }>;
284
+ /** Unix timestamp (ms) when the message was sent. */
246
285
  createdAt: number;
247
286
  }
248
- /** Data included in conversation webhook events. */
287
+ /**
288
+ * Data included in conversation webhook events (`conversation.closed`).
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * const event: ChatWebhookPayload = JSON.parse(body);
293
+ * if (event.type === "conversation.closed") {
294
+ * const conv = event.data as WebhookConversationData;
295
+ * console.log(`Conversation ${conv.id} closed at ${new Date(conv.closedAt!)}`);
296
+ * }
297
+ * ```
298
+ */
249
299
  export interface WebhookConversationData {
300
+ /** Unique SpokPay conversation ID. */
250
301
  id: string;
302
+ /** The type of conversation (e.g., `"order_support"`, `"general"`). */
251
303
  type: string;
304
+ /** Current status of the conversation (e.g., `"closed"`). */
252
305
  status: string;
306
+ /** Your external ID, if provided when creating the conversation. */
253
307
  externalId?: string;
308
+ /** Conversation subject or title. */
254
309
  subject?: string;
310
+ /** Arbitrary metadata as a JSON string, if provided. */
255
311
  metadata?: string;
312
+ /** Unix timestamp (ms) when the conversation was created. */
256
313
  createdAt: number;
314
+ /** Unix timestamp (ms) when the conversation was closed. */
257
315
  closedAt?: number;
258
316
  }
317
+ /**
318
+ * Data included in `item.delivered` webhook events.
319
+ *
320
+ * Fired when a staff member marks an order item as delivered from the SpokPay dashboard.
321
+ *
322
+ * @example
323
+ * ```ts
324
+ * const event: ChatWebhookPayload = JSON.parse(body);
325
+ * if (event.type === "item.delivered") {
326
+ * const data = event.data as WebhookItemDeliveredData;
327
+ * console.log(`${data.item.name} delivered by ${data.deliveredBy}`);
328
+ * }
329
+ * ```
330
+ */
331
+ export interface WebhookItemDeliveredData {
332
+ /** SpokPay conversation ID. */
333
+ conversationId: string;
334
+ /** Your external ID for the conversation. */
335
+ externalId?: string;
336
+ /** The delivered item. */
337
+ item: {
338
+ /** Your external ID for this product (from charge items). */
339
+ externalId?: string;
340
+ /** Product name. */
341
+ name: string;
342
+ /** Quantity delivered. */
343
+ quantity: number;
344
+ /** Delivery proof image, if provided. */
345
+ deliveryProof?: {
346
+ /** Direct URL to the proof image. */
347
+ url: string;
348
+ /** Filename. */
349
+ filename: string;
350
+ /** MIME content type. */
351
+ contentType: string;
352
+ };
353
+ };
354
+ /** Display name of the staff member who delivered. */
355
+ deliveredBy: string;
356
+ /** Unix timestamp (ms) when the item was delivered. */
357
+ deliveredAt: number;
358
+ }
359
+ /**
360
+ * Data included in `item.refunded` webhook events.
361
+ *
362
+ * Fired when a staff member refunds an order item from the SpokPay dashboard.
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * const event: ChatWebhookPayload = JSON.parse(body);
367
+ * if (event.type === "item.refunded") {
368
+ * const data = event.data as WebhookItemRefundedData;
369
+ * console.log(`${data.item.name} refunded: ${data.item.refundReason}`);
370
+ * }
371
+ * ```
372
+ */
373
+ export interface WebhookItemRefundedData {
374
+ /** SpokPay conversation ID. */
375
+ conversationId: string;
376
+ /** Your external ID for the conversation. */
377
+ externalId?: string;
378
+ /** The refunded item. */
379
+ item: {
380
+ /** Your external ID for this product (from charge items). */
381
+ externalId?: string;
382
+ /** Product name. */
383
+ name: string;
384
+ /** Quantity refunded. */
385
+ quantity: number;
386
+ /** Refund amount. */
387
+ refundAmount?: number;
388
+ /** Reason for the refund. */
389
+ refundReason?: string;
390
+ };
391
+ /** Display name of the staff member who processed the refund. */
392
+ refundedBy: string;
393
+ /** Unix timestamp (ms) when the item was refunded. */
394
+ refundedAt: number;
395
+ }
259
396
  /**
260
397
  * Options for verifying a webhook signature.
261
398
  *
@@ -270,7 +407,22 @@ export interface VerifyWebhookOptions {
270
407
  secret: string;
271
408
  }
272
409
  /**
273
- * Error thrown when the SpokPay API returns a non-2xx response.
410
+ * Error thrown when the SpokPay Chat API returns a non-2xx response.
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * import { SpokPayChatApiError } from "@spokpay/chat";
415
+ *
416
+ * try {
417
+ * await chat.conversations.get({ id: "invalid_id" });
418
+ * } catch (error) {
419
+ * if (error instanceof SpokPayChatApiError) {
420
+ * console.error(error.message); // "Conversation not found"
421
+ * console.error(error.statusCode); // 404
422
+ * console.error(error.body); // Raw API response body
423
+ * }
424
+ * }
425
+ * ```
274
426
  */
275
427
  export declare class SpokPayChatApiError extends Error {
276
428
  /** HTTP status code from the API response. */
package/dist/types.js CHANGED
@@ -1,5 +1,20 @@
1
1
  /**
2
- * Error thrown when the SpokPay API returns a non-2xx response.
2
+ * Error thrown when the SpokPay Chat API returns a non-2xx response.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { SpokPayChatApiError } from "@spokpay/chat";
7
+ *
8
+ * try {
9
+ * await chat.conversations.get({ id: "invalid_id" });
10
+ * } catch (error) {
11
+ * if (error instanceof SpokPayChatApiError) {
12
+ * console.error(error.message); // "Conversation not found"
13
+ * console.error(error.statusCode); // 404
14
+ * console.error(error.body); // Raw API response body
15
+ * }
16
+ * }
17
+ * ```
3
18
  */
4
19
  export class SpokPayChatApiError extends Error {
5
20
  /** HTTP status code from the API response. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spokpay/chat",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "SpokPay Chat SDK — add live chat conversations to your app",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",