@medalsocial/sdk 1.2.1 → 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 +131 -1
- package/dist/openapi/medal-social.openapi.json +1257 -86
- package/dist/src/index.d.mts +437 -4
- package/dist/src/index.d.ts +437 -4
- package/dist/src/index.js +224 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/index.mjs +218 -7
- package/dist/src/index.mjs.map +1 -1
- package/dist/src/openapi.generated.d.mts +674 -0
- package/dist/src/openapi.generated.d.ts +674 -0
- package/dist/src/openapi.generated.js.map +1 -1
- package/openapi/medal-social.openapi.yaml +710 -0
- package/package.json +14 -14
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:
|