@dispatchtickets/sdk 0.5.0 → 0.7.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/index.cjs CHANGED
@@ -551,6 +551,66 @@ var BrandsResource = class extends BaseResource {
551
551
  getInboundEmail(brandId, domain = "inbound.dispatchtickets.com") {
552
552
  return `${brandId}@${domain}`;
553
553
  }
554
+ // ===========================================================================
555
+ // Portal Token Management
556
+ // ===========================================================================
557
+ /**
558
+ * Generate a portal token for a customer
559
+ *
560
+ * Use this for "authenticated mode" where your backend already knows who the
561
+ * customer is (they're logged into your app). Generate a portal token and
562
+ * pass it to your frontend to initialize the DispatchPortal client.
563
+ *
564
+ * @param brandId - The brand ID
565
+ * @param data - Customer email and optional name
566
+ * @returns Portal token response with JWT token and expiry
567
+ *
568
+ * @example
569
+ * ```typescript
570
+ * // On your backend (Node.js, Next.js API route, etc.)
571
+ * const { token, expiresAt } = await client.brands.generatePortalToken(
572
+ * 'br_abc123',
573
+ * {
574
+ * email: req.user.email,
575
+ * name: req.user.name,
576
+ * }
577
+ * );
578
+ *
579
+ * // Return token to your frontend
580
+ * res.json({ portalToken: token });
581
+ * ```
582
+ */
583
+ async generatePortalToken(brandId, data) {
584
+ return this._post(`/brands/${brandId}/portal/token`, data);
585
+ }
586
+ /**
587
+ * Send a magic link email to a customer
588
+ *
589
+ * Use this for "self-auth mode" where customers access the portal without
590
+ * being logged into your app. They receive an email with a link that
591
+ * authenticates them directly.
592
+ *
593
+ * @param brandId - The brand ID
594
+ * @param data - Customer email and portal URL to redirect to
595
+ * @returns Success status
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * // Customer requests access via your portal login form
600
+ * await client.brands.sendPortalMagicLink('br_abc123', {
601
+ * email: 'customer@example.com',
602
+ * portalUrl: 'https://yourapp.com/support/portal',
603
+ * });
604
+ *
605
+ * // Customer receives email with link like:
606
+ * // https://yourapp.com/support/portal?token=xyz...
607
+ *
608
+ * // Your portal page then calls DispatchPortal.verify(token)
609
+ * ```
610
+ */
611
+ async sendPortalMagicLink(brandId, data) {
612
+ return this._post(`/brands/${brandId}/portal/magic-link`, data);
613
+ }
554
614
  };
555
615
 
556
616
  // src/resources/tickets.ts
@@ -1296,6 +1356,256 @@ var DispatchTickets = class {
1296
1356
  }
1297
1357
  };
1298
1358
 
1359
+ // src/resources/portal-tickets.ts
1360
+ var PortalTicketsResource = class extends BaseResource {
1361
+ /**
1362
+ * List the customer's tickets
1363
+ *
1364
+ * Returns a paginated list of tickets belonging to the authenticated customer.
1365
+ *
1366
+ * @param filters - Optional filters and pagination
1367
+ * @param options - Request options
1368
+ * @returns Paginated ticket list
1369
+ *
1370
+ * @example
1371
+ * ```typescript
1372
+ * // List all tickets
1373
+ * const { data: tickets, pagination } = await portal.tickets.list();
1374
+ *
1375
+ * // Filter by status
1376
+ * const openTickets = await portal.tickets.list({ status: 'open' });
1377
+ *
1378
+ * // Paginate
1379
+ * const page2 = await portal.tickets.list({ cursor: pagination.nextCursor });
1380
+ * ```
1381
+ */
1382
+ async list(filters, options) {
1383
+ const query = this.buildListQuery(filters);
1384
+ return this._get("/portal/tickets", query, options);
1385
+ }
1386
+ /**
1387
+ * Iterate through all tickets with automatic pagination
1388
+ *
1389
+ * @param filters - Optional filters (cursor is managed automatically)
1390
+ * @returns Async iterator of tickets
1391
+ *
1392
+ * @example
1393
+ * ```typescript
1394
+ * for await (const ticket of portal.tickets.listAll({ status: 'open' })) {
1395
+ * console.log(ticket.title);
1396
+ * }
1397
+ * ```
1398
+ */
1399
+ async *listAll(filters) {
1400
+ let cursor;
1401
+ let hasMore = true;
1402
+ while (hasMore) {
1403
+ const response = await this.list({ ...filters, cursor });
1404
+ for (const ticket of response.data) {
1405
+ yield ticket;
1406
+ }
1407
+ cursor = response.pagination.nextCursor ?? void 0;
1408
+ hasMore = response.pagination.hasMore;
1409
+ }
1410
+ }
1411
+ /**
1412
+ * Get a ticket by ID
1413
+ *
1414
+ * Returns the ticket with its comments (excludes internal comments).
1415
+ *
1416
+ * @param ticketId - Ticket ID
1417
+ * @param options - Request options
1418
+ * @returns Ticket detail with comments
1419
+ *
1420
+ * @example
1421
+ * ```typescript
1422
+ * const ticket = await portal.tickets.get('tkt_abc123');
1423
+ * console.log(ticket.title);
1424
+ * console.log(`${ticket.comments.length} comments`);
1425
+ * ```
1426
+ */
1427
+ async get(ticketId, options) {
1428
+ return this._get(`/portal/tickets/${ticketId}`, void 0, options);
1429
+ }
1430
+ /**
1431
+ * Create a new ticket
1432
+ *
1433
+ * @param data - Ticket data
1434
+ * @param options - Request options including idempotency key
1435
+ * @returns Created ticket
1436
+ *
1437
+ * @example
1438
+ * ```typescript
1439
+ * const ticket = await portal.tickets.create({
1440
+ * title: 'Need help with billing',
1441
+ * body: 'I was charged twice for my subscription...',
1442
+ * });
1443
+ * ```
1444
+ */
1445
+ async create(data, options) {
1446
+ return this._post("/portal/tickets", data, {
1447
+ idempotencyKey: options?.idempotencyKey,
1448
+ signal: options?.signal
1449
+ });
1450
+ }
1451
+ /**
1452
+ * Add a comment to a ticket
1453
+ *
1454
+ * @param ticketId - Ticket ID
1455
+ * @param body - Comment text
1456
+ * @param options - Request options including idempotency key
1457
+ * @returns Created comment
1458
+ *
1459
+ * @example
1460
+ * ```typescript
1461
+ * const comment = await portal.tickets.addComment(
1462
+ * 'tkt_abc123',
1463
+ * 'Here is the additional information you requested...'
1464
+ * );
1465
+ * ```
1466
+ */
1467
+ async addComment(ticketId, body, options) {
1468
+ return this._post(`/portal/tickets/${ticketId}/comments`, { body }, {
1469
+ idempotencyKey: options?.idempotencyKey,
1470
+ signal: options?.signal
1471
+ });
1472
+ }
1473
+ /**
1474
+ * Build query parameters from filters
1475
+ */
1476
+ buildListQuery(filters) {
1477
+ if (!filters) return void 0;
1478
+ const query = {};
1479
+ if (filters.status) query.status = filters.status;
1480
+ if (filters.sort) query.sort = filters.sort;
1481
+ if (filters.order) query.order = filters.order;
1482
+ if (filters.limit) query.limit = filters.limit;
1483
+ if (filters.cursor) query.cursor = filters.cursor;
1484
+ return Object.keys(query).length > 0 ? query : void 0;
1485
+ }
1486
+ };
1487
+
1488
+ // src/portal.ts
1489
+ var DEFAULT_BASE_URL = "https://dispatch-tickets-api.onrender.com/v1";
1490
+ var DEFAULT_TIMEOUT = 3e4;
1491
+ var DispatchPortal = class {
1492
+ http;
1493
+ config;
1494
+ /**
1495
+ * Tickets resource for viewing and creating tickets
1496
+ *
1497
+ * @example
1498
+ * ```typescript
1499
+ * // List tickets
1500
+ * const { data: tickets } = await portal.tickets.list();
1501
+ *
1502
+ * // Create a ticket
1503
+ * const ticket = await portal.tickets.create({
1504
+ * title: 'Need help',
1505
+ * body: 'Something is broken...',
1506
+ * });
1507
+ *
1508
+ * // Add a comment
1509
+ * await portal.tickets.addComment(ticket.id, 'Here is more info...');
1510
+ * ```
1511
+ */
1512
+ tickets;
1513
+ /**
1514
+ * Create a new Dispatch Portal client
1515
+ *
1516
+ * @param config - Portal client configuration
1517
+ * @throws Error if token is not provided
1518
+ */
1519
+ constructor(config) {
1520
+ if (!config.token) {
1521
+ throw new Error("Portal token is required");
1522
+ }
1523
+ this.config = config;
1524
+ const httpConfig = {
1525
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
1526
+ apiKey: config.token,
1527
+ // HttpClient uses this as Bearer token
1528
+ timeout: config.timeout ?? DEFAULT_TIMEOUT,
1529
+ maxRetries: 2,
1530
+ // Fewer retries for portal (end-user experience)
1531
+ fetch: config.fetch
1532
+ };
1533
+ this.http = new HttpClient(httpConfig);
1534
+ this.tickets = new PortalTicketsResource(this.http);
1535
+ }
1536
+ /**
1537
+ * Verify a magic link token and get a portal token
1538
+ *
1539
+ * Call this when the customer clicks the magic link and lands on your portal.
1540
+ * The magic link contains a short-lived token that can be exchanged for a
1541
+ * longer-lived portal token.
1542
+ *
1543
+ * @param magicLinkToken - The token from the magic link URL
1544
+ * @param baseUrl - Optional API base URL
1545
+ * @param fetchFn - Optional custom fetch function (for testing)
1546
+ * @returns Portal token response
1547
+ *
1548
+ * @example
1549
+ * ```typescript
1550
+ * // Customer lands on: https://yourapp.com/portal?token=xyz
1551
+ * const urlToken = new URLSearchParams(window.location.search).get('token');
1552
+ *
1553
+ * const { token, email, name } = await DispatchPortal.verify(urlToken);
1554
+ *
1555
+ * // Store token and create client
1556
+ * localStorage.setItem('portalToken', token);
1557
+ * const portal = new DispatchPortal({ token });
1558
+ * ```
1559
+ */
1560
+ static async verify(magicLinkToken, baseUrl = DEFAULT_BASE_URL, fetchFn = fetch) {
1561
+ const url = `${baseUrl}/portal/verify`;
1562
+ const response = await fetchFn(url, {
1563
+ method: "POST",
1564
+ headers: {
1565
+ "Content-Type": "application/json",
1566
+ "Accept": "application/json"
1567
+ },
1568
+ body: JSON.stringify({ token: magicLinkToken })
1569
+ });
1570
+ if (!response.ok) {
1571
+ const errorData = await response.json().catch(() => ({}));
1572
+ throw new Error(errorData.message || "Failed to verify magic link token");
1573
+ }
1574
+ return response.json();
1575
+ }
1576
+ /**
1577
+ * Refresh the current portal token
1578
+ *
1579
+ * Call this before the token expires to get a new token with extended expiry.
1580
+ * The new token will have the same customer context.
1581
+ *
1582
+ * @returns New portal token response
1583
+ *
1584
+ * @example
1585
+ * ```typescript
1586
+ * // Check if token is close to expiry and refresh
1587
+ * const { token: newToken, expiresAt } = await portal.refresh();
1588
+ *
1589
+ * // Create new client with refreshed token
1590
+ * const newPortal = new DispatchPortal({ token: newToken });
1591
+ * ```
1592
+ */
1593
+ async refresh() {
1594
+ return this.http.request({
1595
+ method: "POST",
1596
+ path: "/portal/refresh"
1597
+ });
1598
+ }
1599
+ /**
1600
+ * Get the current token
1601
+ *
1602
+ * Useful for storing/passing the token.
1603
+ */
1604
+ get token() {
1605
+ return this.config.token;
1606
+ }
1607
+ };
1608
+
1299
1609
  // src/types/events.ts
1300
1610
  function isTicketCreatedEvent(event) {
1301
1611
  return event.event === "ticket.created";
@@ -1338,6 +1648,7 @@ async function collectAll(iterable) {
1338
1648
 
1339
1649
  exports.AuthenticationError = AuthenticationError;
1340
1650
  exports.ConflictError = ConflictError;
1651
+ exports.DispatchPortal = DispatchPortal;
1341
1652
  exports.DispatchTickets = DispatchTickets;
1342
1653
  exports.DispatchTicketsError = DispatchTicketsError;
1343
1654
  exports.NetworkError = NetworkError;