@dispatchtickets/sdk 0.3.0 → 0.6.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.js CHANGED
@@ -5,33 +5,45 @@ var DispatchTicketsError = class extends Error {
5
5
  code;
6
6
  statusCode;
7
7
  details;
8
- constructor(message, code, statusCode, details) {
8
+ /** Request ID for debugging with support */
9
+ requestId;
10
+ constructor(message, code, statusCode, details, requestId) {
9
11
  super(message);
10
12
  this.name = "DispatchTicketsError";
11
13
  this.code = code;
12
14
  this.statusCode = statusCode;
13
15
  this.details = details;
16
+ this.requestId = requestId;
14
17
  Object.setPrototypeOf(this, new.target.prototype);
15
18
  }
16
19
  };
17
20
  var AuthenticationError = class extends DispatchTicketsError {
18
- constructor(message = "Invalid or missing API key") {
19
- super(message, "authentication_error", 401);
21
+ constructor(message = "Invalid or missing API key", requestId) {
22
+ super(message, "authentication_error", 401, void 0, requestId);
20
23
  this.name = "AuthenticationError";
21
24
  }
22
25
  };
23
26
  var RateLimitError = class extends DispatchTicketsError {
24
27
  retryAfter;
25
- constructor(message = "Rate limit exceeded", retryAfter) {
26
- super(message, "rate_limit_error", 429);
28
+ /** Rate limit ceiling */
29
+ limit;
30
+ /** Remaining requests in current window */
31
+ remaining;
32
+ /** Unix timestamp when rate limit resets */
33
+ reset;
34
+ constructor(message = "Rate limit exceeded", retryAfter, requestId, rateLimitInfo) {
35
+ super(message, "rate_limit_error", 429, void 0, requestId);
27
36
  this.name = "RateLimitError";
28
37
  this.retryAfter = retryAfter;
38
+ this.limit = rateLimitInfo?.limit;
39
+ this.remaining = rateLimitInfo?.remaining;
40
+ this.reset = rateLimitInfo?.reset;
29
41
  }
30
42
  };
31
43
  var ValidationError = class extends DispatchTicketsError {
32
44
  errors;
33
- constructor(message = "Validation failed", errors) {
34
- super(message, "validation_error", 400, { errors });
45
+ constructor(message = "Validation failed", errors, requestId) {
46
+ super(message, "validation_error", 400, { errors }, requestId);
35
47
  this.name = "ValidationError";
36
48
  this.errors = errors;
37
49
  }
@@ -39,22 +51,22 @@ var ValidationError = class extends DispatchTicketsError {
39
51
  var NotFoundError = class extends DispatchTicketsError {
40
52
  resourceType;
41
53
  resourceId;
42
- constructor(message = "Resource not found", resourceType, resourceId) {
43
- super(message, "not_found", 404, { resourceType, resourceId });
54
+ constructor(message = "Resource not found", resourceType, resourceId, requestId) {
55
+ super(message, "not_found", 404, { resourceType, resourceId }, requestId);
44
56
  this.name = "NotFoundError";
45
57
  this.resourceType = resourceType;
46
58
  this.resourceId = resourceId;
47
59
  }
48
60
  };
49
61
  var ConflictError = class extends DispatchTicketsError {
50
- constructor(message = "Resource conflict") {
51
- super(message, "conflict", 409);
62
+ constructor(message = "Resource conflict", requestId) {
63
+ super(message, "conflict", 409, void 0, requestId);
52
64
  this.name = "ConflictError";
53
65
  }
54
66
  };
55
67
  var ServerError = class extends DispatchTicketsError {
56
- constructor(message = "Internal server error", statusCode = 500) {
57
- super(message, "server_error", statusCode);
68
+ constructor(message = "Internal server error", statusCode = 500, requestId) {
69
+ super(message, "server_error", statusCode, void 0, requestId);
58
70
  this.name = "ServerError";
59
71
  }
60
72
  };
@@ -70,14 +82,68 @@ var NetworkError = class extends DispatchTicketsError {
70
82
  this.name = "NetworkError";
71
83
  }
72
84
  };
85
+ function isDispatchTicketsError(error) {
86
+ return error instanceof DispatchTicketsError;
87
+ }
88
+ function isAuthenticationError(error) {
89
+ return error instanceof AuthenticationError;
90
+ }
91
+ function isRateLimitError(error) {
92
+ return error instanceof RateLimitError;
93
+ }
94
+ function isValidationError(error) {
95
+ return error instanceof ValidationError;
96
+ }
97
+ function isNotFoundError(error) {
98
+ return error instanceof NotFoundError;
99
+ }
100
+ function isConflictError(error) {
101
+ return error instanceof ConflictError;
102
+ }
103
+ function isServerError(error) {
104
+ return error instanceof ServerError;
105
+ }
106
+ function isTimeoutError(error) {
107
+ return error instanceof TimeoutError;
108
+ }
109
+ function isNetworkError(error) {
110
+ return error instanceof NetworkError;
111
+ }
73
112
 
74
113
  // src/utils/http.ts
75
114
  var HttpClient = class {
76
115
  config;
77
116
  fetchFn;
117
+ retryConfig;
118
+ /** Rate limit info from the last response */
119
+ _lastRateLimit;
120
+ /** Request ID from the last response */
121
+ _lastRequestId;
78
122
  constructor(config) {
79
123
  this.config = config;
80
124
  this.fetchFn = config.fetch ?? fetch;
125
+ this.retryConfig = {
126
+ maxRetries: config.retry?.maxRetries ?? config.maxRetries,
127
+ retryableStatuses: config.retry?.retryableStatuses ?? [429, 500, 502, 503, 504],
128
+ retryOnNetworkError: config.retry?.retryOnNetworkError ?? true,
129
+ retryOnTimeout: config.retry?.retryOnTimeout ?? true,
130
+ initialDelayMs: config.retry?.initialDelayMs ?? 1e3,
131
+ maxDelayMs: config.retry?.maxDelayMs ?? 3e4,
132
+ backoffMultiplier: config.retry?.backoffMultiplier ?? 2,
133
+ jitter: config.retry?.jitter ?? 0.25
134
+ };
135
+ }
136
+ /**
137
+ * Get rate limit info from the last response
138
+ */
139
+ get lastRateLimit() {
140
+ return this._lastRateLimit;
141
+ }
142
+ /**
143
+ * Get request ID from the last response
144
+ */
145
+ get lastRequestId() {
146
+ return this._lastRequestId;
81
147
  }
82
148
  /**
83
149
  * Execute an HTTP request with retry logic
@@ -87,43 +153,89 @@ var HttpClient = class {
87
153
  const headers = this.buildHeaders(options.headers, options.idempotencyKey);
88
154
  let lastError;
89
155
  let attempt = 0;
90
- while (attempt <= this.config.maxRetries) {
156
+ while (attempt <= this.retryConfig.maxRetries) {
157
+ const requestContext = {
158
+ method: options.method,
159
+ url,
160
+ headers,
161
+ body: options.body,
162
+ attempt
163
+ };
91
164
  try {
92
- const response = await this.executeRequest(url, options.method, headers, options.body);
93
- return await this.handleResponse(response);
165
+ if (this.config.hooks?.onRequest) {
166
+ await this.config.hooks.onRequest(requestContext);
167
+ }
168
+ const startTime = Date.now();
169
+ const response = await this.executeRequest(
170
+ url,
171
+ options.method,
172
+ headers,
173
+ options.body,
174
+ options.signal
175
+ );
176
+ const durationMs = Date.now() - startTime;
177
+ const result = await this.handleResponse(response, requestContext, durationMs);
178
+ return result;
94
179
  } catch (error) {
95
180
  lastError = error;
96
- if (error instanceof DispatchTicketsError) {
97
- if (error instanceof AuthenticationError || error instanceof ValidationError || error instanceof NotFoundError || error instanceof ConflictError) {
98
- throw error;
99
- }
100
- if (error instanceof RateLimitError && error.retryAfter) {
101
- if (attempt < this.config.maxRetries) {
102
- await this.sleep(error.retryAfter * 1e3);
103
- attempt++;
104
- continue;
105
- }
106
- }
107
- if (error instanceof ServerError) {
108
- if (attempt < this.config.maxRetries) {
109
- await this.sleep(this.calculateBackoff(attempt));
110
- attempt++;
111
- continue;
112
- }
113
- }
181
+ if (this.config.hooks?.onError) {
182
+ await this.config.hooks.onError(lastError, requestContext);
114
183
  }
115
- if (error instanceof NetworkError || error instanceof TimeoutError) {
116
- if (attempt < this.config.maxRetries) {
117
- await this.sleep(this.calculateBackoff(attempt));
118
- attempt++;
119
- continue;
184
+ if (attempt < this.retryConfig.maxRetries && this.shouldRetry(lastError)) {
185
+ const delay = this.calculateDelay(lastError, attempt);
186
+ if (this.config.hooks?.onRetry) {
187
+ await this.config.hooks.onRetry(requestContext, lastError, delay);
120
188
  }
189
+ await this.sleep(delay);
190
+ attempt++;
191
+ continue;
121
192
  }
122
- throw error;
193
+ throw lastError;
123
194
  }
124
195
  }
125
196
  throw lastError || new NetworkError("Request failed after retries");
126
197
  }
198
+ /**
199
+ * Execute request and return response with rate limit info
200
+ */
201
+ async requestWithRateLimit(options) {
202
+ const data = await this.request(options);
203
+ return {
204
+ data,
205
+ rateLimit: this._lastRateLimit,
206
+ requestId: this._lastRequestId
207
+ };
208
+ }
209
+ shouldRetry(error) {
210
+ if (error instanceof AuthenticationError || error instanceof ValidationError || error instanceof NotFoundError || error instanceof ConflictError) {
211
+ return false;
212
+ }
213
+ if (error instanceof RateLimitError) {
214
+ return true;
215
+ }
216
+ if (error instanceof ServerError && error.statusCode) {
217
+ return this.retryConfig.retryableStatuses.includes(error.statusCode);
218
+ }
219
+ if (error instanceof NetworkError) {
220
+ return this.retryConfig.retryOnNetworkError;
221
+ }
222
+ if (error instanceof TimeoutError) {
223
+ return this.retryConfig.retryOnTimeout;
224
+ }
225
+ return false;
226
+ }
227
+ calculateDelay(error, attempt) {
228
+ if (error instanceof RateLimitError && error.retryAfter) {
229
+ return error.retryAfter * 1e3;
230
+ }
231
+ const baseDelay = this.retryConfig.initialDelayMs;
232
+ const delay = Math.min(
233
+ baseDelay * Math.pow(this.retryConfig.backoffMultiplier, attempt),
234
+ this.retryConfig.maxDelayMs
235
+ );
236
+ const jitter = delay * this.retryConfig.jitter * Math.random();
237
+ return delay + jitter;
238
+ }
127
239
  buildUrl(path, query) {
128
240
  const url = new URL(path, this.config.baseUrl);
129
241
  if (query) {
@@ -147,9 +259,11 @@ var HttpClient = class {
147
259
  }
148
260
  return headers;
149
261
  }
150
- async executeRequest(url, method, headers, body) {
262
+ async executeRequest(url, method, headers, body, userSignal) {
151
263
  const controller = new AbortController();
152
264
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
265
+ const abortHandler = () => controller.abort();
266
+ userSignal?.addEventListener("abort", abortHandler);
153
267
  try {
154
268
  if (this.config.debug) {
155
269
  console.log(`[DispatchTickets] ${method} ${url}`);
@@ -157,6 +271,9 @@ var HttpClient = class {
157
271
  console.log("[DispatchTickets] Body:", JSON.stringify(body, null, 2));
158
272
  }
159
273
  }
274
+ if (userSignal?.aborted) {
275
+ throw new Error("Request aborted");
276
+ }
160
277
  const response = await this.fetchFn(url, {
161
278
  method,
162
279
  headers,
@@ -167,6 +284,9 @@ var HttpClient = class {
167
284
  } catch (error) {
168
285
  if (error instanceof Error) {
169
286
  if (error.name === "AbortError") {
287
+ if (userSignal?.aborted) {
288
+ throw new NetworkError("Request aborted by user");
289
+ }
170
290
  throw new TimeoutError(`Request timed out after ${this.config.timeout}ms`);
171
291
  }
172
292
  throw new NetworkError(error.message);
@@ -174,11 +294,42 @@ var HttpClient = class {
174
294
  throw new NetworkError("Unknown network error");
175
295
  } finally {
176
296
  clearTimeout(timeoutId);
297
+ userSignal?.removeEventListener("abort", abortHandler);
298
+ }
299
+ }
300
+ extractRateLimitInfo(response) {
301
+ const limit = response.headers.get("x-ratelimit-limit");
302
+ const remaining = response.headers.get("x-ratelimit-remaining");
303
+ const reset = response.headers.get("x-ratelimit-reset");
304
+ if (limit && remaining && reset) {
305
+ return {
306
+ limit: parseInt(limit, 10),
307
+ remaining: parseInt(remaining, 10),
308
+ reset: parseInt(reset, 10)
309
+ };
177
310
  }
311
+ return void 0;
178
312
  }
179
- async handleResponse(response) {
313
+ async handleResponse(response, requestContext, durationMs) {
180
314
  const contentType = response.headers.get("content-type");
181
315
  const isJson = contentType?.includes("application/json");
316
+ const requestId = response.headers.get("x-request-id") ?? void 0;
317
+ const rateLimitInfo = this.extractRateLimitInfo(response);
318
+ this._lastRequestId = requestId;
319
+ this._lastRateLimit = rateLimitInfo;
320
+ if (this.config.debug && requestId) {
321
+ console.log(`[DispatchTickets] Request ID: ${requestId}`);
322
+ }
323
+ if (response.ok && this.config.hooks?.onResponse) {
324
+ await this.config.hooks.onResponse({
325
+ request: requestContext,
326
+ status: response.status,
327
+ headers: response.headers,
328
+ requestId,
329
+ rateLimit: rateLimitInfo,
330
+ durationMs
331
+ });
332
+ }
182
333
  if (response.ok) {
183
334
  if (response.status === 204 || !isJson) {
184
335
  return void 0;
@@ -195,31 +346,36 @@ var HttpClient = class {
195
346
  const message = errorData.message || errorData.error || response.statusText;
196
347
  switch (response.status) {
197
348
  case 401:
198
- throw new AuthenticationError(message);
349
+ throw new AuthenticationError(message, requestId);
199
350
  case 400:
200
351
  case 422:
201
- throw new ValidationError(message, errorData.errors);
352
+ throw new ValidationError(message, errorData.errors, requestId);
202
353
  case 404:
203
- throw new NotFoundError(message);
354
+ throw new NotFoundError(message, void 0, void 0, requestId);
204
355
  case 409:
205
- throw new ConflictError(message);
356
+ throw new ConflictError(message, requestId);
206
357
  case 429: {
207
358
  const retryAfter = response.headers.get("retry-after");
208
- throw new RateLimitError(message, retryAfter ? parseInt(retryAfter, 10) : void 0);
359
+ throw new RateLimitError(
360
+ message,
361
+ retryAfter ? parseInt(retryAfter, 10) : void 0,
362
+ requestId,
363
+ rateLimitInfo
364
+ );
209
365
  }
210
366
  default:
211
367
  if (response.status >= 500) {
212
- throw new ServerError(message, response.status);
368
+ throw new ServerError(message, response.status, requestId);
213
369
  }
214
- throw new DispatchTicketsError(message, "api_error", response.status, errorData);
370
+ throw new DispatchTicketsError(
371
+ message,
372
+ "api_error",
373
+ response.status,
374
+ errorData,
375
+ requestId
376
+ );
215
377
  }
216
378
  }
217
- calculateBackoff(attempt) {
218
- const baseDelay = 1e3;
219
- const maxDelay = 3e4;
220
- const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
221
- return delay + Math.random() * delay * 0.25;
222
- }
223
379
  sleep(ms) {
224
380
  return new Promise((resolve) => setTimeout(resolve, ms));
225
381
  }
@@ -231,11 +387,12 @@ var BaseResource = class {
231
387
  constructor(http) {
232
388
  this.http = http;
233
389
  }
234
- async _get(path, query) {
390
+ async _get(path, query, options) {
235
391
  return this.http.request({
236
392
  method: "GET",
237
393
  path,
238
- query
394
+ query,
395
+ signal: options?.signal
239
396
  });
240
397
  }
241
398
  async _post(path, body, options) {
@@ -244,28 +401,32 @@ var BaseResource = class {
244
401
  path,
245
402
  body,
246
403
  idempotencyKey: options?.idempotencyKey,
247
- query: options?.query
404
+ query: options?.query,
405
+ signal: options?.signal
248
406
  });
249
407
  }
250
- async _patch(path, body) {
408
+ async _patch(path, body, options) {
251
409
  return this.http.request({
252
410
  method: "PATCH",
253
411
  path,
254
- body
412
+ body,
413
+ signal: options?.signal
255
414
  });
256
415
  }
257
- async _put(path, body) {
416
+ async _put(path, body, options) {
258
417
  return this.http.request({
259
418
  method: "PUT",
260
419
  path,
261
- body
420
+ body,
421
+ signal: options?.signal
262
422
  });
263
423
  }
264
- async _delete(path, query) {
424
+ async _delete(path, query, options) {
265
425
  return this.http.request({
266
426
  method: "DELETE",
267
427
  path,
268
- query
428
+ query,
429
+ signal: options?.signal
269
430
  });
270
431
  }
271
432
  };
@@ -388,6 +549,66 @@ var BrandsResource = class extends BaseResource {
388
549
  getInboundEmail(brandId, domain = "inbound.dispatchtickets.com") {
389
550
  return `${brandId}@${domain}`;
390
551
  }
552
+ // ===========================================================================
553
+ // Portal Token Management
554
+ // ===========================================================================
555
+ /**
556
+ * Generate a portal token for a customer
557
+ *
558
+ * Use this for "authenticated mode" where your backend already knows who the
559
+ * customer is (they're logged into your app). Generate a portal token and
560
+ * pass it to your frontend to initialize the DispatchPortal client.
561
+ *
562
+ * @param brandId - The brand ID
563
+ * @param data - Customer email and optional name
564
+ * @returns Portal token response with JWT token and expiry
565
+ *
566
+ * @example
567
+ * ```typescript
568
+ * // On your backend (Node.js, Next.js API route, etc.)
569
+ * const { token, expiresAt } = await client.brands.generatePortalToken(
570
+ * 'br_abc123',
571
+ * {
572
+ * email: req.user.email,
573
+ * name: req.user.name,
574
+ * }
575
+ * );
576
+ *
577
+ * // Return token to your frontend
578
+ * res.json({ portalToken: token });
579
+ * ```
580
+ */
581
+ async generatePortalToken(brandId, data) {
582
+ return this._post(`/brands/${brandId}/portal/token`, data);
583
+ }
584
+ /**
585
+ * Send a magic link email to a customer
586
+ *
587
+ * Use this for "self-auth mode" where customers access the portal without
588
+ * being logged into your app. They receive an email with a link that
589
+ * authenticates them directly.
590
+ *
591
+ * @param brandId - The brand ID
592
+ * @param data - Customer email and portal URL to redirect to
593
+ * @returns Success status
594
+ *
595
+ * @example
596
+ * ```typescript
597
+ * // Customer requests access via your portal login form
598
+ * await client.brands.sendPortalMagicLink('br_abc123', {
599
+ * email: 'customer@example.com',
600
+ * portalUrl: 'https://yourapp.com/support/portal',
601
+ * });
602
+ *
603
+ * // Customer receives email with link like:
604
+ * // https://yourapp.com/support/portal?token=xyz...
605
+ *
606
+ * // Your portal page then calls DispatchPortal.verify(token)
607
+ * ```
608
+ */
609
+ async sendPortalMagicLink(brandId, data) {
610
+ return this._post(`/brands/${brandId}/portal/magic-link`, data);
611
+ }
391
612
  };
392
613
 
393
614
  // src/resources/tickets.ts
@@ -904,48 +1125,207 @@ var DispatchTickets = class {
904
1125
  http;
905
1126
  /**
906
1127
  * Accounts resource for managing the current account and API keys
1128
+ *
1129
+ * @example
1130
+ * ```typescript
1131
+ * // Get current account
1132
+ * const account = await client.accounts.me();
1133
+ *
1134
+ * // Get usage statistics
1135
+ * const usage = await client.accounts.getUsage();
1136
+ *
1137
+ * // Create a new API key
1138
+ * const newKey = await client.accounts.createApiKey({
1139
+ * name: 'Production',
1140
+ * allBrands: true,
1141
+ * });
1142
+ * ```
907
1143
  */
908
1144
  accounts;
909
1145
  /**
910
1146
  * Brands (workspaces) resource
1147
+ *
1148
+ * Brands are isolated containers for tickets. Each brand can have its own
1149
+ * email address, categories, tags, and settings.
1150
+ *
1151
+ * @example
1152
+ * ```typescript
1153
+ * // List all brands
1154
+ * const brands = await client.brands.list();
1155
+ *
1156
+ * // Create a new brand
1157
+ * const brand = await client.brands.create({
1158
+ * name: 'Acme Support',
1159
+ * slug: 'acme',
1160
+ * });
1161
+ *
1162
+ * // Get inbound email address
1163
+ * const email = client.brands.getInboundEmail('br_abc123');
1164
+ * // Returns: br_abc123@inbound.dispatchtickets.com
1165
+ * ```
911
1166
  */
912
1167
  brands;
913
1168
  /**
914
1169
  * Tickets resource
1170
+ *
1171
+ * @example
1172
+ * ```typescript
1173
+ * // Create a ticket
1174
+ * const ticket = await client.tickets.create('ws_abc123', {
1175
+ * title: 'Issue with billing',
1176
+ * body: 'I was charged twice...',
1177
+ * priority: 'high',
1178
+ * });
1179
+ *
1180
+ * // Iterate through all tickets
1181
+ * for await (const ticket of client.tickets.list('ws_abc123', { status: 'open' })) {
1182
+ * console.log(ticket.title);
1183
+ * }
1184
+ * ```
915
1185
  */
916
1186
  tickets;
917
1187
  /**
918
1188
  * Comments resource
1189
+ *
1190
+ * @example
1191
+ * ```typescript
1192
+ * // Add a comment
1193
+ * const comment = await client.comments.create('ws_abc123', 'tkt_xyz', {
1194
+ * body: 'Thanks for your patience!',
1195
+ * authorType: 'AGENT',
1196
+ * });
1197
+ *
1198
+ * // List comments
1199
+ * const comments = await client.comments.list('ws_abc123', 'tkt_xyz');
1200
+ * ```
919
1201
  */
920
1202
  comments;
921
1203
  /**
922
1204
  * Attachments resource
1205
+ *
1206
+ * @example
1207
+ * ```typescript
1208
+ * // Simple upload
1209
+ * const attachment = await client.attachments.upload(
1210
+ * 'ws_abc123',
1211
+ * 'tkt_xyz',
1212
+ * fileBuffer,
1213
+ * 'document.pdf',
1214
+ * 'application/pdf'
1215
+ * );
1216
+ *
1217
+ * // Get download URL
1218
+ * const { downloadUrl } = await client.attachments.get('ws_abc123', 'tkt_xyz', 'att_abc');
1219
+ * ```
923
1220
  */
924
1221
  attachments;
925
1222
  /**
926
1223
  * Webhooks resource
1224
+ *
1225
+ * @example
1226
+ * ```typescript
1227
+ * // Create a webhook
1228
+ * const webhook = await client.webhooks.create('ws_abc123', {
1229
+ * url: 'https://example.com/webhook',
1230
+ * secret: 'your-secret',
1231
+ * events: ['ticket.created', 'ticket.updated'],
1232
+ * });
1233
+ * ```
927
1234
  */
928
1235
  webhooks;
929
1236
  /**
930
1237
  * Categories resource
1238
+ *
1239
+ * @example
1240
+ * ```typescript
1241
+ * // Create a category
1242
+ * await client.categories.create('ws_abc123', { name: 'Billing', color: '#ef4444' });
1243
+ *
1244
+ * // Get category stats
1245
+ * const stats = await client.categories.getStats('ws_abc123');
1246
+ * ```
931
1247
  */
932
1248
  categories;
933
1249
  /**
934
1250
  * Tags resource
1251
+ *
1252
+ * @example
1253
+ * ```typescript
1254
+ * // Create a tag
1255
+ * await client.tags.create('ws_abc123', { name: 'urgent', color: '#f59e0b' });
1256
+ *
1257
+ * // Merge tags
1258
+ * await client.tags.merge('ws_abc123', 'tag_target', ['tag_source1', 'tag_source2']);
1259
+ * ```
935
1260
  */
936
1261
  tags;
937
1262
  /**
938
1263
  * Customers resource
1264
+ *
1265
+ * @example
1266
+ * ```typescript
1267
+ * // Create a customer
1268
+ * const customer = await client.customers.create('ws_abc123', {
1269
+ * email: 'user@example.com',
1270
+ * name: 'Jane Doe',
1271
+ * });
1272
+ *
1273
+ * // Search customers
1274
+ * const results = await client.customers.search('ws_abc123', 'jane');
1275
+ * ```
939
1276
  */
940
1277
  customers;
941
1278
  /**
942
1279
  * Custom fields resource
1280
+ *
1281
+ * @example
1282
+ * ```typescript
1283
+ * // Get all field definitions
1284
+ * const fields = await client.fields.getAll('ws_abc123');
1285
+ *
1286
+ * // Create a field
1287
+ * await client.fields.create('ws_abc123', 'ticket', {
1288
+ * key: 'order_id',
1289
+ * label: 'Order ID',
1290
+ * type: 'text',
1291
+ * required: true,
1292
+ * });
1293
+ * ```
943
1294
  */
944
1295
  fields;
945
1296
  /**
946
1297
  * Static webhook utilities
1298
+ *
1299
+ * @example
1300
+ * ```typescript
1301
+ * // Verify webhook signature
1302
+ * const isValid = DispatchTickets.webhooks.verifySignature(
1303
+ * rawBody,
1304
+ * req.headers['x-dispatch-signature'],
1305
+ * 'your-secret'
1306
+ * );
1307
+ *
1308
+ * // Generate signature for testing
1309
+ * const signature = DispatchTickets.webhooks.generateSignature(
1310
+ * JSON.stringify(payload),
1311
+ * 'your-secret'
1312
+ * );
1313
+ * ```
947
1314
  */
948
1315
  static webhooks = webhookUtils;
1316
+ /**
1317
+ * Create a new Dispatch Tickets client
1318
+ *
1319
+ * @param config - Client configuration options
1320
+ * @throws Error if API key is not provided
1321
+ *
1322
+ * @example
1323
+ * ```typescript
1324
+ * const client = new DispatchTickets({
1325
+ * apiKey: 'sk_live_...',
1326
+ * });
1327
+ * ```
1328
+ */
949
1329
  constructor(config) {
950
1330
  if (!config.apiKey) {
951
1331
  throw new Error("API key is required");
@@ -954,9 +1334,11 @@ var DispatchTickets = class {
954
1334
  baseUrl: config.baseUrl || "https://dispatch-tickets-api.onrender.com/v1",
955
1335
  apiKey: config.apiKey,
956
1336
  timeout: config.timeout ?? 3e4,
957
- maxRetries: config.maxRetries ?? 3,
1337
+ maxRetries: config.maxRetries ?? config.retry?.maxRetries ?? 3,
958
1338
  debug: config.debug ?? false,
959
- fetch: config.fetch
1339
+ fetch: config.fetch,
1340
+ retry: config.retry,
1341
+ hooks: config.hooks
960
1342
  };
961
1343
  this.http = new HttpClient(httpConfig);
962
1344
  this.accounts = new AccountsResource(this.http);
@@ -972,6 +1354,256 @@ var DispatchTickets = class {
972
1354
  }
973
1355
  };
974
1356
 
1357
+ // src/resources/portal-tickets.ts
1358
+ var PortalTicketsResource = class extends BaseResource {
1359
+ /**
1360
+ * List the customer's tickets
1361
+ *
1362
+ * Returns a paginated list of tickets belonging to the authenticated customer.
1363
+ *
1364
+ * @param filters - Optional filters and pagination
1365
+ * @param options - Request options
1366
+ * @returns Paginated ticket list
1367
+ *
1368
+ * @example
1369
+ * ```typescript
1370
+ * // List all tickets
1371
+ * const { data: tickets, pagination } = await portal.tickets.list();
1372
+ *
1373
+ * // Filter by status
1374
+ * const openTickets = await portal.tickets.list({ status: 'open' });
1375
+ *
1376
+ * // Paginate
1377
+ * const page2 = await portal.tickets.list({ cursor: pagination.nextCursor });
1378
+ * ```
1379
+ */
1380
+ async list(filters, options) {
1381
+ const query = this.buildListQuery(filters);
1382
+ return this._get("/portal/tickets", query, options);
1383
+ }
1384
+ /**
1385
+ * Iterate through all tickets with automatic pagination
1386
+ *
1387
+ * @param filters - Optional filters (cursor is managed automatically)
1388
+ * @returns Async iterator of tickets
1389
+ *
1390
+ * @example
1391
+ * ```typescript
1392
+ * for await (const ticket of portal.tickets.listAll({ status: 'open' })) {
1393
+ * console.log(ticket.title);
1394
+ * }
1395
+ * ```
1396
+ */
1397
+ async *listAll(filters) {
1398
+ let cursor;
1399
+ let hasMore = true;
1400
+ while (hasMore) {
1401
+ const response = await this.list({ ...filters, cursor });
1402
+ for (const ticket of response.data) {
1403
+ yield ticket;
1404
+ }
1405
+ cursor = response.pagination.nextCursor ?? void 0;
1406
+ hasMore = response.pagination.hasMore;
1407
+ }
1408
+ }
1409
+ /**
1410
+ * Get a ticket by ID
1411
+ *
1412
+ * Returns the ticket with its comments (excludes internal comments).
1413
+ *
1414
+ * @param ticketId - Ticket ID
1415
+ * @param options - Request options
1416
+ * @returns Ticket detail with comments
1417
+ *
1418
+ * @example
1419
+ * ```typescript
1420
+ * const ticket = await portal.tickets.get('tkt_abc123');
1421
+ * console.log(ticket.title);
1422
+ * console.log(`${ticket.comments.length} comments`);
1423
+ * ```
1424
+ */
1425
+ async get(ticketId, options) {
1426
+ return this._get(`/portal/tickets/${ticketId}`, void 0, options);
1427
+ }
1428
+ /**
1429
+ * Create a new ticket
1430
+ *
1431
+ * @param data - Ticket data
1432
+ * @param options - Request options including idempotency key
1433
+ * @returns Created ticket
1434
+ *
1435
+ * @example
1436
+ * ```typescript
1437
+ * const ticket = await portal.tickets.create({
1438
+ * title: 'Need help with billing',
1439
+ * body: 'I was charged twice for my subscription...',
1440
+ * });
1441
+ * ```
1442
+ */
1443
+ async create(data, options) {
1444
+ return this._post("/portal/tickets", data, {
1445
+ idempotencyKey: options?.idempotencyKey,
1446
+ signal: options?.signal
1447
+ });
1448
+ }
1449
+ /**
1450
+ * Add a comment to a ticket
1451
+ *
1452
+ * @param ticketId - Ticket ID
1453
+ * @param body - Comment text
1454
+ * @param options - Request options including idempotency key
1455
+ * @returns Created comment
1456
+ *
1457
+ * @example
1458
+ * ```typescript
1459
+ * const comment = await portal.tickets.addComment(
1460
+ * 'tkt_abc123',
1461
+ * 'Here is the additional information you requested...'
1462
+ * );
1463
+ * ```
1464
+ */
1465
+ async addComment(ticketId, body, options) {
1466
+ return this._post(`/portal/tickets/${ticketId}/comments`, { body }, {
1467
+ idempotencyKey: options?.idempotencyKey,
1468
+ signal: options?.signal
1469
+ });
1470
+ }
1471
+ /**
1472
+ * Build query parameters from filters
1473
+ */
1474
+ buildListQuery(filters) {
1475
+ if (!filters) return void 0;
1476
+ const query = {};
1477
+ if (filters.status) query.status = filters.status;
1478
+ if (filters.sort) query.sort = filters.sort;
1479
+ if (filters.order) query.order = filters.order;
1480
+ if (filters.limit) query.limit = filters.limit;
1481
+ if (filters.cursor) query.cursor = filters.cursor;
1482
+ return Object.keys(query).length > 0 ? query : void 0;
1483
+ }
1484
+ };
1485
+
1486
+ // src/portal.ts
1487
+ var DEFAULT_BASE_URL = "https://dispatch-tickets-api.onrender.com/v1";
1488
+ var DEFAULT_TIMEOUT = 3e4;
1489
+ var DispatchPortal = class {
1490
+ http;
1491
+ config;
1492
+ /**
1493
+ * Tickets resource for viewing and creating tickets
1494
+ *
1495
+ * @example
1496
+ * ```typescript
1497
+ * // List tickets
1498
+ * const { data: tickets } = await portal.tickets.list();
1499
+ *
1500
+ * // Create a ticket
1501
+ * const ticket = await portal.tickets.create({
1502
+ * title: 'Need help',
1503
+ * body: 'Something is broken...',
1504
+ * });
1505
+ *
1506
+ * // Add a comment
1507
+ * await portal.tickets.addComment(ticket.id, 'Here is more info...');
1508
+ * ```
1509
+ */
1510
+ tickets;
1511
+ /**
1512
+ * Create a new Dispatch Portal client
1513
+ *
1514
+ * @param config - Portal client configuration
1515
+ * @throws Error if token is not provided
1516
+ */
1517
+ constructor(config) {
1518
+ if (!config.token) {
1519
+ throw new Error("Portal token is required");
1520
+ }
1521
+ this.config = config;
1522
+ const httpConfig = {
1523
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
1524
+ apiKey: config.token,
1525
+ // HttpClient uses this as Bearer token
1526
+ timeout: config.timeout ?? DEFAULT_TIMEOUT,
1527
+ maxRetries: 2,
1528
+ // Fewer retries for portal (end-user experience)
1529
+ fetch: config.fetch
1530
+ };
1531
+ this.http = new HttpClient(httpConfig);
1532
+ this.tickets = new PortalTicketsResource(this.http);
1533
+ }
1534
+ /**
1535
+ * Verify a magic link token and get a portal token
1536
+ *
1537
+ * Call this when the customer clicks the magic link and lands on your portal.
1538
+ * The magic link contains a short-lived token that can be exchanged for a
1539
+ * longer-lived portal token.
1540
+ *
1541
+ * @param magicLinkToken - The token from the magic link URL
1542
+ * @param baseUrl - Optional API base URL
1543
+ * @param fetchFn - Optional custom fetch function (for testing)
1544
+ * @returns Portal token response
1545
+ *
1546
+ * @example
1547
+ * ```typescript
1548
+ * // Customer lands on: https://yourapp.com/portal?token=xyz
1549
+ * const urlToken = new URLSearchParams(window.location.search).get('token');
1550
+ *
1551
+ * const { token, email, name } = await DispatchPortal.verify(urlToken);
1552
+ *
1553
+ * // Store token and create client
1554
+ * localStorage.setItem('portalToken', token);
1555
+ * const portal = new DispatchPortal({ token });
1556
+ * ```
1557
+ */
1558
+ static async verify(magicLinkToken, baseUrl = DEFAULT_BASE_URL, fetchFn = fetch) {
1559
+ const url = `${baseUrl}/portal/verify`;
1560
+ const response = await fetchFn(url, {
1561
+ method: "POST",
1562
+ headers: {
1563
+ "Content-Type": "application/json",
1564
+ "Accept": "application/json"
1565
+ },
1566
+ body: JSON.stringify({ token: magicLinkToken })
1567
+ });
1568
+ if (!response.ok) {
1569
+ const errorData = await response.json().catch(() => ({}));
1570
+ throw new Error(errorData.message || "Failed to verify magic link token");
1571
+ }
1572
+ return response.json();
1573
+ }
1574
+ /**
1575
+ * Refresh the current portal token
1576
+ *
1577
+ * Call this before the token expires to get a new token with extended expiry.
1578
+ * The new token will have the same customer context.
1579
+ *
1580
+ * @returns New portal token response
1581
+ *
1582
+ * @example
1583
+ * ```typescript
1584
+ * // Check if token is close to expiry and refresh
1585
+ * const { token: newToken, expiresAt } = await portal.refresh();
1586
+ *
1587
+ * // Create new client with refreshed token
1588
+ * const newPortal = new DispatchPortal({ token: newToken });
1589
+ * ```
1590
+ */
1591
+ async refresh() {
1592
+ return this.http.request({
1593
+ method: "POST",
1594
+ path: "/portal/refresh"
1595
+ });
1596
+ }
1597
+ /**
1598
+ * Get the current token
1599
+ *
1600
+ * Useful for storing/passing the token.
1601
+ */
1602
+ get token() {
1603
+ return this.config.token;
1604
+ }
1605
+ };
1606
+
975
1607
  // src/types/events.ts
976
1608
  function isTicketCreatedEvent(event) {
977
1609
  return event.event === "ticket.created";
@@ -1012,6 +1644,6 @@ async function collectAll(iterable) {
1012
1644
  return items;
1013
1645
  }
1014
1646
 
1015
- export { AuthenticationError, ConflictError, DispatchTickets, DispatchTicketsError, NetworkError, NotFoundError, RateLimitError, ServerError, TimeoutError, ValidationError, collectAll, isCommentCreatedEvent, isTicketCreatedEvent, isTicketUpdatedEvent, parseWebhookEvent, webhookUtils };
1647
+ export { AuthenticationError, ConflictError, DispatchPortal, DispatchTickets, DispatchTicketsError, NetworkError, NotFoundError, RateLimitError, ServerError, TimeoutError, ValidationError, collectAll, isAuthenticationError, isCommentCreatedEvent, isConflictError, isDispatchTicketsError, isNetworkError, isNotFoundError, isRateLimitError, isServerError, isTicketCreatedEvent, isTicketUpdatedEvent, isTimeoutError, isValidationError, parseWebhookEvent, webhookUtils };
1016
1648
  //# sourceMappingURL=index.js.map
1017
1649
  //# sourceMappingURL=index.js.map