@puzzle-section/sdk-typescript 1.0.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.mjs ADDED
@@ -0,0 +1,957 @@
1
+ // src/errors.ts
2
+ var ApiError = class extends Error {
3
+ code;
4
+ statusCode;
5
+ details;
6
+ constructor(message, code, statusCode, details) {
7
+ super(message);
8
+ this.name = "ApiError";
9
+ this.code = code;
10
+ this.statusCode = statusCode;
11
+ this.details = details;
12
+ if (Error.captureStackTrace) {
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
15
+ }
16
+ };
17
+ var AuthenticationError = class extends ApiError {
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 ApiError {
24
+ retryAfter;
25
+ limit;
26
+ remaining;
27
+ reset;
28
+ constructor(message, retryAfter, limit, remaining, reset) {
29
+ super(message, "RATE_LIMIT_EXCEEDED", 429, {
30
+ retryAfter,
31
+ limit,
32
+ remaining,
33
+ reset
34
+ });
35
+ this.name = "RateLimitError";
36
+ this.retryAfter = retryAfter;
37
+ this.limit = limit;
38
+ this.remaining = remaining;
39
+ this.reset = reset;
40
+ }
41
+ };
42
+ var NotFoundError = class extends ApiError {
43
+ resourceType;
44
+ resourceId;
45
+ constructor(resourceType, resourceId) {
46
+ super(
47
+ `${resourceType} with ID '${resourceId}' not found`,
48
+ "NOT_FOUND",
49
+ 404,
50
+ { resourceType, resourceId }
51
+ );
52
+ this.name = "NotFoundError";
53
+ this.resourceType = resourceType;
54
+ this.resourceId = resourceId;
55
+ }
56
+ };
57
+ var ValidationError = class extends ApiError {
58
+ validationErrors;
59
+ constructor(message, validationErrors) {
60
+ super(message, "VALIDATION_ERROR", 400, { validationErrors });
61
+ this.name = "ValidationError";
62
+ this.validationErrors = validationErrors;
63
+ }
64
+ };
65
+ var ServerError = class extends ApiError {
66
+ constructor(message = "An unexpected server error occurred") {
67
+ super(message, "SERVER_ERROR", 500);
68
+ this.name = "ServerError";
69
+ }
70
+ };
71
+ function createErrorFromResponse(statusCode, body, headers) {
72
+ const errorBody = body.error || { code: "UNKNOWN", message: "Unknown error" };
73
+ switch (statusCode) {
74
+ case 401:
75
+ return new AuthenticationError(errorBody.message);
76
+ case 404:
77
+ return new NotFoundError(
78
+ errorBody.details?.resourceType || "Resource",
79
+ errorBody.details?.resourceId || "unknown"
80
+ );
81
+ case 429: {
82
+ const retryAfter = parseInt(headers?.get("Retry-After") || "60", 10);
83
+ const limit = parseInt(headers?.get("X-RateLimit-Limit") || "0", 10);
84
+ const remaining = parseInt(
85
+ headers?.get("X-RateLimit-Remaining") || "0",
86
+ 10
87
+ );
88
+ const reset = parseInt(headers?.get("X-RateLimit-Reset") || "0", 10);
89
+ return new RateLimitError(
90
+ errorBody.message,
91
+ retryAfter,
92
+ limit,
93
+ remaining,
94
+ reset
95
+ );
96
+ }
97
+ case 400:
98
+ return new ValidationError(
99
+ errorBody.message,
100
+ errorBody.details || {}
101
+ );
102
+ case 500:
103
+ case 502:
104
+ case 503:
105
+ case 504:
106
+ return new ServerError(errorBody.message);
107
+ default:
108
+ return new ApiError(
109
+ errorBody.message,
110
+ errorBody.code,
111
+ statusCode,
112
+ errorBody.details
113
+ );
114
+ }
115
+ }
116
+
117
+ // src/api/puzzles.ts
118
+ var PuzzlesApi = class {
119
+ constructor(request) {
120
+ this.request = request;
121
+ }
122
+ /**
123
+ * Get daily puzzles
124
+ *
125
+ * @param params - Optional parameters
126
+ * @returns List of daily puzzles
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * // Get today's puzzles
131
+ * const puzzles = await client.puzzles.getDaily();
132
+ *
133
+ * // Get puzzles for a specific date
134
+ * const puzzles = await client.puzzles.getDaily({ date: '2024-01-15' });
135
+ * ```
136
+ */
137
+ async getDaily(params) {
138
+ return this.request({
139
+ method: "GET",
140
+ path: "/puzzles/daily",
141
+ query: {
142
+ date: params?.date,
143
+ types: params?.types?.join(","),
144
+ difficulties: params?.difficulties?.join(",")
145
+ }
146
+ });
147
+ }
148
+ /**
149
+ * Get a specific puzzle by ID
150
+ *
151
+ * @param id - Puzzle ID
152
+ * @returns Puzzle details
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const puzzle = await client.puzzles.getById('puzzle-id');
157
+ * ```
158
+ */
159
+ async getById(id) {
160
+ return this.request({
161
+ method: "GET",
162
+ path: `/puzzles/${id}`
163
+ });
164
+ }
165
+ /**
166
+ * Get puzzles by type
167
+ *
168
+ * @param type - Puzzle type (sudoku, wordsearch, etc.)
169
+ * @param params - Optional filtering parameters
170
+ * @returns Paginated list of puzzles
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const sudokus = await client.puzzles.getByType('sudoku', {
175
+ * difficulty: 'medium',
176
+ * limit: 10,
177
+ * });
178
+ * ```
179
+ */
180
+ async getByType(type, params) {
181
+ return this.request({
182
+ method: "GET",
183
+ path: `/puzzles/type/${type}`,
184
+ query: {
185
+ difficulty: params?.difficulty,
186
+ limit: params?.limit,
187
+ page: params?.page
188
+ }
189
+ });
190
+ }
191
+ /**
192
+ * Get available puzzle types
193
+ *
194
+ * @returns List of available puzzle types
195
+ */
196
+ async getTypes() {
197
+ return this.request({
198
+ method: "GET",
199
+ path: "/puzzles/types"
200
+ });
201
+ }
202
+ /**
203
+ * Get puzzle by date and type
204
+ *
205
+ * @param date - Date in YYYY-MM-DD format
206
+ * @param type - Puzzle type
207
+ * @param difficulty - Optional difficulty filter
208
+ * @returns Puzzle for the specified date
209
+ */
210
+ async getByDate(date, type, difficulty) {
211
+ return this.request({
212
+ method: "GET",
213
+ path: `/puzzles/date/${date}/${type}`,
214
+ query: { difficulty }
215
+ });
216
+ }
217
+ /**
218
+ * Validate puzzle solution
219
+ *
220
+ * @param id - Puzzle ID
221
+ * @param solution - User's solution
222
+ * @returns Validation result
223
+ */
224
+ async validateSolution(id, solution) {
225
+ return this.request({
226
+ method: "POST",
227
+ path: `/puzzles/${id}/validate`,
228
+ body: { solution }
229
+ });
230
+ }
231
+ };
232
+
233
+ // src/api/users.ts
234
+ var UsersApi = class {
235
+ constructor(request) {
236
+ this.request = request;
237
+ }
238
+ /**
239
+ * Get current user profile
240
+ *
241
+ * Requires a user token to be set.
242
+ *
243
+ * @returns Current user profile
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * client.setUserToken('usr_xxxxxxxxxxxx');
248
+ * const user = await client.users.getCurrent();
249
+ * ```
250
+ */
251
+ async getCurrent() {
252
+ return this.request({
253
+ method: "GET",
254
+ path: "/users/me"
255
+ });
256
+ }
257
+ /**
258
+ * Get user statistics
259
+ *
260
+ * Requires a user token to be set.
261
+ *
262
+ * @returns User statistics
263
+ */
264
+ async getStats() {
265
+ return this.request({
266
+ method: "GET",
267
+ path: "/users/me/stats"
268
+ });
269
+ }
270
+ /**
271
+ * Get user by ID
272
+ *
273
+ * @param id - User ID
274
+ * @returns User profile (public fields only)
275
+ */
276
+ async getById(id) {
277
+ return this.request({
278
+ method: "GET",
279
+ path: `/users/${id}`
280
+ });
281
+ }
282
+ /**
283
+ * Update current user profile
284
+ *
285
+ * Requires a user token to be set.
286
+ *
287
+ * @param updates - Profile updates
288
+ * @returns Updated user profile
289
+ */
290
+ async update(updates) {
291
+ return this.request({
292
+ method: "PATCH",
293
+ path: "/users/me",
294
+ body: updates
295
+ });
296
+ }
297
+ /**
298
+ * Get user achievements
299
+ *
300
+ * Requires a user token to be set.
301
+ *
302
+ * @returns List of user achievements
303
+ */
304
+ async getAchievements() {
305
+ return this.request({
306
+ method: "GET",
307
+ path: "/users/me/achievements"
308
+ });
309
+ }
310
+ };
311
+
312
+ // src/api/progress.ts
313
+ var ProgressApi = class {
314
+ constructor(request) {
315
+ this.request = request;
316
+ }
317
+ /**
318
+ * Get saved progress for a puzzle
319
+ *
320
+ * @param puzzleId - Puzzle ID
321
+ * @returns Saved progress or null if not found
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * const progress = await client.progress.get('puzzle-id');
326
+ * if (progress.data) {
327
+ * console.log(`Elapsed time: ${progress.data.elapsed_time}s`);
328
+ * }
329
+ * ```
330
+ */
331
+ async get(puzzleId) {
332
+ return this.request({
333
+ method: "GET",
334
+ path: `/progress/${puzzleId}`
335
+ });
336
+ }
337
+ /**
338
+ * Save puzzle progress
339
+ *
340
+ * @param input - Progress data to save
341
+ * @returns Saved progress
342
+ *
343
+ * @example
344
+ * ```typescript
345
+ * await client.progress.save({
346
+ * puzzleId: 'puzzle-id',
347
+ * elapsedTime: 120,
348
+ * state: {
349
+ * grid: [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
350
+ * },
351
+ * });
352
+ * ```
353
+ */
354
+ async save(input) {
355
+ return this.request({
356
+ method: "POST",
357
+ path: `/progress/${input.puzzleId}`,
358
+ body: {
359
+ elapsed_time: input.elapsedTime,
360
+ state_data: input.state,
361
+ is_paused: input.isPaused ?? false
362
+ }
363
+ });
364
+ }
365
+ /**
366
+ * Mark puzzle as complete
367
+ *
368
+ * @param input - Completion data
369
+ * @returns Completion record with score
370
+ *
371
+ * @example
372
+ * ```typescript
373
+ * const completion = await client.progress.complete({
374
+ * puzzleId: 'puzzle-id',
375
+ * elapsedTime: 300,
376
+ * hintsUsed: 2,
377
+ * });
378
+ * console.log(`Score: ${completion.data.score}`);
379
+ * ```
380
+ */
381
+ async complete(input) {
382
+ return this.request({
383
+ method: "POST",
384
+ path: `/progress/${input.puzzleId}/complete`,
385
+ body: {
386
+ elapsed_time: input.elapsedTime,
387
+ hints_used: input.hintsUsed ?? 0
388
+ }
389
+ });
390
+ }
391
+ /**
392
+ * Delete saved progress
393
+ *
394
+ * @param puzzleId - Puzzle ID
395
+ */
396
+ async delete(puzzleId) {
397
+ return this.request({
398
+ method: "DELETE",
399
+ path: `/progress/${puzzleId}`
400
+ });
401
+ }
402
+ /**
403
+ * Get all active progress for current user
404
+ *
405
+ * @returns List of in-progress puzzles
406
+ */
407
+ async getAll() {
408
+ return this.request({
409
+ method: "GET",
410
+ path: "/progress"
411
+ });
412
+ }
413
+ /**
414
+ * Get completion history for current user
415
+ *
416
+ * @param params - Optional pagination params
417
+ * @returns List of completed puzzles
418
+ */
419
+ async getCompletions(params) {
420
+ return this.request({
421
+ method: "GET",
422
+ path: "/progress/completions",
423
+ query: params
424
+ });
425
+ }
426
+ };
427
+
428
+ // src/api/health.ts
429
+ var HealthApi = class {
430
+ constructor(request) {
431
+ this.request = request;
432
+ }
433
+ /**
434
+ * Check API health
435
+ *
436
+ * @returns Health status
437
+ *
438
+ * @example
439
+ * ```typescript
440
+ * const health = await client.health.check();
441
+ * console.log(health.data.status); // 'healthy'
442
+ * ```
443
+ */
444
+ async check() {
445
+ return this.request({
446
+ method: "GET",
447
+ path: "/health"
448
+ });
449
+ }
450
+ /**
451
+ * Simple ping check
452
+ *
453
+ * @returns Ping response
454
+ */
455
+ async ping() {
456
+ return this.request({
457
+ method: "GET",
458
+ path: "/health/ping"
459
+ });
460
+ }
461
+ };
462
+
463
+ // src/api/admin.ts
464
+ var AdminApi = class {
465
+ constructor(request) {
466
+ this.request = request;
467
+ }
468
+ /**
469
+ * List admin puzzles
470
+ *
471
+ * @param params - Optional filter parameters
472
+ * @returns Paginated list of admin puzzles
473
+ *
474
+ * @example
475
+ * ```typescript
476
+ * const result = await client.admin.listPuzzles({
477
+ * type: 'nonogram',
478
+ * status: 'published',
479
+ * page: 1,
480
+ * pageSize: 20,
481
+ * });
482
+ * ```
483
+ */
484
+ async listPuzzles(params) {
485
+ return this.request({
486
+ method: "GET",
487
+ path: "/admin/puzzles",
488
+ query: {
489
+ type: params?.type,
490
+ status: params?.status,
491
+ search: params?.search,
492
+ page: params?.page,
493
+ pageSize: params?.pageSize
494
+ }
495
+ });
496
+ }
497
+ /**
498
+ * List deleted admin puzzles (recycle bin)
499
+ *
500
+ * @param params - Optional filter parameters
501
+ * @returns Paginated list of deleted admin puzzles
502
+ *
503
+ * @example
504
+ * ```typescript
505
+ * const result = await client.admin.listDeletedPuzzles();
506
+ * ```
507
+ */
508
+ async listDeletedPuzzles(params) {
509
+ return this.request({
510
+ method: "GET",
511
+ path: "/admin/puzzles/deleted",
512
+ query: {
513
+ type: params?.type,
514
+ search: params?.search,
515
+ page: params?.page,
516
+ pageSize: params?.pageSize
517
+ }
518
+ });
519
+ }
520
+ /**
521
+ * Get a single admin puzzle by ID
522
+ *
523
+ * @param id - Puzzle ID
524
+ * @returns Admin puzzle with variants
525
+ *
526
+ * @example
527
+ * ```typescript
528
+ * const puzzle = await client.admin.getPuzzle('puzzle-id');
529
+ * ```
530
+ */
531
+ async getPuzzle(id) {
532
+ return this.request({
533
+ method: "GET",
534
+ path: `/admin/puzzles/${id}`
535
+ });
536
+ }
537
+ /**
538
+ * Create a new admin puzzle
539
+ *
540
+ * @param data - Puzzle creation data
541
+ * @returns Created puzzle
542
+ *
543
+ * @example
544
+ * ```typescript
545
+ * const puzzle = await client.admin.createPuzzle({
546
+ * title: 'My Nonogram',
547
+ * type: 'nonogram',
548
+ * });
549
+ * ```
550
+ */
551
+ async createPuzzle(data) {
552
+ return this.request({
553
+ method: "POST",
554
+ path: "/admin/puzzles",
555
+ body: data
556
+ });
557
+ }
558
+ /**
559
+ * Update an admin puzzle
560
+ *
561
+ * @param id - Puzzle ID
562
+ * @param data - Update data
563
+ * @returns Updated puzzle
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * const puzzle = await client.admin.updatePuzzle('puzzle-id', {
568
+ * title: 'Updated Title',
569
+ * status: 'published',
570
+ * });
571
+ * ```
572
+ */
573
+ async updatePuzzle(id, data) {
574
+ return this.request({
575
+ method: "PUT",
576
+ path: `/admin/puzzles/${id}`,
577
+ body: data
578
+ });
579
+ }
580
+ /**
581
+ * Soft delete an admin puzzle (move to recycle bin)
582
+ *
583
+ * @param id - Puzzle ID
584
+ * @returns Success message
585
+ *
586
+ * @example
587
+ * ```typescript
588
+ * await client.admin.deletePuzzle('puzzle-id');
589
+ * ```
590
+ */
591
+ async deletePuzzle(id) {
592
+ return this.request({
593
+ method: "DELETE",
594
+ path: `/admin/puzzles/${id}`
595
+ });
596
+ }
597
+ /**
598
+ * Remove a platform puzzle from the tenant's library.
599
+ * Deactivates all calendar assignments for this puzzle. Only applies to platform puzzles
600
+ * (tenantId=null). For tenant-owned puzzles, use deletePuzzle instead.
601
+ * Requires tenant API key authentication.
602
+ *
603
+ * @param id - Admin puzzle ID
604
+ * @returns Result with deactivatedCount
605
+ *
606
+ * @example
607
+ * ```typescript
608
+ * const result = await client.admin.removeFromLibrary('puzzle-id');
609
+ * console.log(`Deactivated ${result.data.deactivatedCount} calendar assignments`);
610
+ * ```
611
+ */
612
+ async removeFromLibrary(id) {
613
+ return this.request({
614
+ method: "POST",
615
+ path: `/admin/puzzles/${id}/remove-from-library`
616
+ });
617
+ }
618
+ /**
619
+ * Restore a soft-deleted puzzle from recycle bin
620
+ *
621
+ * @param id - Puzzle ID
622
+ * @returns Restored puzzle
623
+ *
624
+ * @example
625
+ * ```typescript
626
+ * const puzzle = await client.admin.restorePuzzle('puzzle-id');
627
+ * ```
628
+ */
629
+ async restorePuzzle(id) {
630
+ return this.request({
631
+ method: "POST",
632
+ path: `/admin/puzzles/${id}/restore`
633
+ });
634
+ }
635
+ /**
636
+ * Permanently delete a puzzle (cannot be undone)
637
+ *
638
+ * @param id - Puzzle ID
639
+ * @returns Success message
640
+ *
641
+ * @example
642
+ * ```typescript
643
+ * await client.admin.permanentlyDeletePuzzle('puzzle-id');
644
+ * ```
645
+ */
646
+ async permanentlyDeletePuzzle(id) {
647
+ return this.request({
648
+ method: "DELETE",
649
+ path: `/admin/puzzles/${id}/permanent`
650
+ });
651
+ }
652
+ /**
653
+ * Publish an admin puzzle
654
+ *
655
+ * @param id - Puzzle ID
656
+ * @returns Published puzzle
657
+ *
658
+ * @example
659
+ * ```typescript
660
+ * const puzzle = await client.admin.publishPuzzle('puzzle-id');
661
+ * ```
662
+ */
663
+ async publishPuzzle(id) {
664
+ return this.request({
665
+ method: "POST",
666
+ path: `/admin/puzzles/${id}/publish`
667
+ });
668
+ }
669
+ /**
670
+ * Unpublish an admin puzzle
671
+ *
672
+ * @param id - Puzzle ID
673
+ * @returns Unpublished puzzle
674
+ *
675
+ * @example
676
+ * ```typescript
677
+ * const puzzle = await client.admin.unpublishPuzzle('puzzle-id');
678
+ * ```
679
+ */
680
+ async unpublishPuzzle(id) {
681
+ return this.request({
682
+ method: "POST",
683
+ path: `/admin/puzzles/${id}/unpublish`
684
+ });
685
+ }
686
+ /**
687
+ * Create or update a puzzle variant
688
+ *
689
+ * @param puzzleId - Puzzle ID
690
+ * @param data - Variant data
691
+ * @returns Created/updated variant
692
+ *
693
+ * @example
694
+ * ```typescript
695
+ * const variant = await client.admin.upsertVariant('puzzle-id', {
696
+ * difficulty: 'medium',
697
+ * enabled: true,
698
+ * width: 15,
699
+ * height: 15,
700
+ * grid_data: [[0, 1, 0], ...],
701
+ * });
702
+ * ```
703
+ */
704
+ async upsertVariant(puzzleId, data) {
705
+ return this.request({
706
+ method: "PUT",
707
+ path: `/admin/puzzles/${puzzleId}/variants`,
708
+ body: data
709
+ });
710
+ }
711
+ /**
712
+ * Delete a puzzle variant
713
+ *
714
+ * @param puzzleId - Puzzle ID
715
+ * @param variantId - Variant ID
716
+ * @returns Success message
717
+ *
718
+ * @example
719
+ * ```typescript
720
+ * await client.admin.deleteVariant('puzzle-id', 'variant-id');
721
+ * ```
722
+ */
723
+ async deleteVariant(puzzleId, variantId) {
724
+ return this.request({
725
+ method: "DELETE",
726
+ path: `/admin/puzzles/${puzzleId}/variants/${variantId}`
727
+ });
728
+ }
729
+ /**
730
+ * Evaluate fun factor of a nonogram puzzle
731
+ *
732
+ * @param data - Grid data and puzzle metadata
733
+ * @returns Fun factor evaluation result
734
+ *
735
+ * @example
736
+ * ```typescript
737
+ * const result = await client.admin.evaluateFunFactor({
738
+ * grid_data: [[0, 1, 0], ...],
739
+ * difficulty: 'medium',
740
+ * width: 15,
741
+ * height: 15,
742
+ * });
743
+ * ```
744
+ */
745
+ async evaluateFunFactor(data) {
746
+ return this.request({
747
+ method: "POST",
748
+ path: "/admin/puzzles/evaluate-fun-factor",
749
+ body: data
750
+ });
751
+ }
752
+ /**
753
+ * Filter a word from a wordsearch puzzle
754
+ *
755
+ * Adds the word to the tenant's filtered word list and removes it from
756
+ * the specified puzzle. The word will not appear in future puzzle generations
757
+ * and is immediately removed from the current puzzle for all users.
758
+ *
759
+ * @param puzzleId - ID of the wordsearch puzzle
760
+ * @param data - Word to filter and optional audit metadata
761
+ * @returns Updated puzzle data with the word removed
762
+ *
763
+ * @example
764
+ * ```typescript
765
+ * const result = await client.admin.filterWordFromPuzzle('puzzle-id', {
766
+ * word: 'offensive',
767
+ * reason: 'Inappropriate content',
768
+ * addedByName: 'Admin User',
769
+ * addedByEmail: 'admin@example.com',
770
+ * });
771
+ * ```
772
+ */
773
+ async filterWordFromPuzzle(puzzleId, data) {
774
+ return this.request({
775
+ method: "POST",
776
+ path: `/v1/puzzles/${puzzleId}/filter-word`,
777
+ body: data
778
+ });
779
+ }
780
+ };
781
+
782
+ // src/client.ts
783
+ var PuzzleSectionClient = class {
784
+ config;
785
+ /**
786
+ * Puzzles API
787
+ */
788
+ puzzles;
789
+ /**
790
+ * Users API
791
+ */
792
+ users;
793
+ /**
794
+ * Progress API
795
+ */
796
+ progress;
797
+ /**
798
+ * Health API
799
+ */
800
+ health;
801
+ /**
802
+ * Admin API
803
+ */
804
+ admin;
805
+ constructor(config) {
806
+ if (!config.apiKey) {
807
+ throw new Error("API key is required");
808
+ }
809
+ this.config = {
810
+ apiKey: config.apiKey,
811
+ baseUrl: config.baseUrl || "https://api.puzzlesection.app",
812
+ timeout: config.timeout || 3e4,
813
+ retryCount: config.retryCount || 3,
814
+ userToken: config.userToken,
815
+ fetch: config.fetch || globalThis.fetch.bind(globalThis)
816
+ };
817
+ this.puzzles = new PuzzlesApi(this.request.bind(this));
818
+ this.users = new UsersApi(this.request.bind(this));
819
+ this.progress = new ProgressApi(this.request.bind(this));
820
+ this.health = new HealthApi(this.request.bind(this));
821
+ this.admin = new AdminApi(this.request.bind(this));
822
+ }
823
+ /**
824
+ * Set the user token for user-specific operations
825
+ */
826
+ setUserToken(token) {
827
+ this.config.userToken = token;
828
+ }
829
+ /**
830
+ * Make an API request
831
+ */
832
+ async request(options) {
833
+ const { method, path, body, query, headers = {} } = options;
834
+ const url = new URL(`${this.config.baseUrl}/api/v1${path}`);
835
+ if (query) {
836
+ for (const [key, value] of Object.entries(query)) {
837
+ if (value !== void 0) {
838
+ url.searchParams.set(key, String(value));
839
+ }
840
+ }
841
+ }
842
+ const requestHeaders = {
843
+ "Content-Type": "application/json",
844
+ "X-API-Key": this.config.apiKey,
845
+ ...headers
846
+ };
847
+ if (this.config.userToken) {
848
+ requestHeaders["X-User-Token"] = this.config.userToken;
849
+ }
850
+ const requestInit = {
851
+ method,
852
+ headers: requestHeaders
853
+ };
854
+ if (body && method !== "GET") {
855
+ requestInit.body = JSON.stringify(body);
856
+ }
857
+ let lastError;
858
+ for (let attempt = 0; attempt <= this.config.retryCount; attempt++) {
859
+ try {
860
+ const response = await this.executeRequest(url.toString(), requestInit);
861
+ return response;
862
+ } catch (error) {
863
+ lastError = error;
864
+ if (error instanceof ApiError) {
865
+ if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
866
+ throw error;
867
+ }
868
+ if (error.statusCode === 429 && attempt < this.config.retryCount) {
869
+ const retryAfter = error.retryAfter || Math.pow(2, attempt);
870
+ await this.sleep(retryAfter * 1e3);
871
+ continue;
872
+ }
873
+ }
874
+ if (attempt < this.config.retryCount) {
875
+ await this.sleep(Math.pow(2, attempt) * 1e3);
876
+ }
877
+ }
878
+ }
879
+ throw lastError;
880
+ }
881
+ /**
882
+ * Execute a single request
883
+ */
884
+ async executeRequest(url, init) {
885
+ const controller = new AbortController();
886
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
887
+ try {
888
+ const response = await this.config.fetch(url, {
889
+ ...init,
890
+ signal: controller.signal
891
+ });
892
+ clearTimeout(timeoutId);
893
+ const rateLimit = {
894
+ limit: parseInt(response.headers.get("X-RateLimit-Limit") || "0", 10),
895
+ remaining: parseInt(response.headers.get("X-RateLimit-Remaining") || "0", 10),
896
+ reset: parseInt(response.headers.get("X-RateLimit-Reset") || "0", 10)
897
+ };
898
+ let body;
899
+ const contentType = response.headers.get("content-type") || "";
900
+ if (contentType && !contentType.includes("application/json")) {
901
+ throw new ApiError(
902
+ `Server returned non-JSON response (HTTP ${response.status})`,
903
+ "INVALID_RESPONSE",
904
+ response.status
905
+ );
906
+ }
907
+ try {
908
+ body = await response.json();
909
+ } catch {
910
+ throw new ApiError(
911
+ `Failed to parse server response (HTTP ${response.status})`,
912
+ "INVALID_RESPONSE",
913
+ response.status
914
+ );
915
+ }
916
+ if (!response.ok || !body.success) {
917
+ throw createErrorFromResponse(
918
+ response.status,
919
+ body,
920
+ response.headers
921
+ );
922
+ }
923
+ return {
924
+ data: body.data,
925
+ rateLimit
926
+ };
927
+ } catch (error) {
928
+ clearTimeout(timeoutId);
929
+ if (error instanceof ApiError) {
930
+ throw error;
931
+ }
932
+ if (error.name === "AbortError") {
933
+ throw new ApiError("Request timed out", "TIMEOUT", 408);
934
+ }
935
+ throw new ApiError(
936
+ `Network error: ${error.message}`,
937
+ "NETWORK_ERROR",
938
+ 0
939
+ );
940
+ }
941
+ }
942
+ /**
943
+ * Sleep for a given duration
944
+ */
945
+ sleep(ms) {
946
+ return new Promise((resolve) => setTimeout(resolve, ms));
947
+ }
948
+ };
949
+ export {
950
+ ApiError,
951
+ AuthenticationError,
952
+ NotFoundError,
953
+ PuzzleSectionClient,
954
+ RateLimitError,
955
+ ServerError,
956
+ ValidationError
957
+ };