@dispatchtickets/sdk 0.1.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 +327 -0
- package/dist/index.cjs +923 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1116 -0
- package/dist/index.d.ts +1116 -0
- package/dist/index.js +910 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var DispatchTicketsError = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
statusCode;
|
|
7
|
+
details;
|
|
8
|
+
constructor(message, code, statusCode, details) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "DispatchTicketsError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
this.details = details;
|
|
14
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var AuthenticationError = class extends DispatchTicketsError {
|
|
18
|
+
constructor(message = "Invalid or missing API key") {
|
|
19
|
+
super(message, "authentication_error", 401);
|
|
20
|
+
this.name = "AuthenticationError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var RateLimitError = class extends DispatchTicketsError {
|
|
24
|
+
retryAfter;
|
|
25
|
+
constructor(message = "Rate limit exceeded", retryAfter) {
|
|
26
|
+
super(message, "rate_limit_error", 429);
|
|
27
|
+
this.name = "RateLimitError";
|
|
28
|
+
this.retryAfter = retryAfter;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var ValidationError = class extends DispatchTicketsError {
|
|
32
|
+
errors;
|
|
33
|
+
constructor(message = "Validation failed", errors) {
|
|
34
|
+
super(message, "validation_error", 400, { errors });
|
|
35
|
+
this.name = "ValidationError";
|
|
36
|
+
this.errors = errors;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var NotFoundError = class extends DispatchTicketsError {
|
|
40
|
+
resourceType;
|
|
41
|
+
resourceId;
|
|
42
|
+
constructor(message = "Resource not found", resourceType, resourceId) {
|
|
43
|
+
super(message, "not_found", 404, { resourceType, resourceId });
|
|
44
|
+
this.name = "NotFoundError";
|
|
45
|
+
this.resourceType = resourceType;
|
|
46
|
+
this.resourceId = resourceId;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var ConflictError = class extends DispatchTicketsError {
|
|
50
|
+
constructor(message = "Resource conflict") {
|
|
51
|
+
super(message, "conflict", 409);
|
|
52
|
+
this.name = "ConflictError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var ServerError = class extends DispatchTicketsError {
|
|
56
|
+
constructor(message = "Internal server error", statusCode = 500) {
|
|
57
|
+
super(message, "server_error", statusCode);
|
|
58
|
+
this.name = "ServerError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var TimeoutError = class extends DispatchTicketsError {
|
|
62
|
+
constructor(message = "Request timed out") {
|
|
63
|
+
super(message, "timeout_error");
|
|
64
|
+
this.name = "TimeoutError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var NetworkError = class extends DispatchTicketsError {
|
|
68
|
+
constructor(message = "Network error") {
|
|
69
|
+
super(message, "network_error");
|
|
70
|
+
this.name = "NetworkError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/utils/http.ts
|
|
75
|
+
var HttpClient = class {
|
|
76
|
+
config;
|
|
77
|
+
constructor(config) {
|
|
78
|
+
this.config = config;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Execute an HTTP request with retry logic
|
|
82
|
+
*/
|
|
83
|
+
async request(options) {
|
|
84
|
+
const url = this.buildUrl(options.path, options.query);
|
|
85
|
+
const headers = this.buildHeaders(options.headers, options.idempotencyKey);
|
|
86
|
+
let lastError;
|
|
87
|
+
let attempt = 0;
|
|
88
|
+
while (attempt <= this.config.maxRetries) {
|
|
89
|
+
try {
|
|
90
|
+
const response = await this.executeRequest(url, options.method, headers, options.body);
|
|
91
|
+
return await this.handleResponse(response);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
lastError = error;
|
|
94
|
+
if (error instanceof DispatchTicketsError) {
|
|
95
|
+
if (error instanceof AuthenticationError || error instanceof ValidationError || error instanceof NotFoundError || error instanceof ConflictError) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
if (error instanceof RateLimitError && error.retryAfter) {
|
|
99
|
+
if (attempt < this.config.maxRetries) {
|
|
100
|
+
await this.sleep(error.retryAfter * 1e3);
|
|
101
|
+
attempt++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (error instanceof ServerError) {
|
|
106
|
+
if (attempt < this.config.maxRetries) {
|
|
107
|
+
await this.sleep(this.calculateBackoff(attempt));
|
|
108
|
+
attempt++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (error instanceof NetworkError || error instanceof TimeoutError) {
|
|
114
|
+
if (attempt < this.config.maxRetries) {
|
|
115
|
+
await this.sleep(this.calculateBackoff(attempt));
|
|
116
|
+
attempt++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw lastError || new NetworkError("Request failed after retries");
|
|
124
|
+
}
|
|
125
|
+
buildUrl(path, query) {
|
|
126
|
+
const url = new URL(path, this.config.baseUrl);
|
|
127
|
+
if (query) {
|
|
128
|
+
for (const [key, value] of Object.entries(query)) {
|
|
129
|
+
if (value !== void 0) {
|
|
130
|
+
url.searchParams.set(key, String(value));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return url.toString();
|
|
135
|
+
}
|
|
136
|
+
buildHeaders(customHeaders, idempotencyKey) {
|
|
137
|
+
const headers = {
|
|
138
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
"Accept": "application/json",
|
|
141
|
+
...customHeaders
|
|
142
|
+
};
|
|
143
|
+
if (idempotencyKey) {
|
|
144
|
+
headers["X-Idempotency-Key"] = idempotencyKey;
|
|
145
|
+
}
|
|
146
|
+
return headers;
|
|
147
|
+
}
|
|
148
|
+
async executeRequest(url, method, headers, body) {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
151
|
+
try {
|
|
152
|
+
if (this.config.debug) {
|
|
153
|
+
console.log(`[DispatchTickets] ${method} ${url}`);
|
|
154
|
+
if (body) {
|
|
155
|
+
console.log("[DispatchTickets] Body:", JSON.stringify(body, null, 2));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const response = await fetch(url, {
|
|
159
|
+
method,
|
|
160
|
+
headers,
|
|
161
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
162
|
+
signal: controller.signal
|
|
163
|
+
});
|
|
164
|
+
return response;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error instanceof Error) {
|
|
167
|
+
if (error.name === "AbortError") {
|
|
168
|
+
throw new TimeoutError(`Request timed out after ${this.config.timeout}ms`);
|
|
169
|
+
}
|
|
170
|
+
throw new NetworkError(error.message);
|
|
171
|
+
}
|
|
172
|
+
throw new NetworkError("Unknown network error");
|
|
173
|
+
} finally {
|
|
174
|
+
clearTimeout(timeoutId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async handleResponse(response) {
|
|
178
|
+
const contentType = response.headers.get("content-type");
|
|
179
|
+
const isJson = contentType?.includes("application/json");
|
|
180
|
+
if (response.ok) {
|
|
181
|
+
if (response.status === 204 || !isJson) {
|
|
182
|
+
return void 0;
|
|
183
|
+
}
|
|
184
|
+
return await response.json();
|
|
185
|
+
}
|
|
186
|
+
let errorData = {};
|
|
187
|
+
if (isJson) {
|
|
188
|
+
try {
|
|
189
|
+
errorData = await response.json();
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const message = errorData.message || errorData.error || response.statusText;
|
|
194
|
+
switch (response.status) {
|
|
195
|
+
case 401:
|
|
196
|
+
throw new AuthenticationError(message);
|
|
197
|
+
case 400:
|
|
198
|
+
case 422:
|
|
199
|
+
throw new ValidationError(message, errorData.errors);
|
|
200
|
+
case 404:
|
|
201
|
+
throw new NotFoundError(message);
|
|
202
|
+
case 409:
|
|
203
|
+
throw new ConflictError(message);
|
|
204
|
+
case 429: {
|
|
205
|
+
const retryAfter = response.headers.get("retry-after");
|
|
206
|
+
throw new RateLimitError(message, retryAfter ? parseInt(retryAfter, 10) : void 0);
|
|
207
|
+
}
|
|
208
|
+
default:
|
|
209
|
+
if (response.status >= 500) {
|
|
210
|
+
throw new ServerError(message, response.status);
|
|
211
|
+
}
|
|
212
|
+
throw new DispatchTicketsError(message, "api_error", response.status, errorData);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
calculateBackoff(attempt) {
|
|
216
|
+
const baseDelay = 1e3;
|
|
217
|
+
const maxDelay = 3e4;
|
|
218
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
219
|
+
return delay + Math.random() * delay * 0.25;
|
|
220
|
+
}
|
|
221
|
+
sleep(ms) {
|
|
222
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// src/resources/base.ts
|
|
227
|
+
var BaseResource = class {
|
|
228
|
+
http;
|
|
229
|
+
constructor(http) {
|
|
230
|
+
this.http = http;
|
|
231
|
+
}
|
|
232
|
+
async _get(path, query) {
|
|
233
|
+
return this.http.request({
|
|
234
|
+
method: "GET",
|
|
235
|
+
path,
|
|
236
|
+
query
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async _post(path, body, options) {
|
|
240
|
+
return this.http.request({
|
|
241
|
+
method: "POST",
|
|
242
|
+
path,
|
|
243
|
+
body,
|
|
244
|
+
idempotencyKey: options?.idempotencyKey,
|
|
245
|
+
query: options?.query
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async _patch(path, body) {
|
|
249
|
+
return this.http.request({
|
|
250
|
+
method: "PATCH",
|
|
251
|
+
path,
|
|
252
|
+
body
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async _put(path, body) {
|
|
256
|
+
return this.http.request({
|
|
257
|
+
method: "PUT",
|
|
258
|
+
path,
|
|
259
|
+
body
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async _delete(path, query) {
|
|
263
|
+
return this.http.request({
|
|
264
|
+
method: "DELETE",
|
|
265
|
+
path,
|
|
266
|
+
query
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/resources/brands.ts
|
|
272
|
+
var BrandsResource = class extends BaseResource {
|
|
273
|
+
/**
|
|
274
|
+
* Create a new brand
|
|
275
|
+
*/
|
|
276
|
+
async create(data) {
|
|
277
|
+
return this._post("/brands", data);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* List all brands
|
|
281
|
+
*/
|
|
282
|
+
async list() {
|
|
283
|
+
return this._get("/brands");
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get a brand by ID
|
|
287
|
+
*/
|
|
288
|
+
async get(brandId) {
|
|
289
|
+
return this._get(`/brands/${brandId}`);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Update a brand
|
|
293
|
+
*/
|
|
294
|
+
async update(brandId, data) {
|
|
295
|
+
return this._patch(`/brands/${brandId}`, data);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Delete a brand
|
|
299
|
+
* @param brandId - The brand ID
|
|
300
|
+
* @param confirm - Set to true to actually delete; false to preview what would be deleted
|
|
301
|
+
*/
|
|
302
|
+
async delete(brandId, confirm = true) {
|
|
303
|
+
if (confirm) {
|
|
304
|
+
await this._delete(`/brands/${brandId}`, { confirm: true });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
return this._delete(`/brands/${brandId}`);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Get the ticket schema for a brand
|
|
311
|
+
*/
|
|
312
|
+
async getSchema(brandId) {
|
|
313
|
+
return this._get(`/brands/${brandId}/schema`);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Update the ticket schema for a brand
|
|
317
|
+
*/
|
|
318
|
+
async updateSchema(brandId, schema) {
|
|
319
|
+
return this._put(`/brands/${brandId}/schema`, schema);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/resources/tickets.ts
|
|
324
|
+
var TicketsResource = class extends BaseResource {
|
|
325
|
+
/**
|
|
326
|
+
* Create a new ticket
|
|
327
|
+
*/
|
|
328
|
+
async create(brandId, data, options) {
|
|
329
|
+
return this._post(`/brands/${brandId}/tickets`, data, {
|
|
330
|
+
idempotencyKey: options?.idempotencyKey
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* List tickets with pagination (async iterator)
|
|
335
|
+
* Automatically fetches all pages
|
|
336
|
+
*/
|
|
337
|
+
async *list(brandId, filters) {
|
|
338
|
+
let cursor;
|
|
339
|
+
let hasMore = true;
|
|
340
|
+
while (hasMore) {
|
|
341
|
+
const page = await this.listPage(brandId, { ...filters, cursor });
|
|
342
|
+
for (const ticket of page.data) {
|
|
343
|
+
yield ticket;
|
|
344
|
+
}
|
|
345
|
+
cursor = page.pagination.cursor;
|
|
346
|
+
hasMore = page.pagination.hasMore;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* List a single page of tickets
|
|
351
|
+
*/
|
|
352
|
+
async listPage(brandId, filters) {
|
|
353
|
+
const query = this.buildListQuery(filters);
|
|
354
|
+
return this._get(`/brands/${brandId}/tickets`, query);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Get a ticket by ID
|
|
358
|
+
*/
|
|
359
|
+
async get(brandId, ticketId) {
|
|
360
|
+
return this._get(`/brands/${brandId}/tickets/${ticketId}`);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Update a ticket
|
|
364
|
+
*/
|
|
365
|
+
async update(brandId, ticketId, data) {
|
|
366
|
+
return this._patch(`/brands/${brandId}/tickets/${ticketId}`, data);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Delete a ticket
|
|
370
|
+
*/
|
|
371
|
+
async delete(brandId, ticketId) {
|
|
372
|
+
return this._delete(`/brands/${brandId}/tickets/${ticketId}`);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Mark a ticket as spam or not spam
|
|
376
|
+
*/
|
|
377
|
+
async markAsSpam(brandId, ticketId, isSpam) {
|
|
378
|
+
return this._post(`/brands/${brandId}/tickets/${ticketId}/spam`, { isSpam });
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Merge tickets into a target ticket
|
|
382
|
+
*/
|
|
383
|
+
async merge(brandId, targetTicketId, sourceTicketIds) {
|
|
384
|
+
return this._post(`/brands/${brandId}/tickets/${targetTicketId}/merge`, {
|
|
385
|
+
sourceTicketIds
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Perform a bulk action on multiple tickets
|
|
390
|
+
*/
|
|
391
|
+
async bulk(brandId, action, ticketIds, options) {
|
|
392
|
+
return this._post(`/brands/${brandId}/tickets/bulk`, {
|
|
393
|
+
action,
|
|
394
|
+
ticketIds,
|
|
395
|
+
...options
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
buildListQuery(filters) {
|
|
399
|
+
if (!filters) return {};
|
|
400
|
+
const query = {};
|
|
401
|
+
if (filters.status) {
|
|
402
|
+
query.status = Array.isArray(filters.status) ? filters.status.join(",") : filters.status;
|
|
403
|
+
}
|
|
404
|
+
if (filters.priority) query.priority = filters.priority;
|
|
405
|
+
if (filters.assigneeId) query.assigneeId = filters.assigneeId;
|
|
406
|
+
if (filters.customerId) query.customerId = filters.customerId;
|
|
407
|
+
if (filters.customerEmail) query.customerEmail = filters.customerEmail;
|
|
408
|
+
if (filters.createdBy) query.createdBy = filters.createdBy;
|
|
409
|
+
if (filters.createdAfter) query.createdAfter = filters.createdAfter;
|
|
410
|
+
if (filters.createdBefore) query.createdBefore = filters.createdBefore;
|
|
411
|
+
if (filters.hasAttachments !== void 0) query.hasAttachments = filters.hasAttachments;
|
|
412
|
+
if (filters.isSpam !== void 0) query.isSpam = filters.isSpam;
|
|
413
|
+
if (filters.search) query.search = filters.search;
|
|
414
|
+
if (filters.categoryId) query.categoryId = filters.categoryId;
|
|
415
|
+
if (filters.tagIds) query.tagIds = filters.tagIds.join(",");
|
|
416
|
+
if (filters.source) query.source = filters.source;
|
|
417
|
+
if (filters.sort) query.sort = filters.sort;
|
|
418
|
+
if (filters.order) query.order = filters.order;
|
|
419
|
+
if (filters.limit) query.limit = filters.limit;
|
|
420
|
+
if (filters.cursor) query.cursor = filters.cursor;
|
|
421
|
+
return query;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// src/resources/comments.ts
|
|
426
|
+
var CommentsResource = class extends BaseResource {
|
|
427
|
+
/**
|
|
428
|
+
* Create a new comment on a ticket
|
|
429
|
+
*/
|
|
430
|
+
async create(brandId, ticketId, data, options) {
|
|
431
|
+
return this._post(
|
|
432
|
+
`/brands/${brandId}/tickets/${ticketId}/comments`,
|
|
433
|
+
data,
|
|
434
|
+
{ idempotencyKey: options?.idempotencyKey }
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* List all comments on a ticket
|
|
439
|
+
*/
|
|
440
|
+
async list(brandId, ticketId) {
|
|
441
|
+
return this._get(`/brands/${brandId}/tickets/${ticketId}/comments`);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Get a comment by ID
|
|
445
|
+
*/
|
|
446
|
+
async get(brandId, ticketId, commentId) {
|
|
447
|
+
return this._get(
|
|
448
|
+
`/brands/${brandId}/tickets/${ticketId}/comments/${commentId}`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Update a comment
|
|
453
|
+
*/
|
|
454
|
+
async update(brandId, ticketId, commentId, data) {
|
|
455
|
+
return this._patch(
|
|
456
|
+
`/brands/${brandId}/tickets/${ticketId}/comments/${commentId}`,
|
|
457
|
+
data
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Delete a comment
|
|
462
|
+
*/
|
|
463
|
+
async delete(brandId, ticketId, commentId) {
|
|
464
|
+
return this._delete(
|
|
465
|
+
`/brands/${brandId}/tickets/${ticketId}/comments/${commentId}`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/resources/attachments.ts
|
|
471
|
+
var AttachmentsResource = class extends BaseResource {
|
|
472
|
+
/**
|
|
473
|
+
* Initiate an upload and get a presigned URL
|
|
474
|
+
*/
|
|
475
|
+
async initiateUpload(brandId, ticketId, data) {
|
|
476
|
+
return this._post(
|
|
477
|
+
`/brands/${brandId}/tickets/${ticketId}/attachments`,
|
|
478
|
+
data
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Confirm that an upload has completed
|
|
483
|
+
*/
|
|
484
|
+
async confirmUpload(brandId, ticketId, attachmentId) {
|
|
485
|
+
return this._post(
|
|
486
|
+
`/brands/${brandId}/tickets/${ticketId}/attachments/${attachmentId}/confirm`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* List all attachments on a ticket
|
|
491
|
+
*/
|
|
492
|
+
async list(brandId, ticketId) {
|
|
493
|
+
return this._get(`/brands/${brandId}/tickets/${ticketId}/attachments`);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Get an attachment with its download URL
|
|
497
|
+
*/
|
|
498
|
+
async get(brandId, ticketId, attachmentId) {
|
|
499
|
+
return this._get(
|
|
500
|
+
`/brands/${brandId}/tickets/${ticketId}/attachments/${attachmentId}`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Delete an attachment
|
|
505
|
+
*/
|
|
506
|
+
async delete(brandId, ticketId, attachmentId) {
|
|
507
|
+
await this._delete(
|
|
508
|
+
`/brands/${brandId}/tickets/${ticketId}/attachments/${attachmentId}`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Convenience method: Upload a file in one step
|
|
513
|
+
* Handles: initiate -> upload to presigned URL -> confirm
|
|
514
|
+
*/
|
|
515
|
+
async upload(brandId, ticketId, file, filename, contentType) {
|
|
516
|
+
let size;
|
|
517
|
+
if (file instanceof Blob) {
|
|
518
|
+
size = file.size;
|
|
519
|
+
} else if (Buffer.isBuffer(file)) {
|
|
520
|
+
size = file.length;
|
|
521
|
+
} else {
|
|
522
|
+
size = file.byteLength;
|
|
523
|
+
}
|
|
524
|
+
const { uploadUrl, attachmentId } = await this.initiateUpload(brandId, ticketId, {
|
|
525
|
+
filename,
|
|
526
|
+
contentType,
|
|
527
|
+
size
|
|
528
|
+
});
|
|
529
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
530
|
+
method: "PUT",
|
|
531
|
+
headers: {
|
|
532
|
+
"Content-Type": contentType,
|
|
533
|
+
"Content-Length": String(size)
|
|
534
|
+
},
|
|
535
|
+
body: file
|
|
536
|
+
});
|
|
537
|
+
if (!uploadResponse.ok) {
|
|
538
|
+
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
539
|
+
}
|
|
540
|
+
return this.confirmUpload(brandId, ticketId, attachmentId);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/resources/webhooks.ts
|
|
545
|
+
var WebhooksResource = class extends BaseResource {
|
|
546
|
+
/**
|
|
547
|
+
* Create a new webhook
|
|
548
|
+
*/
|
|
549
|
+
async create(brandId, data) {
|
|
550
|
+
return this._post(`/brands/${brandId}/webhooks`, data);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* List all webhooks for a brand
|
|
554
|
+
*/
|
|
555
|
+
async list(brandId) {
|
|
556
|
+
return this._get(`/brands/${brandId}/webhooks`);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Get a webhook by ID
|
|
560
|
+
*/
|
|
561
|
+
async get(brandId, webhookId) {
|
|
562
|
+
return this._get(`/brands/${brandId}/webhooks/${webhookId}`);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Delete a webhook
|
|
566
|
+
*/
|
|
567
|
+
async delete(brandId, webhookId) {
|
|
568
|
+
await this._delete(`/brands/${brandId}/webhooks/${webhookId}`);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Get webhook delivery history
|
|
572
|
+
*/
|
|
573
|
+
async getDeliveries(brandId, webhookId) {
|
|
574
|
+
return this._get(
|
|
575
|
+
`/brands/${brandId}/webhooks/${webhookId}/deliveries`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/resources/categories.ts
|
|
581
|
+
var CategoriesResource = class extends BaseResource {
|
|
582
|
+
/**
|
|
583
|
+
* Create a new category
|
|
584
|
+
*/
|
|
585
|
+
async create(brandId, data) {
|
|
586
|
+
return this._post(`/brands/${brandId}/categories`, data);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* List all categories for a brand
|
|
590
|
+
*/
|
|
591
|
+
async list(brandId) {
|
|
592
|
+
return this._get(`/brands/${brandId}/categories`);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Get a category by ID
|
|
596
|
+
*/
|
|
597
|
+
async get(brandId, categoryId) {
|
|
598
|
+
return this._get(`/brands/${brandId}/categories/${categoryId}`);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Update a category
|
|
602
|
+
*/
|
|
603
|
+
async update(brandId, categoryId, data) {
|
|
604
|
+
return this._patch(`/brands/${brandId}/categories/${categoryId}`, data);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Delete a category
|
|
608
|
+
*/
|
|
609
|
+
async delete(brandId, categoryId) {
|
|
610
|
+
await this._delete(`/brands/${brandId}/categories/${categoryId}`);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Get category statistics (ticket counts)
|
|
614
|
+
*/
|
|
615
|
+
async getStats(brandId) {
|
|
616
|
+
return this._get(`/brands/${brandId}/categories/stats`);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Reorder categories
|
|
620
|
+
*/
|
|
621
|
+
async reorder(brandId, categoryIds) {
|
|
622
|
+
await this._post(`/brands/${brandId}/categories/reorder`, { categoryIds });
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/resources/tags.ts
|
|
627
|
+
var TagsResource = class extends BaseResource {
|
|
628
|
+
/**
|
|
629
|
+
* Create a new tag
|
|
630
|
+
*/
|
|
631
|
+
async create(brandId, data) {
|
|
632
|
+
return this._post(`/brands/${brandId}/tags`, data);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* List all tags for a brand
|
|
636
|
+
*/
|
|
637
|
+
async list(brandId) {
|
|
638
|
+
return this._get(`/brands/${brandId}/tags`);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Update a tag
|
|
642
|
+
*/
|
|
643
|
+
async update(brandId, tagId, data) {
|
|
644
|
+
return this._patch(`/brands/${brandId}/tags/${tagId}`, data);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Delete a tag
|
|
648
|
+
*/
|
|
649
|
+
async delete(brandId, tagId) {
|
|
650
|
+
await this._delete(`/brands/${brandId}/tags/${tagId}`);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Merge tags into a target tag
|
|
654
|
+
*/
|
|
655
|
+
async merge(brandId, targetTagId, sourceTagIds) {
|
|
656
|
+
return this._post(`/brands/${brandId}/tags/${targetTagId}/merge`, {
|
|
657
|
+
sourceTagIds
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// src/resources/customers.ts
|
|
663
|
+
var CustomersResource = class extends BaseResource {
|
|
664
|
+
/**
|
|
665
|
+
* Create a new customer
|
|
666
|
+
*/
|
|
667
|
+
async create(brandId, data) {
|
|
668
|
+
return this._post(`/brands/${brandId}/customers`, data);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* List customers with pagination (async iterator)
|
|
672
|
+
*/
|
|
673
|
+
async *list(brandId, filters) {
|
|
674
|
+
let cursor;
|
|
675
|
+
let hasMore = true;
|
|
676
|
+
while (hasMore) {
|
|
677
|
+
const page = await this.listPage(brandId, { ...filters, cursor });
|
|
678
|
+
for (const customer of page.data) {
|
|
679
|
+
yield customer;
|
|
680
|
+
}
|
|
681
|
+
cursor = page.pagination.cursor;
|
|
682
|
+
hasMore = page.pagination.hasMore;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* List a single page of customers
|
|
687
|
+
*/
|
|
688
|
+
async listPage(brandId, filters) {
|
|
689
|
+
const query = this.buildListQuery(filters);
|
|
690
|
+
return this._get(`/brands/${brandId}/customers`, query);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Search customers (autocomplete)
|
|
694
|
+
*/
|
|
695
|
+
async search(brandId, query) {
|
|
696
|
+
return this._get(`/brands/${brandId}/customers/search`, { q: query });
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Get a customer by ID
|
|
700
|
+
*/
|
|
701
|
+
async get(brandId, customerId) {
|
|
702
|
+
return this._get(`/brands/${brandId}/customers/${customerId}`);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Update a customer
|
|
706
|
+
*/
|
|
707
|
+
async update(brandId, customerId, data) {
|
|
708
|
+
return this._patch(`/brands/${brandId}/customers/${customerId}`, data);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Delete a customer
|
|
712
|
+
*/
|
|
713
|
+
async delete(brandId, customerId) {
|
|
714
|
+
return this._delete(`/brands/${brandId}/customers/${customerId}`);
|
|
715
|
+
}
|
|
716
|
+
buildListQuery(filters) {
|
|
717
|
+
if (!filters) return {};
|
|
718
|
+
const query = {};
|
|
719
|
+
if (filters.limit) query.limit = filters.limit;
|
|
720
|
+
if (filters.cursor) query.cursor = filters.cursor;
|
|
721
|
+
if (filters.sort) query.sort = filters.sort;
|
|
722
|
+
if (filters.order) query.order = filters.order;
|
|
723
|
+
return query;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// src/resources/fields.ts
|
|
728
|
+
var FieldsResource = class extends BaseResource {
|
|
729
|
+
/**
|
|
730
|
+
* Get all field definitions for a brand
|
|
731
|
+
*/
|
|
732
|
+
async getAll(brandId) {
|
|
733
|
+
return this._get(`/brands/${brandId}/fields`);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Get field definitions for a specific entity type
|
|
737
|
+
*/
|
|
738
|
+
async list(brandId, entityType) {
|
|
739
|
+
return this._get(`/brands/${brandId}/fields/${entityType}`);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Create a new field definition
|
|
743
|
+
*/
|
|
744
|
+
async create(brandId, entityType, data) {
|
|
745
|
+
return this._post(`/brands/${brandId}/fields/${entityType}`, data);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Update a field definition
|
|
749
|
+
*/
|
|
750
|
+
async update(brandId, entityType, key, data) {
|
|
751
|
+
return this._patch(
|
|
752
|
+
`/brands/${brandId}/fields/${entityType}/${key}`,
|
|
753
|
+
data
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Delete a field definition
|
|
758
|
+
*/
|
|
759
|
+
async delete(brandId, entityType, key) {
|
|
760
|
+
await this._delete(`/brands/${brandId}/fields/${entityType}/${key}`);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Reorder field definitions
|
|
764
|
+
*/
|
|
765
|
+
async reorder(brandId, entityType, keys) {
|
|
766
|
+
await this._post(`/brands/${brandId}/fields/${entityType}/reorder`, { keys });
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
var webhookUtils = {
|
|
770
|
+
/**
|
|
771
|
+
* Verify a webhook signature
|
|
772
|
+
*
|
|
773
|
+
* @param payload - The raw request body as a string
|
|
774
|
+
* @param signature - The X-Dispatch-Signature header value
|
|
775
|
+
* @param secret - Your webhook secret
|
|
776
|
+
* @returns true if the signature is valid
|
|
777
|
+
*
|
|
778
|
+
* @example
|
|
779
|
+
* ```typescript
|
|
780
|
+
* import { DispatchTickets } from '@dispatchtickets/sdk';
|
|
781
|
+
*
|
|
782
|
+
* app.post('/webhooks', (req, res) => {
|
|
783
|
+
* const signature = req.headers['x-dispatch-signature'];
|
|
784
|
+
* const isValid = DispatchTickets.webhooks.verifySignature(
|
|
785
|
+
* req.rawBody,
|
|
786
|
+
* signature,
|
|
787
|
+
* process.env.WEBHOOK_SECRET
|
|
788
|
+
* );
|
|
789
|
+
*
|
|
790
|
+
* if (!isValid) {
|
|
791
|
+
* return res.status(401).send('Invalid signature');
|
|
792
|
+
* }
|
|
793
|
+
*
|
|
794
|
+
* // Process webhook...
|
|
795
|
+
* });
|
|
796
|
+
* ```
|
|
797
|
+
*/
|
|
798
|
+
verifySignature(payload, signature, secret) {
|
|
799
|
+
if (!signature || !secret) {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
const parts = signature.split("=");
|
|
803
|
+
if (parts.length !== 2 || parts[0] !== "sha256") {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
const receivedSignature = parts[1];
|
|
807
|
+
const expectedSignature = createHmac("sha256", secret).update(payload).digest("hex");
|
|
808
|
+
try {
|
|
809
|
+
const receivedBuffer = Buffer.from(receivedSignature, "hex");
|
|
810
|
+
const expectedBuffer = Buffer.from(expectedSignature, "hex");
|
|
811
|
+
if (receivedBuffer.length !== expectedBuffer.length) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
return timingSafeEqual(receivedBuffer, expectedBuffer);
|
|
815
|
+
} catch {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
/**
|
|
820
|
+
* Generate a signature for testing purposes
|
|
821
|
+
*
|
|
822
|
+
* @param payload - The payload to sign
|
|
823
|
+
* @param secret - The secret to sign with
|
|
824
|
+
* @returns The signature in the format sha256=<hex>
|
|
825
|
+
*/
|
|
826
|
+
generateSignature(payload, secret) {
|
|
827
|
+
const signature = createHmac("sha256", secret).update(payload).digest("hex");
|
|
828
|
+
return `sha256=${signature}`;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// src/client.ts
|
|
833
|
+
var DispatchTickets = class {
|
|
834
|
+
http;
|
|
835
|
+
/**
|
|
836
|
+
* Brands (workspaces) resource
|
|
837
|
+
*/
|
|
838
|
+
brands;
|
|
839
|
+
/**
|
|
840
|
+
* Tickets resource
|
|
841
|
+
*/
|
|
842
|
+
tickets;
|
|
843
|
+
/**
|
|
844
|
+
* Comments resource
|
|
845
|
+
*/
|
|
846
|
+
comments;
|
|
847
|
+
/**
|
|
848
|
+
* Attachments resource
|
|
849
|
+
*/
|
|
850
|
+
attachments;
|
|
851
|
+
/**
|
|
852
|
+
* Webhooks resource
|
|
853
|
+
*/
|
|
854
|
+
webhooks;
|
|
855
|
+
/**
|
|
856
|
+
* Categories resource
|
|
857
|
+
*/
|
|
858
|
+
categories;
|
|
859
|
+
/**
|
|
860
|
+
* Tags resource
|
|
861
|
+
*/
|
|
862
|
+
tags;
|
|
863
|
+
/**
|
|
864
|
+
* Customers resource
|
|
865
|
+
*/
|
|
866
|
+
customers;
|
|
867
|
+
/**
|
|
868
|
+
* Custom fields resource
|
|
869
|
+
*/
|
|
870
|
+
fields;
|
|
871
|
+
/**
|
|
872
|
+
* Static webhook utilities
|
|
873
|
+
*/
|
|
874
|
+
static webhooks = webhookUtils;
|
|
875
|
+
constructor(config) {
|
|
876
|
+
if (!config.apiKey) {
|
|
877
|
+
throw new Error("API key is required");
|
|
878
|
+
}
|
|
879
|
+
const httpConfig = {
|
|
880
|
+
baseUrl: config.baseUrl || "https://dispatch-tickets-api.onrender.com/v1",
|
|
881
|
+
apiKey: config.apiKey,
|
|
882
|
+
timeout: config.timeout ?? 3e4,
|
|
883
|
+
maxRetries: config.maxRetries ?? 3,
|
|
884
|
+
debug: config.debug ?? false
|
|
885
|
+
};
|
|
886
|
+
this.http = new HttpClient(httpConfig);
|
|
887
|
+
this.brands = new BrandsResource(this.http);
|
|
888
|
+
this.tickets = new TicketsResource(this.http);
|
|
889
|
+
this.comments = new CommentsResource(this.http);
|
|
890
|
+
this.attachments = new AttachmentsResource(this.http);
|
|
891
|
+
this.webhooks = new WebhooksResource(this.http);
|
|
892
|
+
this.categories = new CategoriesResource(this.http);
|
|
893
|
+
this.tags = new TagsResource(this.http);
|
|
894
|
+
this.customers = new CustomersResource(this.http);
|
|
895
|
+
this.fields = new FieldsResource(this.http);
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/utils/pagination.ts
|
|
900
|
+
async function collectAll(iterable) {
|
|
901
|
+
const items = [];
|
|
902
|
+
for await (const item of iterable) {
|
|
903
|
+
items.push(item);
|
|
904
|
+
}
|
|
905
|
+
return items;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export { AuthenticationError, ConflictError, DispatchTickets, DispatchTicketsError, NetworkError, NotFoundError, RateLimitError, ServerError, TimeoutError, ValidationError, collectAll, webhookUtils };
|
|
909
|
+
//# sourceMappingURL=index.js.map
|
|
910
|
+
//# sourceMappingURL=index.js.map
|