@medalsocial/sdk 1.2.0 → 1.3.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Medal Social SDK
2
2
 
3
- TypeScript SDK for the [Medal Social](https://medalsocial.com) API. Manage posts, emails, contacts, deals, and GDPR compliance programmatically.
3
+ TypeScript SDK for the [Medal Social](https://medalsocial.com) API. Manage posts, emails, contacts, deals, helpdesk conversations, webhooks, and GDPR compliance programmatically.
4
4
 
5
5
  ## Install
6
6
 
@@ -236,6 +236,68 @@ await medal.gdpr.cookieConsent({
236
236
  });
237
237
  ```
238
238
 
239
+ ### Helpdesk
240
+
241
+ ```ts
242
+ // List/search conversations
243
+ const conversations = await medal.helpdesk.conversations.list({
244
+ status: 'open', // 'open' | 'snoozed' | 'closed'
245
+ assignee_user_id: 'user_1',
246
+ requester: 'jane@example.com', // match visitor name/email
247
+ query: 'refund', // free-text search
248
+ channels: ['widget', 'whatsapp'], // channel filter
249
+ limit: 50,
250
+ });
251
+
252
+ // Read one conversation + its messages
253
+ const { data: conversation } = await medal.helpdesk.conversations.get('conv_id');
254
+ const messages = await medal.helpdesk.conversations.messages('conv_id', { limit: 50 });
255
+
256
+ // Assign / snooze / close
257
+ await medal.helpdesk.conversations.update('conv_id', { assignee_user_id: 'user_1' });
258
+ await medal.helpdesk.conversations.update('conv_id', { status: 'closed', assignee_user_id: null });
259
+
260
+ // Reply as an operator (see "Helpdesk bridge" below for idempotency)
261
+ const { data: reply } = await medal.helpdesk.replies.create(
262
+ {
263
+ conversation_id: 'conv_id',
264
+ body: 'Thanks for reaching out — on it!',
265
+ author_name: 'Support Bot', // optional display name
266
+ message_type: 'chat', // or 'note' for an internal note
267
+ },
268
+ { idempotencyKey: crypto.randomUUID() },
269
+ );
270
+ ```
271
+
272
+ ### Webhooks
273
+
274
+ ```ts
275
+ // Create an endpoint. The signing secret is returned EXACTLY ONCE — store it
276
+ // securely immediately; you cannot retrieve it again.
277
+ const { data: endpoint } = await medal.webhooks.create(
278
+ {
279
+ name: 'Helpdesk bridge',
280
+ url: 'https://example.com/medal/webhook', // must be https
281
+ event_types: ['helpdesk.message_received', 'helpdesk.conversation_status_changed'],
282
+ channels: ['widget'], // optional channel filter
283
+ },
284
+ { idempotencyKey: crypto.randomUUID() },
285
+ );
286
+ console.log(endpoint.secret); // whsec_… — shown only in this response
287
+
288
+ // Manage endpoints
289
+ const { data: endpoints } = await medal.webhooks.list();
290
+ const { data: one } = await medal.webhooks.get(endpoint.id);
291
+ await medal.webhooks.update(endpoint.id, { enabled: false });
292
+ await medal.webhooks.delete(endpoint.id);
293
+
294
+ // Observe deliveries + send a signed test event
295
+ const { data: deliveries } = await medal.webhooks.deliveries(endpoint.id, { limit: 20 });
296
+ await medal.webhooks.test(endpoint.id); // queues a 'test.ping' delivery
297
+ ```
298
+
299
+ Failed deliveries retry with exponential backoff (up to 6 attempts) before being dead-lettered.
300
+
239
301
  ### Workspaces
240
302
 
241
303
  ```ts
@@ -243,6 +305,74 @@ const { data: workspaces } = await medal.workspaces.list();
243
305
  console.log(workspaces); // [{ id, name, slug }]
244
306
  ```
245
307
 
308
+ ## Helpdesk bridge
309
+
310
+ Build a two-way bridge: receive helpdesk events on a webhook, and reply through the API.
311
+
312
+ Every delivery is signed. The `X-Medal-Signature` header carries `sha256=<base64(HMAC-SHA256("{timestamp}.{rawBody}", secret))>`, where `timestamp` is the `X-Medal-Timestamp` header (Unix ms). Use `verifyWebhookSignature` to authenticate the delivery and get a fully typed event back — it recomputes the HMAC with Web Crypto (works in Node.js 18+, Deno, Bun, Cloudflare Workers) and rejects stale timestamps (default tolerance 5 minutes).
313
+
314
+ ```ts
315
+ import { Medal, verifyWebhookSignature, WebhookVerificationError } from '@medalsocial/sdk';
316
+
317
+ const medal = new Medal(process.env.MEDAL_API_KEY);
318
+
319
+ // Example: a fetch-style handler (Cloudflare Workers, Hono, Next.js route, …).
320
+ // IMPORTANT: verify against the RAW body string — do not JSON.parse first.
321
+ export async function handleWebhook(request: Request): Promise<Response> {
322
+ const payload = await request.text();
323
+
324
+ let event;
325
+ try {
326
+ event = await verifyWebhookSignature({
327
+ payload,
328
+ timestamp: request.headers.get('X-Medal-Timestamp') ?? '',
329
+ signature: request.headers.get('X-Medal-Signature') ?? '',
330
+ secret: process.env.MEDAL_WEBHOOK_SECRET, // the whsec_… from webhooks.create
331
+ });
332
+ } catch (err) {
333
+ if (err instanceof WebhookVerificationError) {
334
+ return new Response(`Invalid webhook: ${err.code}`, { status: 401 });
335
+ }
336
+ throw err;
337
+ }
338
+
339
+ switch (event.type) {
340
+ case 'helpdesk.message_received': {
341
+ const { conversation, message } = event.data;
342
+ // Reply with an idempotency key so retried deliveries never double-post.
343
+ // The X-Medal-Delivery-Id header (== event.id) is a perfect key.
344
+ await medal.helpdesk.replies.create(
345
+ {
346
+ conversation_id: conversation.id,
347
+ body: `Thanks! We received: "${message.body}"`,
348
+ author_name: 'Bridge Bot',
349
+ },
350
+ { idempotencyKey: `reply:${event.id}` },
351
+ );
352
+ break;
353
+ }
354
+ case 'helpdesk.conversation_status_changed':
355
+ console.log(event.data.previousStatus, '→', event.data.status);
356
+ break;
357
+ case 'helpdesk.conversation_assigned':
358
+ console.log('assigned to', event.data.assigneeUserId);
359
+ break;
360
+ case 'test.ping':
361
+ break; // sent by medal.webhooks.test()
362
+ }
363
+
364
+ return new Response('ok', { status: 200 }); // 2xx acknowledges the delivery
365
+ }
366
+ ```
367
+
368
+ Event types: `helpdesk.conversation_created`, `helpdesk.conversation_assigned`, `helpdesk.conversation_status_changed`, `helpdesk.message_received`, `helpdesk.message_sent`, `helpdesk.message_delivery_updated`, and `test.ping`. All are discriminated on `event.type` — TypeScript narrows `event.data` automatically in a `switch`.
369
+
370
+ Notes:
371
+
372
+ - **Idempotency**: deliveries are retried on failure, so make your handler idempotent. Deduplicate on `event.id` (also sent as the `X-Medal-Delivery-Id` and `Idempotency-Key` request headers). When replying via `medal.helpdesk.replies.create`, always pass an `idempotencyKey` — it is required for capability-scoped tokens.
373
+ - **Respond fast**: return a 2xx within 10 seconds; do slow work asynchronously.
374
+ - **Secret handling**: the endpoint secret is returned only by `webhooks.create`. If lost, delete the endpoint and create a new one.
375
+
246
376
  ## Error Handling
247
377
 
248
378
  All API errors throw `MedalApiError` with structured error details: