@dispatchtickets/sdk 0.3.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
@@ -7,33 +7,45 @@ var DispatchTicketsError = class extends Error {
7
7
  code;
8
8
  statusCode;
9
9
  details;
10
- constructor(message, code, statusCode, details) {
10
+ /** Request ID for debugging with support */
11
+ requestId;
12
+ constructor(message, code, statusCode, details, requestId) {
11
13
  super(message);
12
14
  this.name = "DispatchTicketsError";
13
15
  this.code = code;
14
16
  this.statusCode = statusCode;
15
17
  this.details = details;
18
+ this.requestId = requestId;
16
19
  Object.setPrototypeOf(this, new.target.prototype);
17
20
  }
18
21
  };
19
22
  var AuthenticationError = class extends DispatchTicketsError {
20
- constructor(message = "Invalid or missing API key") {
21
- super(message, "authentication_error", 401);
23
+ constructor(message = "Invalid or missing API key", requestId) {
24
+ super(message, "authentication_error", 401, void 0, requestId);
22
25
  this.name = "AuthenticationError";
23
26
  }
24
27
  };
25
28
  var RateLimitError = class extends DispatchTicketsError {
26
29
  retryAfter;
27
- constructor(message = "Rate limit exceeded", retryAfter) {
28
- super(message, "rate_limit_error", 429);
30
+ /** Rate limit ceiling */
31
+ limit;
32
+ /** Remaining requests in current window */
33
+ remaining;
34
+ /** Unix timestamp when rate limit resets */
35
+ reset;
36
+ constructor(message = "Rate limit exceeded", retryAfter, requestId, rateLimitInfo) {
37
+ super(message, "rate_limit_error", 429, void 0, requestId);
29
38
  this.name = "RateLimitError";
30
39
  this.retryAfter = retryAfter;
40
+ this.limit = rateLimitInfo?.limit;
41
+ this.remaining = rateLimitInfo?.remaining;
42
+ this.reset = rateLimitInfo?.reset;
31
43
  }
32
44
  };
33
45
  var ValidationError = class extends DispatchTicketsError {
34
46
  errors;
35
- constructor(message = "Validation failed", errors) {
36
- super(message, "validation_error", 400, { errors });
47
+ constructor(message = "Validation failed", errors, requestId) {
48
+ super(message, "validation_error", 400, { errors }, requestId);
37
49
  this.name = "ValidationError";
38
50
  this.errors = errors;
39
51
  }
@@ -41,22 +53,22 @@ var ValidationError = class extends DispatchTicketsError {
41
53
  var NotFoundError = class extends DispatchTicketsError {
42
54
  resourceType;
43
55
  resourceId;
44
- constructor(message = "Resource not found", resourceType, resourceId) {
45
- super(message, "not_found", 404, { resourceType, resourceId });
56
+ constructor(message = "Resource not found", resourceType, resourceId, requestId) {
57
+ super(message, "not_found", 404, { resourceType, resourceId }, requestId);
46
58
  this.name = "NotFoundError";
47
59
  this.resourceType = resourceType;
48
60
  this.resourceId = resourceId;
49
61
  }
50
62
  };
51
63
  var ConflictError = class extends DispatchTicketsError {
52
- constructor(message = "Resource conflict") {
53
- super(message, "conflict", 409);
64
+ constructor(message = "Resource conflict", requestId) {
65
+ super(message, "conflict", 409, void 0, requestId);
54
66
  this.name = "ConflictError";
55
67
  }
56
68
  };
57
69
  var ServerError = class extends DispatchTicketsError {
58
- constructor(message = "Internal server error", statusCode = 500) {
59
- super(message, "server_error", statusCode);
70
+ constructor(message = "Internal server error", statusCode = 500, requestId) {
71
+ super(message, "server_error", statusCode, void 0, requestId);
60
72
  this.name = "ServerError";
61
73
  }
62
74
  };
@@ -72,14 +84,68 @@ var NetworkError = class extends DispatchTicketsError {
72
84
  this.name = "NetworkError";
73
85
  }
74
86
  };
87
+ function isDispatchTicketsError(error) {
88
+ return error instanceof DispatchTicketsError;
89
+ }
90
+ function isAuthenticationError(error) {
91
+ return error instanceof AuthenticationError;
92
+ }
93
+ function isRateLimitError(error) {
94
+ return error instanceof RateLimitError;
95
+ }
96
+ function isValidationError(error) {
97
+ return error instanceof ValidationError;
98
+ }
99
+ function isNotFoundError(error) {
100
+ return error instanceof NotFoundError;
101
+ }
102
+ function isConflictError(error) {
103
+ return error instanceof ConflictError;
104
+ }
105
+ function isServerError(error) {
106
+ return error instanceof ServerError;
107
+ }
108
+ function isTimeoutError(error) {
109
+ return error instanceof TimeoutError;
110
+ }
111
+ function isNetworkError(error) {
112
+ return error instanceof NetworkError;
113
+ }
75
114
 
76
115
  // src/utils/http.ts
77
116
  var HttpClient = class {
78
117
  config;
79
118
  fetchFn;
119
+ retryConfig;
120
+ /** Rate limit info from the last response */
121
+ _lastRateLimit;
122
+ /** Request ID from the last response */
123
+ _lastRequestId;
80
124
  constructor(config) {
81
125
  this.config = config;
82
126
  this.fetchFn = config.fetch ?? fetch;
127
+ this.retryConfig = {
128
+ maxRetries: config.retry?.maxRetries ?? config.maxRetries,
129
+ retryableStatuses: config.retry?.retryableStatuses ?? [429, 500, 502, 503, 504],
130
+ retryOnNetworkError: config.retry?.retryOnNetworkError ?? true,
131
+ retryOnTimeout: config.retry?.retryOnTimeout ?? true,
132
+ initialDelayMs: config.retry?.initialDelayMs ?? 1e3,
133
+ maxDelayMs: config.retry?.maxDelayMs ?? 3e4,
134
+ backoffMultiplier: config.retry?.backoffMultiplier ?? 2,
135
+ jitter: config.retry?.jitter ?? 0.25
136
+ };
137
+ }
138
+ /**
139
+ * Get rate limit info from the last response
140
+ */
141
+ get lastRateLimit() {
142
+ return this._lastRateLimit;
143
+ }
144
+ /**
145
+ * Get request ID from the last response
146
+ */
147
+ get lastRequestId() {
148
+ return this._lastRequestId;
83
149
  }
84
150
  /**
85
151
  * Execute an HTTP request with retry logic
@@ -89,43 +155,89 @@ var HttpClient = class {
89
155
  const headers = this.buildHeaders(options.headers, options.idempotencyKey);
90
156
  let lastError;
91
157
  let attempt = 0;
92
- while (attempt <= this.config.maxRetries) {
158
+ while (attempt <= this.retryConfig.maxRetries) {
159
+ const requestContext = {
160
+ method: options.method,
161
+ url,
162
+ headers,
163
+ body: options.body,
164
+ attempt
165
+ };
93
166
  try {
94
- const response = await this.executeRequest(url, options.method, headers, options.body);
95
- return await this.handleResponse(response);
167
+ if (this.config.hooks?.onRequest) {
168
+ await this.config.hooks.onRequest(requestContext);
169
+ }
170
+ const startTime = Date.now();
171
+ const response = await this.executeRequest(
172
+ url,
173
+ options.method,
174
+ headers,
175
+ options.body,
176
+ options.signal
177
+ );
178
+ const durationMs = Date.now() - startTime;
179
+ const result = await this.handleResponse(response, requestContext, durationMs);
180
+ return result;
96
181
  } catch (error) {
97
182
  lastError = error;
98
- if (error instanceof DispatchTicketsError) {
99
- if (error instanceof AuthenticationError || error instanceof ValidationError || error instanceof NotFoundError || error instanceof ConflictError) {
100
- throw error;
101
- }
102
- if (error instanceof RateLimitError && error.retryAfter) {
103
- if (attempt < this.config.maxRetries) {
104
- await this.sleep(error.retryAfter * 1e3);
105
- attempt++;
106
- continue;
107
- }
108
- }
109
- if (error instanceof ServerError) {
110
- if (attempt < this.config.maxRetries) {
111
- await this.sleep(this.calculateBackoff(attempt));
112
- attempt++;
113
- continue;
114
- }
115
- }
183
+ if (this.config.hooks?.onError) {
184
+ await this.config.hooks.onError(lastError, requestContext);
116
185
  }
117
- if (error instanceof NetworkError || error instanceof TimeoutError) {
118
- if (attempt < this.config.maxRetries) {
119
- await this.sleep(this.calculateBackoff(attempt));
120
- attempt++;
121
- continue;
186
+ if (attempt < this.retryConfig.maxRetries && this.shouldRetry(lastError)) {
187
+ const delay = this.calculateDelay(lastError, attempt);
188
+ if (this.config.hooks?.onRetry) {
189
+ await this.config.hooks.onRetry(requestContext, lastError, delay);
122
190
  }
191
+ await this.sleep(delay);
192
+ attempt++;
193
+ continue;
123
194
  }
124
- throw error;
195
+ throw lastError;
125
196
  }
126
197
  }
127
198
  throw lastError || new NetworkError("Request failed after retries");
128
199
  }
200
+ /**
201
+ * Execute request and return response with rate limit info
202
+ */
203
+ async requestWithRateLimit(options) {
204
+ const data = await this.request(options);
205
+ return {
206
+ data,
207
+ rateLimit: this._lastRateLimit,
208
+ requestId: this._lastRequestId
209
+ };
210
+ }
211
+ shouldRetry(error) {
212
+ if (error instanceof AuthenticationError || error instanceof ValidationError || error instanceof NotFoundError || error instanceof ConflictError) {
213
+ return false;
214
+ }
215
+ if (error instanceof RateLimitError) {
216
+ return true;
217
+ }
218
+ if (error instanceof ServerError && error.statusCode) {
219
+ return this.retryConfig.retryableStatuses.includes(error.statusCode);
220
+ }
221
+ if (error instanceof NetworkError) {
222
+ return this.retryConfig.retryOnNetworkError;
223
+ }
224
+ if (error instanceof TimeoutError) {
225
+ return this.retryConfig.retryOnTimeout;
226
+ }
227
+ return false;
228
+ }
229
+ calculateDelay(error, attempt) {
230
+ if (error instanceof RateLimitError && error.retryAfter) {
231
+ return error.retryAfter * 1e3;
232
+ }
233
+ const baseDelay = this.retryConfig.initialDelayMs;
234
+ const delay = Math.min(
235
+ baseDelay * Math.pow(this.retryConfig.backoffMultiplier, attempt),
236
+ this.retryConfig.maxDelayMs
237
+ );
238
+ const jitter = delay * this.retryConfig.jitter * Math.random();
239
+ return delay + jitter;
240
+ }
129
241
  buildUrl(path, query) {
130
242
  const url = new URL(path, this.config.baseUrl);
131
243
  if (query) {
@@ -149,9 +261,11 @@ var HttpClient = class {
149
261
  }
150
262
  return headers;
151
263
  }
152
- async executeRequest(url, method, headers, body) {
264
+ async executeRequest(url, method, headers, body, userSignal) {
153
265
  const controller = new AbortController();
154
266
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
267
+ const abortHandler = () => controller.abort();
268
+ userSignal?.addEventListener("abort", abortHandler);
155
269
  try {
156
270
  if (this.config.debug) {
157
271
  console.log(`[DispatchTickets] ${method} ${url}`);
@@ -159,6 +273,9 @@ var HttpClient = class {
159
273
  console.log("[DispatchTickets] Body:", JSON.stringify(body, null, 2));
160
274
  }
161
275
  }
276
+ if (userSignal?.aborted) {
277
+ throw new Error("Request aborted");
278
+ }
162
279
  const response = await this.fetchFn(url, {
163
280
  method,
164
281
  headers,
@@ -169,6 +286,9 @@ var HttpClient = class {
169
286
  } catch (error) {
170
287
  if (error instanceof Error) {
171
288
  if (error.name === "AbortError") {
289
+ if (userSignal?.aborted) {
290
+ throw new NetworkError("Request aborted by user");
291
+ }
172
292
  throw new TimeoutError(`Request timed out after ${this.config.timeout}ms`);
173
293
  }
174
294
  throw new NetworkError(error.message);
@@ -176,11 +296,42 @@ var HttpClient = class {
176
296
  throw new NetworkError("Unknown network error");
177
297
  } finally {
178
298
  clearTimeout(timeoutId);
299
+ userSignal?.removeEventListener("abort", abortHandler);
300
+ }
301
+ }
302
+ extractRateLimitInfo(response) {
303
+ const limit = response.headers.get("x-ratelimit-limit");
304
+ const remaining = response.headers.get("x-ratelimit-remaining");
305
+ const reset = response.headers.get("x-ratelimit-reset");
306
+ if (limit && remaining && reset) {
307
+ return {
308
+ limit: parseInt(limit, 10),
309
+ remaining: parseInt(remaining, 10),
310
+ reset: parseInt(reset, 10)
311
+ };
179
312
  }
313
+ return void 0;
180
314
  }
181
- async handleResponse(response) {
315
+ async handleResponse(response, requestContext, durationMs) {
182
316
  const contentType = response.headers.get("content-type");
183
317
  const isJson = contentType?.includes("application/json");
318
+ const requestId = response.headers.get("x-request-id") ?? void 0;
319
+ const rateLimitInfo = this.extractRateLimitInfo(response);
320
+ this._lastRequestId = requestId;
321
+ this._lastRateLimit = rateLimitInfo;
322
+ if (this.config.debug && requestId) {
323
+ console.log(`[DispatchTickets] Request ID: ${requestId}`);
324
+ }
325
+ if (response.ok && this.config.hooks?.onResponse) {
326
+ await this.config.hooks.onResponse({
327
+ request: requestContext,
328
+ status: response.status,
329
+ headers: response.headers,
330
+ requestId,
331
+ rateLimit: rateLimitInfo,
332
+ durationMs
333
+ });
334
+ }
184
335
  if (response.ok) {
185
336
  if (response.status === 204 || !isJson) {
186
337
  return void 0;
@@ -197,31 +348,36 @@ var HttpClient = class {
197
348
  const message = errorData.message || errorData.error || response.statusText;
198
349
  switch (response.status) {
199
350
  case 401:
200
- throw new AuthenticationError(message);
351
+ throw new AuthenticationError(message, requestId);
201
352
  case 400:
202
353
  case 422:
203
- throw new ValidationError(message, errorData.errors);
354
+ throw new ValidationError(message, errorData.errors, requestId);
204
355
  case 404:
205
- throw new NotFoundError(message);
356
+ throw new NotFoundError(message, void 0, void 0, requestId);
206
357
  case 409:
207
- throw new ConflictError(message);
358
+ throw new ConflictError(message, requestId);
208
359
  case 429: {
209
360
  const retryAfter = response.headers.get("retry-after");
210
- throw new RateLimitError(message, retryAfter ? parseInt(retryAfter, 10) : void 0);
361
+ throw new RateLimitError(
362
+ message,
363
+ retryAfter ? parseInt(retryAfter, 10) : void 0,
364
+ requestId,
365
+ rateLimitInfo
366
+ );
211
367
  }
212
368
  default:
213
369
  if (response.status >= 500) {
214
- throw new ServerError(message, response.status);
370
+ throw new ServerError(message, response.status, requestId);
215
371
  }
216
- throw new DispatchTicketsError(message, "api_error", response.status, errorData);
372
+ throw new DispatchTicketsError(
373
+ message,
374
+ "api_error",
375
+ response.status,
376
+ errorData,
377
+ requestId
378
+ );
217
379
  }
218
380
  }
219
- calculateBackoff(attempt) {
220
- const baseDelay = 1e3;
221
- const maxDelay = 3e4;
222
- const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
223
- return delay + Math.random() * delay * 0.25;
224
- }
225
381
  sleep(ms) {
226
382
  return new Promise((resolve) => setTimeout(resolve, ms));
227
383
  }
@@ -233,11 +389,12 @@ var BaseResource = class {
233
389
  constructor(http) {
234
390
  this.http = http;
235
391
  }
236
- async _get(path, query) {
392
+ async _get(path, query, options) {
237
393
  return this.http.request({
238
394
  method: "GET",
239
395
  path,
240
- query
396
+ query,
397
+ signal: options?.signal
241
398
  });
242
399
  }
243
400
  async _post(path, body, options) {
@@ -246,28 +403,32 @@ var BaseResource = class {
246
403
  path,
247
404
  body,
248
405
  idempotencyKey: options?.idempotencyKey,
249
- query: options?.query
406
+ query: options?.query,
407
+ signal: options?.signal
250
408
  });
251
409
  }
252
- async _patch(path, body) {
410
+ async _patch(path, body, options) {
253
411
  return this.http.request({
254
412
  method: "PATCH",
255
413
  path,
256
- body
414
+ body,
415
+ signal: options?.signal
257
416
  });
258
417
  }
259
- async _put(path, body) {
418
+ async _put(path, body, options) {
260
419
  return this.http.request({
261
420
  method: "PUT",
262
421
  path,
263
- body
422
+ body,
423
+ signal: options?.signal
264
424
  });
265
425
  }
266
- async _delete(path, query) {
426
+ async _delete(path, query, options) {
267
427
  return this.http.request({
268
428
  method: "DELETE",
269
429
  path,
270
- query
430
+ query,
431
+ signal: options?.signal
271
432
  });
272
433
  }
273
434
  };
@@ -390,6 +551,66 @@ var BrandsResource = class extends BaseResource {
390
551
  getInboundEmail(brandId, domain = "inbound.dispatchtickets.com") {
391
552
  return `${brandId}@${domain}`;
392
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
+ }
393
614
  };
394
615
 
395
616
  // src/resources/tickets.ts
@@ -906,48 +1127,207 @@ var DispatchTickets = class {
906
1127
  http;
907
1128
  /**
908
1129
  * Accounts resource for managing the current account and API keys
1130
+ *
1131
+ * @example
1132
+ * ```typescript
1133
+ * // Get current account
1134
+ * const account = await client.accounts.me();
1135
+ *
1136
+ * // Get usage statistics
1137
+ * const usage = await client.accounts.getUsage();
1138
+ *
1139
+ * // Create a new API key
1140
+ * const newKey = await client.accounts.createApiKey({
1141
+ * name: 'Production',
1142
+ * allBrands: true,
1143
+ * });
1144
+ * ```
909
1145
  */
910
1146
  accounts;
911
1147
  /**
912
1148
  * Brands (workspaces) resource
1149
+ *
1150
+ * Brands are isolated containers for tickets. Each brand can have its own
1151
+ * email address, categories, tags, and settings.
1152
+ *
1153
+ * @example
1154
+ * ```typescript
1155
+ * // List all brands
1156
+ * const brands = await client.brands.list();
1157
+ *
1158
+ * // Create a new brand
1159
+ * const brand = await client.brands.create({
1160
+ * name: 'Acme Support',
1161
+ * slug: 'acme',
1162
+ * });
1163
+ *
1164
+ * // Get inbound email address
1165
+ * const email = client.brands.getInboundEmail('br_abc123');
1166
+ * // Returns: br_abc123@inbound.dispatchtickets.com
1167
+ * ```
913
1168
  */
914
1169
  brands;
915
1170
  /**
916
1171
  * Tickets resource
1172
+ *
1173
+ * @example
1174
+ * ```typescript
1175
+ * // Create a ticket
1176
+ * const ticket = await client.tickets.create('ws_abc123', {
1177
+ * title: 'Issue with billing',
1178
+ * body: 'I was charged twice...',
1179
+ * priority: 'high',
1180
+ * });
1181
+ *
1182
+ * // Iterate through all tickets
1183
+ * for await (const ticket of client.tickets.list('ws_abc123', { status: 'open' })) {
1184
+ * console.log(ticket.title);
1185
+ * }
1186
+ * ```
917
1187
  */
918
1188
  tickets;
919
1189
  /**
920
1190
  * Comments resource
1191
+ *
1192
+ * @example
1193
+ * ```typescript
1194
+ * // Add a comment
1195
+ * const comment = await client.comments.create('ws_abc123', 'tkt_xyz', {
1196
+ * body: 'Thanks for your patience!',
1197
+ * authorType: 'AGENT',
1198
+ * });
1199
+ *
1200
+ * // List comments
1201
+ * const comments = await client.comments.list('ws_abc123', 'tkt_xyz');
1202
+ * ```
921
1203
  */
922
1204
  comments;
923
1205
  /**
924
1206
  * Attachments resource
1207
+ *
1208
+ * @example
1209
+ * ```typescript
1210
+ * // Simple upload
1211
+ * const attachment = await client.attachments.upload(
1212
+ * 'ws_abc123',
1213
+ * 'tkt_xyz',
1214
+ * fileBuffer,
1215
+ * 'document.pdf',
1216
+ * 'application/pdf'
1217
+ * );
1218
+ *
1219
+ * // Get download URL
1220
+ * const { downloadUrl } = await client.attachments.get('ws_abc123', 'tkt_xyz', 'att_abc');
1221
+ * ```
925
1222
  */
926
1223
  attachments;
927
1224
  /**
928
1225
  * Webhooks resource
1226
+ *
1227
+ * @example
1228
+ * ```typescript
1229
+ * // Create a webhook
1230
+ * const webhook = await client.webhooks.create('ws_abc123', {
1231
+ * url: 'https://example.com/webhook',
1232
+ * secret: 'your-secret',
1233
+ * events: ['ticket.created', 'ticket.updated'],
1234
+ * });
1235
+ * ```
929
1236
  */
930
1237
  webhooks;
931
1238
  /**
932
1239
  * Categories resource
1240
+ *
1241
+ * @example
1242
+ * ```typescript
1243
+ * // Create a category
1244
+ * await client.categories.create('ws_abc123', { name: 'Billing', color: '#ef4444' });
1245
+ *
1246
+ * // Get category stats
1247
+ * const stats = await client.categories.getStats('ws_abc123');
1248
+ * ```
933
1249
  */
934
1250
  categories;
935
1251
  /**
936
1252
  * Tags resource
1253
+ *
1254
+ * @example
1255
+ * ```typescript
1256
+ * // Create a tag
1257
+ * await client.tags.create('ws_abc123', { name: 'urgent', color: '#f59e0b' });
1258
+ *
1259
+ * // Merge tags
1260
+ * await client.tags.merge('ws_abc123', 'tag_target', ['tag_source1', 'tag_source2']);
1261
+ * ```
937
1262
  */
938
1263
  tags;
939
1264
  /**
940
1265
  * Customers resource
1266
+ *
1267
+ * @example
1268
+ * ```typescript
1269
+ * // Create a customer
1270
+ * const customer = await client.customers.create('ws_abc123', {
1271
+ * email: 'user@example.com',
1272
+ * name: 'Jane Doe',
1273
+ * });
1274
+ *
1275
+ * // Search customers
1276
+ * const results = await client.customers.search('ws_abc123', 'jane');
1277
+ * ```
941
1278
  */
942
1279
  customers;
943
1280
  /**
944
1281
  * Custom fields resource
1282
+ *
1283
+ * @example
1284
+ * ```typescript
1285
+ * // Get all field definitions
1286
+ * const fields = await client.fields.getAll('ws_abc123');
1287
+ *
1288
+ * // Create a field
1289
+ * await client.fields.create('ws_abc123', 'ticket', {
1290
+ * key: 'order_id',
1291
+ * label: 'Order ID',
1292
+ * type: 'text',
1293
+ * required: true,
1294
+ * });
1295
+ * ```
945
1296
  */
946
1297
  fields;
947
1298
  /**
948
1299
  * Static webhook utilities
1300
+ *
1301
+ * @example
1302
+ * ```typescript
1303
+ * // Verify webhook signature
1304
+ * const isValid = DispatchTickets.webhooks.verifySignature(
1305
+ * rawBody,
1306
+ * req.headers['x-dispatch-signature'],
1307
+ * 'your-secret'
1308
+ * );
1309
+ *
1310
+ * // Generate signature for testing
1311
+ * const signature = DispatchTickets.webhooks.generateSignature(
1312
+ * JSON.stringify(payload),
1313
+ * 'your-secret'
1314
+ * );
1315
+ * ```
949
1316
  */
950
1317
  static webhooks = webhookUtils;
1318
+ /**
1319
+ * Create a new Dispatch Tickets client
1320
+ *
1321
+ * @param config - Client configuration options
1322
+ * @throws Error if API key is not provided
1323
+ *
1324
+ * @example
1325
+ * ```typescript
1326
+ * const client = new DispatchTickets({
1327
+ * apiKey: 'sk_live_...',
1328
+ * });
1329
+ * ```
1330
+ */
951
1331
  constructor(config) {
952
1332
  if (!config.apiKey) {
953
1333
  throw new Error("API key is required");
@@ -956,9 +1336,11 @@ var DispatchTickets = class {
956
1336
  baseUrl: config.baseUrl || "https://dispatch-tickets-api.onrender.com/v1",
957
1337
  apiKey: config.apiKey,
958
1338
  timeout: config.timeout ?? 3e4,
959
- maxRetries: config.maxRetries ?? 3,
1339
+ maxRetries: config.maxRetries ?? config.retry?.maxRetries ?? 3,
960
1340
  debug: config.debug ?? false,
961
- fetch: config.fetch
1341
+ fetch: config.fetch,
1342
+ retry: config.retry,
1343
+ hooks: config.hooks
962
1344
  };
963
1345
  this.http = new HttpClient(httpConfig);
964
1346
  this.accounts = new AccountsResource(this.http);
@@ -974,6 +1356,256 @@ var DispatchTickets = class {
974
1356
  }
975
1357
  };
976
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
+
977
1609
  // src/types/events.ts
978
1610
  function isTicketCreatedEvent(event) {
979
1611
  return event.event === "ticket.created";
@@ -1016,6 +1648,7 @@ async function collectAll(iterable) {
1016
1648
 
1017
1649
  exports.AuthenticationError = AuthenticationError;
1018
1650
  exports.ConflictError = ConflictError;
1651
+ exports.DispatchPortal = DispatchPortal;
1019
1652
  exports.DispatchTickets = DispatchTickets;
1020
1653
  exports.DispatchTicketsError = DispatchTicketsError;
1021
1654
  exports.NetworkError = NetworkError;
@@ -1025,9 +1658,18 @@ exports.ServerError = ServerError;
1025
1658
  exports.TimeoutError = TimeoutError;
1026
1659
  exports.ValidationError = ValidationError;
1027
1660
  exports.collectAll = collectAll;
1661
+ exports.isAuthenticationError = isAuthenticationError;
1028
1662
  exports.isCommentCreatedEvent = isCommentCreatedEvent;
1663
+ exports.isConflictError = isConflictError;
1664
+ exports.isDispatchTicketsError = isDispatchTicketsError;
1665
+ exports.isNetworkError = isNetworkError;
1666
+ exports.isNotFoundError = isNotFoundError;
1667
+ exports.isRateLimitError = isRateLimitError;
1668
+ exports.isServerError = isServerError;
1029
1669
  exports.isTicketCreatedEvent = isTicketCreatedEvent;
1030
1670
  exports.isTicketUpdatedEvent = isTicketUpdatedEvent;
1671
+ exports.isTimeoutError = isTimeoutError;
1672
+ exports.isValidationError = isValidationError;
1031
1673
  exports.parseWebhookEvent = parseWebhookEvent;
1032
1674
  exports.webhookUtils = webhookUtils;
1033
1675
  //# sourceMappingURL=index.cjs.map