@cstar.help/js 0.5.2 → 0.6.1

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
@@ -1,10 +1,13 @@
1
1
  // src/errors.ts
2
2
  var ErrorType = {
3
3
  AUTHENTICATION_ERROR: "authentication_error",
4
+ PERMISSION_ERROR: "permission_error",
4
5
  INVALID_REQUEST_ERROR: "invalid_request_error",
5
6
  NOT_FOUND_ERROR: "not_found_error",
7
+ CONFLICT_ERROR: "conflict_error",
6
8
  RATE_LIMIT_ERROR: "rate_limit_error",
7
- API_ERROR: "api_error"
9
+ API_ERROR: "api_error",
10
+ SERVER_ERROR: "server_error"
8
11
  };
9
12
  var CStarError = class extends Error {
10
13
  constructor(statusCode, body) {
@@ -43,6 +46,24 @@ var CStarValidationError = class extends CStarError {
43
46
  this.name = "CStarValidationError";
44
47
  }
45
48
  };
49
+ var CStarPermissionError = class extends CStarError {
50
+ constructor(statusCode, body) {
51
+ super(statusCode, body);
52
+ this.name = "CStarPermissionError";
53
+ }
54
+ };
55
+ var CStarConflictError = class extends CStarError {
56
+ constructor(statusCode, body) {
57
+ super(statusCode, body);
58
+ this.name = "CStarConflictError";
59
+ }
60
+ };
61
+ var CStarServerError = class extends CStarError {
62
+ constructor(statusCode, body) {
63
+ super(statusCode, body);
64
+ this.name = "CStarServerError";
65
+ }
66
+ };
46
67
  function parseError(statusCode, body, retryAfter) {
47
68
  const errorBody = body?.error ?? {
48
69
  type: ErrorType.API_ERROR,
@@ -53,19 +74,28 @@ function parseError(statusCode, body, retryAfter) {
53
74
  switch (errorBody.type) {
54
75
  case ErrorType.AUTHENTICATION_ERROR:
55
76
  return new CStarAuthenticationError(statusCode, errorBody);
77
+ case ErrorType.PERMISSION_ERROR:
78
+ return new CStarPermissionError(statusCode, errorBody);
56
79
  case ErrorType.RATE_LIMIT_ERROR:
57
80
  return new CStarRateLimitError(statusCode, errorBody, retryAfter ?? 60);
58
81
  case ErrorType.NOT_FOUND_ERROR:
59
82
  return new CStarNotFoundError(statusCode, errorBody);
60
83
  case ErrorType.INVALID_REQUEST_ERROR:
61
84
  return new CStarValidationError(statusCode, errorBody);
85
+ case ErrorType.CONFLICT_ERROR:
86
+ return new CStarConflictError(statusCode, errorBody);
87
+ case ErrorType.SERVER_ERROR:
88
+ return new CStarServerError(statusCode, errorBody);
62
89
  default:
90
+ if (statusCode === 403) return new CStarPermissionError(statusCode, errorBody);
91
+ if (statusCode === 409) return new CStarConflictError(statusCode, errorBody);
92
+ if (statusCode >= 500) return new CStarServerError(statusCode, errorBody);
63
93
  return new CStarError(statusCode, errorBody);
64
94
  }
65
95
  }
66
96
 
67
97
  // src/client.ts
68
- var SDK_VERSION = "0.5.0";
98
+ var SDK_VERSION = "0.5.2";
69
99
  var DEFAULT_BASE_URL = "https://www.cstar.help";
70
100
  var DEFAULT_API_VERSION = "2026-03-01";
71
101
  var DEFAULT_MAX_RETRIES = 3;
@@ -122,10 +152,21 @@ var HttpClient = class {
122
152
  }
123
153
  if (response.ok) {
124
154
  if (response.status === 204) return void 0;
125
- return await response.json();
155
+ const json = await response.json();
156
+ const requestId2 = response.headers.get("x-request-id") ?? response.headers.get("x-cstar-request-id");
157
+ if (requestId2 && json && typeof json === "object" && "meta" in json) {
158
+ const existing = json.meta;
159
+ const metaBase = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
160
+ json.meta = { ...metaBase, requestId: requestId2 };
161
+ }
162
+ return json;
126
163
  }
127
164
  const errorBody = await response.json().catch(() => null);
128
165
  const retryAfter = parseInt(response.headers.get("Retry-After") ?? "", 10) || void 0;
166
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("x-cstar-request-id");
167
+ if (requestId && errorBody?.error) {
168
+ errorBody.error.request_id = requestId;
169
+ }
129
170
  if (response.status < 500 && response.status !== 429) {
130
171
  throw parseError(response.status, errorBody, retryAfter);
131
172
  }
@@ -169,6 +210,14 @@ var HttpClient = class {
169
210
  }
170
211
  return headers;
171
212
  }
213
+ /** Get the base URL for building SSE or other endpoints. */
214
+ getBaseUrl() {
215
+ return `${this.config.baseUrl}/api/v1/teams/${this.config.teamId}`;
216
+ }
217
+ /** Get authorization headers for non-fetch requests (e.g., SSE). */
218
+ getAuthHeaders() {
219
+ return this.buildHeaders();
220
+ }
172
221
  /** Exponential backoff: 500ms, 1s, 2s, 4s... capped at 8s, plus jitter. */
173
222
  getBackoffDelay(attempt) {
174
223
  const base = Math.min(500 * Math.pow(2, attempt - 1), 8e3);
@@ -180,6 +229,504 @@ var HttpClient = class {
180
229
  }
181
230
  };
182
231
 
232
+ // src/realtime.ts
233
+ var CStarRealtime = class {
234
+ constructor(httpClient, options) {
235
+ this.httpClient = httpClient;
236
+ this.eventSource = null;
237
+ this.handlers = /* @__PURE__ */ new Map();
238
+ this.anyHandlers = /* @__PURE__ */ new Set();
239
+ this.errorHandlers = /* @__PURE__ */ new Set();
240
+ this.connectionHandlers = /* @__PURE__ */ new Set();
241
+ this.lastEventId = null;
242
+ this.retryCount = 0;
243
+ this.retryTimer = null;
244
+ this._connected = false;
245
+ this.destroyed = false;
246
+ this.connecting = false;
247
+ /** Short-lived SSE token (exchanged from secret key). */
248
+ this.sseToken = null;
249
+ /** When the current SSE token expires (epoch ms). */
250
+ this.sseTokenExpiresAt = 0;
251
+ /** Timer that refreshes the SSE token before it expires. */
252
+ this.tokenRefreshTimer = null;
253
+ this.autoReconnect = options?.autoReconnect ?? true;
254
+ this.maxRetries = options?.maxRetries ?? 10;
255
+ this.types = options?.types;
256
+ }
257
+ /** Subscribe to a specific event type. Supports wildcards (e.g., 'ticket.*'). Returns an unsubscribe function. */
258
+ on(eventType, handler) {
259
+ let set = this.handlers.get(eventType);
260
+ if (!set) {
261
+ set = /* @__PURE__ */ new Set();
262
+ this.handlers.set(eventType, set);
263
+ }
264
+ set.add(handler);
265
+ this.ensureConnected();
266
+ return () => {
267
+ set.delete(handler);
268
+ };
269
+ }
270
+ /** Subscribe to all events. Returns an unsubscribe function. */
271
+ onAny(handler) {
272
+ this.anyHandlers.add(handler);
273
+ this.ensureConnected();
274
+ return () => {
275
+ this.anyHandlers.delete(handler);
276
+ };
277
+ }
278
+ /** Handle connection errors. */
279
+ onError(handler) {
280
+ this.errorHandlers.add(handler);
281
+ return this;
282
+ }
283
+ /** Handle connection state changes. */
284
+ onConnectionChange(handler) {
285
+ this.connectionHandlers.add(handler);
286
+ return this;
287
+ }
288
+ /** Remove a connection state handler. */
289
+ offConnectionChange(handler) {
290
+ this.connectionHandlers.delete(handler);
291
+ return this;
292
+ }
293
+ /** Remove a handler for a specific event type. */
294
+ off(eventType, handler) {
295
+ const set = this.handlers.get(eventType);
296
+ if (set) {
297
+ set.delete(handler);
298
+ if (set.size === 0) this.handlers.delete(eventType);
299
+ }
300
+ return this;
301
+ }
302
+ /** Remove a handler from the "any" subscription. */
303
+ offAny(handler) {
304
+ this.anyHandlers.delete(handler);
305
+ return this;
306
+ }
307
+ /** Start the SSE connection. Called automatically on first handler registration. */
308
+ connect() {
309
+ if (this.eventSource || this.destroyed || this.connecting) return;
310
+ this.connecting = true;
311
+ const headers = this.httpClient.getAuthHeaders();
312
+ const rawKey = headers.Authorization?.replace("Bearer ", "") ?? "";
313
+ if (rawKey.startsWith("sk_") && !this.hasFreshSseToken()) {
314
+ this.exchangeToken().then(() => {
315
+ this.connecting = false;
316
+ this.connect();
317
+ }).catch(() => {
318
+ this.connecting = false;
319
+ this.connectWithToken(rawKey);
320
+ });
321
+ return;
322
+ }
323
+ const token = this.sseToken ?? rawKey;
324
+ this.connectWithToken(token);
325
+ }
326
+ /**
327
+ * Open the EventSource with the given auth token.
328
+ * Extracted so both the happy path and the fallback path share logic.
329
+ */
330
+ connectWithToken(authToken) {
331
+ const baseUrl = this.httpClient.getBaseUrl();
332
+ const params = new URLSearchParams();
333
+ if (this.types && this.types.length > 0) {
334
+ params.set("types", this.types.join(","));
335
+ }
336
+ if (this.lastEventId) {
337
+ params.set("since", this.lastEventId);
338
+ }
339
+ const qs = params.toString();
340
+ const url = `${baseUrl}/events/stream${qs ? `?${qs}` : ""}`;
341
+ if (authToken.startsWith("sk_") && typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined") {
342
+ console.warn(
343
+ "[cStar] Secret API key is being passed via SSE query parameter. Query strings may be logged by proxies and CDNs. For production, use a server-side proxy to add the Authorization header."
344
+ );
345
+ }
346
+ const separator = qs ? "&" : "?";
347
+ const authenticatedUrl = authToken ? `${url}${separator}token=${authToken}` : url;
348
+ this.eventSource = new EventSource(authenticatedUrl);
349
+ this.eventSource.onopen = () => {
350
+ this.connecting = false;
351
+ this.retryCount = 0;
352
+ this.setConnected(true);
353
+ };
354
+ this.eventSource.onerror = () => {
355
+ this.connecting = false;
356
+ this.setConnected(false);
357
+ if (this.eventSource) {
358
+ this.eventSource.close();
359
+ this.eventSource = null;
360
+ }
361
+ if (this.autoReconnect && !this.destroyed && this.retryCount < this.maxRetries) {
362
+ this.scheduleReconnect();
363
+ } else if (this.retryCount >= this.maxRetries) {
364
+ this.emitError(new Error(`Realtime connection failed after ${this.maxRetries} retries`));
365
+ }
366
+ };
367
+ this.eventSource.onmessage = (event) => {
368
+ this.handleMessage(event);
369
+ };
370
+ const knownTypes = [
371
+ "ticket.created",
372
+ "ticket.updated",
373
+ "ticket.closed",
374
+ "ticket.assigned",
375
+ "message.created",
376
+ "message.updated",
377
+ "notification.created",
378
+ "mention.created",
379
+ // Gamification events (future)
380
+ "boss.spawned",
381
+ "boss.damaged",
382
+ "boss.defeated",
383
+ "achievement.unlocked",
384
+ "quest.completed",
385
+ "level.up"
386
+ ];
387
+ for (const type of knownTypes) {
388
+ this.eventSource.addEventListener(type, (event) => {
389
+ const msgEvent = event;
390
+ if (msgEvent.lastEventId) {
391
+ this.lastEventId = msgEvent.lastEventId;
392
+ }
393
+ try {
394
+ const parsed = JSON.parse(msgEvent.data);
395
+ const realtimeEvent = {
396
+ type: parsed.type || type,
397
+ id: msgEvent.lastEventId || "",
398
+ data: parsed.data || parsed,
399
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
400
+ teamId: parsed.teamId || ""
401
+ };
402
+ this.dispatch(realtimeEvent);
403
+ } catch {
404
+ }
405
+ });
406
+ }
407
+ }
408
+ /** Stop listening and clean up all resources. */
409
+ disconnect() {
410
+ this.destroyed = true;
411
+ if (this.retryTimer) {
412
+ clearTimeout(this.retryTimer);
413
+ this.retryTimer = null;
414
+ }
415
+ if (this.tokenRefreshTimer) {
416
+ clearTimeout(this.tokenRefreshTimer);
417
+ this.tokenRefreshTimer = null;
418
+ }
419
+ if (this.eventSource) {
420
+ this.eventSource.close();
421
+ this.eventSource = null;
422
+ }
423
+ this.sseToken = null;
424
+ this.sseTokenExpiresAt = 0;
425
+ this.setConnected(false);
426
+ this.handlers.clear();
427
+ this.anyHandlers.clear();
428
+ this.errorHandlers.clear();
429
+ this.connectionHandlers.clear();
430
+ }
431
+ /** Whether the SSE connection is currently open. */
432
+ get connected() {
433
+ return this._connected;
434
+ }
435
+ // ── Internal ──
436
+ ensureConnected() {
437
+ if (!this.eventSource && !this.destroyed) {
438
+ this.connect();
439
+ }
440
+ }
441
+ handleMessage(event) {
442
+ if (event.lastEventId) {
443
+ this.lastEventId = event.lastEventId;
444
+ }
445
+ try {
446
+ const parsed = JSON.parse(event.data);
447
+ if (parsed.type === "connected") return;
448
+ const realtimeEvent = {
449
+ type: parsed.type,
450
+ id: event.lastEventId || "",
451
+ data: parsed.data || parsed,
452
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
453
+ teamId: parsed.teamId || ""
454
+ };
455
+ this.dispatch(realtimeEvent);
456
+ } catch {
457
+ }
458
+ }
459
+ dispatch(event) {
460
+ const exactSet = this.handlers.get(event.type);
461
+ if (exactSet) {
462
+ for (const handler of exactSet) handler(event);
463
+ }
464
+ for (const [pattern, handlerSet] of this.handlers) {
465
+ if (pattern.endsWith(".*")) {
466
+ const prefix = pattern.slice(0, -2);
467
+ if (event.type.startsWith(prefix + ".") && pattern !== event.type) {
468
+ for (const handler of handlerSet) handler(event);
469
+ }
470
+ }
471
+ }
472
+ for (const handler of this.anyHandlers) handler(event);
473
+ }
474
+ scheduleReconnect() {
475
+ this.retryCount++;
476
+ const delay = Math.min(500 * Math.pow(2, this.retryCount - 1), 8e3) + Math.random() * 200;
477
+ this.retryTimer = setTimeout(() => {
478
+ this.retryTimer = null;
479
+ if (!this.destroyed) {
480
+ this.connect();
481
+ }
482
+ }, delay);
483
+ }
484
+ setConnected(value) {
485
+ if (this._connected !== value) {
486
+ this._connected = value;
487
+ for (const handler of this.connectionHandlers) handler(value);
488
+ }
489
+ }
490
+ emitError(error) {
491
+ for (const handler of this.errorHandlers) handler(error);
492
+ }
493
+ // ── SSE Token Exchange ──
494
+ /** Whether we hold a token that won't expire for at least 30 seconds. */
495
+ hasFreshSseToken() {
496
+ return !!this.sseToken && this.sseTokenExpiresAt - Date.now() > 3e4;
497
+ }
498
+ /**
499
+ * Exchange the secret API key for a short-lived SSE token via
500
+ * POST /events/token. Schedules automatic refresh before expiry.
501
+ */
502
+ async exchangeToken() {
503
+ const response = await this.httpClient.request("POST", "/events/token");
504
+ const { token, expiresAt, expiresIn } = response.data;
505
+ this.sseToken = token;
506
+ this.sseTokenExpiresAt = new Date(expiresAt).getTime();
507
+ const refreshIn = Math.max((expiresIn - 30) * 1e3, expiresIn / 2 * 1e3);
508
+ this.scheduleTokenRefresh(refreshIn);
509
+ }
510
+ /**
511
+ * Schedule a background token refresh. On success the existing connection
512
+ * keeps working (tokens are validated at connect time). On the next
513
+ * reconnect the fresh token will be used automatically.
514
+ */
515
+ scheduleTokenRefresh(delayMs) {
516
+ if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
517
+ this.tokenRefreshTimer = setTimeout(() => {
518
+ this.tokenRefreshTimer = null;
519
+ if (this.destroyed) return;
520
+ this.exchangeToken().catch(() => {
521
+ });
522
+ }, delayMs);
523
+ }
524
+ };
525
+
526
+ // src/subscription.ts
527
+ var RESOURCE_EVENT_MAP = {
528
+ "/tickets": ["ticket.created", "ticket.updated", "ticket.closed", "ticket.assigned"],
529
+ "/customers": ["customer.created", "customer.updated"],
530
+ "/articles": ["article.created", "article.updated", "article.published"],
531
+ "/messages": ["message.created", "message.updated"],
532
+ "/notifications": ["notification.created"],
533
+ "/mentions": ["mention.created"],
534
+ "/game/boss": ["boss.spawned", "boss.damaged", "boss.defeated"],
535
+ "/game/player": ["achievement.unlocked", "quest.completed", "level.up", "player.updated"],
536
+ "/game/leaderboard": [
537
+ "leaderboard.updated",
538
+ "achievement.unlocked",
539
+ "quest.completed",
540
+ "level.up"
541
+ ],
542
+ "/game/activity": [
543
+ "boss.spawned",
544
+ "boss.defeated",
545
+ "achievement.unlocked",
546
+ "level.up",
547
+ "quest.completed"
548
+ ]
549
+ };
550
+ function getEntityId(item) {
551
+ if (!item || typeof item !== "object") return void 0;
552
+ const obj = item;
553
+ return obj.id ?? obj.userId ?? void 0;
554
+ }
555
+ var Subscription = class {
556
+ constructor(realtime, fetchFn, resourcePath, filterFn) {
557
+ this.realtime = realtime;
558
+ this.fetchFn = fetchFn;
559
+ this.resourcePath = resourcePath;
560
+ this.filterFn = filterFn;
561
+ this._data = [];
562
+ this._loading = true;
563
+ this._error = null;
564
+ this.changeHandlers = /* @__PURE__ */ new Set();
565
+ this.errorEventHandlers = /* @__PURE__ */ new Set();
566
+ this.boundEventHandler = null;
567
+ this.destroyed = false;
568
+ /** Generation counter to prevent stale refresh() from overwriting SSE updates. */
569
+ this.generation = 0;
570
+ this.init();
571
+ }
572
+ /** Current data array (updated in real-time). */
573
+ get data() {
574
+ return this._data;
575
+ }
576
+ /** True during initial data fetch. */
577
+ get loading() {
578
+ return this._loading;
579
+ }
580
+ /** Last error, if any. */
581
+ get error() {
582
+ return this._error;
583
+ }
584
+ on(event, handler) {
585
+ if (event === "change") {
586
+ this.changeHandlers.add(handler);
587
+ } else if (event === "error") {
588
+ this.errorEventHandlers.add(handler);
589
+ }
590
+ return this;
591
+ }
592
+ /** Manually refresh data from the server. */
593
+ async refresh() {
594
+ const gen = ++this.generation;
595
+ try {
596
+ const response = await this.fetchFn();
597
+ if (gen !== this.generation) return;
598
+ this._data = response.data;
599
+ this._error = null;
600
+ this.emitChange();
601
+ } catch (err) {
602
+ if (gen !== this.generation) return;
603
+ this._error = err instanceof Error ? err : new Error(String(err));
604
+ this.emitError(this._error);
605
+ }
606
+ }
607
+ /**
608
+ * Apply an optimistic update to the local data.
609
+ * Returns a rollback function that reverts the change.
610
+ */
611
+ optimisticUpdate(id, changes) {
612
+ const snapshot = [...this._data];
613
+ const index = this._data.findIndex((item) => getEntityId(item) === id);
614
+ if (index !== -1) {
615
+ this._data[index] = { ...this._data[index], ...changes };
616
+ if (this.filterFn && !this.filterFn(this._data[index])) {
617
+ this._data.splice(index, 1);
618
+ }
619
+ this.emitChange();
620
+ }
621
+ return () => {
622
+ this._data = snapshot;
623
+ this.emitChange();
624
+ };
625
+ }
626
+ /** Stop the subscription and clean up all listeners. */
627
+ unsubscribe() {
628
+ this.destroyed = true;
629
+ if (this.boundEventHandler) {
630
+ this.realtime.offAny(this.boundEventHandler);
631
+ this.boundEventHandler = null;
632
+ }
633
+ this.changeHandlers.clear();
634
+ this.errorEventHandlers.clear();
635
+ }
636
+ // ── Internal ──
637
+ async init() {
638
+ this.boundEventHandler = (event) => this.handleEvent(event);
639
+ this.realtime.onAny(this.boundEventHandler);
640
+ const gen = ++this.generation;
641
+ try {
642
+ const response = await this.fetchFn();
643
+ if (gen !== this.generation) return;
644
+ this._data = response.data;
645
+ this._loading = false;
646
+ this._error = null;
647
+ this.emitChange();
648
+ } catch (err) {
649
+ if (gen !== this.generation) return;
650
+ this._loading = false;
651
+ this._error = err instanceof Error ? err : new Error(String(err));
652
+ this.emitError(this._error);
653
+ }
654
+ }
655
+ handleEvent(event) {
656
+ if (this.destroyed) return;
657
+ const eventTypes = RESOURCE_EVENT_MAP[this.resourcePath];
658
+ if (eventTypes) {
659
+ const isRelevant = eventTypes.some((t) => {
660
+ const prefix = t.split(".")[0];
661
+ return event.type.startsWith(prefix + ".");
662
+ });
663
+ if (!isRelevant) return;
664
+ } else {
665
+ const eventPrefix = event.type.split(".")[0];
666
+ const segments = this.resourcePath.replace(/^\//, "").split("/");
667
+ const resourceName = segments[segments.length - 1];
668
+ if (resourceName !== eventPrefix && resourceName !== eventPrefix + "s") return;
669
+ }
670
+ const entityData = this.extractEntity(event);
671
+ if (!entityData || !getEntityId(entityData)) return;
672
+ this.generation++;
673
+ if (event.type.endsWith(".created")) {
674
+ this.handleCreated(entityData);
675
+ } else if (event.type.endsWith(".deleted")) {
676
+ this.handleDeleted(entityData);
677
+ } else if (event.type.endsWith(".updated") || event.type.endsWith(".assigned") || event.type.endsWith(".closed")) {
678
+ this.handleUpdated(entityData);
679
+ }
680
+ }
681
+ handleCreated(entity) {
682
+ if (this.filterFn && !this.filterFn(entity)) return;
683
+ const entityId = getEntityId(entity);
684
+ if (entityId && this._data.some((item) => getEntityId(item) === entityId)) return;
685
+ this._data = [entity, ...this._data];
686
+ this.emitChange();
687
+ }
688
+ handleDeleted(entity) {
689
+ const entityId = getEntityId(entity);
690
+ if (!entityId) return;
691
+ const index = this._data.findIndex((item) => getEntityId(item) === entityId);
692
+ if (index !== -1) {
693
+ this._data = [...this._data.slice(0, index), ...this._data.slice(index + 1)];
694
+ this.emitChange();
695
+ }
696
+ }
697
+ handleUpdated(entity) {
698
+ const entityId = getEntityId(entity);
699
+ const index = entityId ? this._data.findIndex((item) => getEntityId(item) === entityId) : -1;
700
+ if (index !== -1) {
701
+ const updated = { ...this._data[index], ...entity };
702
+ if (this.filterFn && !this.filterFn(updated)) {
703
+ this._data = [...this._data.slice(0, index), ...this._data.slice(index + 1)];
704
+ } else {
705
+ this._data = [...this._data.slice(0, index), updated, ...this._data.slice(index + 1)];
706
+ }
707
+ this.emitChange();
708
+ } else if (this.filterFn && this.filterFn(entity)) {
709
+ this._data = [entity, ...this._data];
710
+ this.emitChange();
711
+ }
712
+ }
713
+ extractEntity(event) {
714
+ const data = event.data;
715
+ const resourceKey = this.resourcePath.replace(/^\//, "").replace(/s$/, "");
716
+ if (data[resourceKey] && typeof data[resourceKey] === "object") {
717
+ return data[resourceKey];
718
+ }
719
+ if (data.id) return data;
720
+ return null;
721
+ }
722
+ emitChange() {
723
+ for (const handler of this.changeHandlers) handler(this._data);
724
+ }
725
+ emitError(error) {
726
+ for (const handler of this.errorEventHandlers) handler(error);
727
+ }
728
+ };
729
+
183
730
  // src/resources/base.ts
184
731
  var BaseResource = class {
185
732
  constructor(client, basePath) {
@@ -238,6 +785,22 @@ var BaseResource = class {
238
785
  page++;
239
786
  }
240
787
  }
788
+ /**
789
+ * Subscribe to real-time changes for this resource type.
790
+ *
791
+ * Fetches initial data, then auto-updates via SSE events.
792
+ * Requires a `CStarRealtime` instance (available from `CStarClient.realtime`).
793
+ *
794
+ * ```typescript
795
+ * const sub = cstar.tickets.subscribe(cstar.realtime, { status: 'open' });
796
+ * sub.on('change', (tickets) => console.log(tickets));
797
+ * // Later: sub.unsubscribe();
798
+ * ```
799
+ */
800
+ subscribe(realtime, params, filterFn) {
801
+ const fetchFn = () => this.list(params);
802
+ return new Subscription(realtime, fetchFn, this.basePath, filterFn);
803
+ }
241
804
  /** Convert params to string values for query string serialization. */
242
805
  serializeQuery(params) {
243
806
  const result = {};
@@ -322,7 +885,7 @@ var WebhooksResource = class extends BaseResource {
322
885
  );
323
886
  }
324
887
  /** Subscribe to an event (REST Hooks / Zapier protocol). */
325
- async subscribe(params, options) {
888
+ async restSubscribe(params, options) {
326
889
  return this.client.request("POST", "/webhooks/subscribe", {
327
890
  body: params,
328
891
  idempotencyKey: options?.idempotencyKey
@@ -1315,6 +1878,72 @@ var GameResource = class {
1315
1878
  async config() {
1316
1879
  return this.client.request("GET", "/game/config");
1317
1880
  }
1881
+ // ── Real-time Subscriptions ──
1882
+ /**
1883
+ * Subscribe to live boss battle state.
1884
+ * Updates on boss.spawned, boss.damaged, boss.defeated events.
1885
+ */
1886
+ subscribeBoss(realtime) {
1887
+ const fetchFn = async () => {
1888
+ const res = await this.boss.current();
1889
+ return {
1890
+ success: true,
1891
+ data: res.data.active ? [res.data] : [],
1892
+ pagination: { total: res.data.active ? 1 : 0, page: 1, pageSize: 1, hasMore: false },
1893
+ meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1894
+ };
1895
+ };
1896
+ return new Subscription(realtime, fetchFn, "/game/boss");
1897
+ }
1898
+ /**
1899
+ * Subscribe to player stats (XP, gold, level, streaks).
1900
+ * Updates on achievement.unlocked, quest.completed, level.up events.
1901
+ */
1902
+ subscribePlayer(realtime, userId) {
1903
+ const fetchFn = async () => {
1904
+ const res = await this.player({ userId });
1905
+ return {
1906
+ success: true,
1907
+ data: [res.data],
1908
+ pagination: { total: 1, page: 1, pageSize: 1, hasMore: false },
1909
+ meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1910
+ };
1911
+ };
1912
+ return new Subscription(realtime, fetchFn, "/game/player");
1913
+ }
1914
+ /**
1915
+ * Subscribe to the team leaderboard.
1916
+ * Updates when scores change.
1917
+ */
1918
+ subscribeLeaderboard(realtime, params) {
1919
+ const fetchFn = async () => {
1920
+ const res = await this.leaderboard(params);
1921
+ return {
1922
+ success: true,
1923
+ data: res.data,
1924
+ pagination: { total: res.data.length, page: 1, pageSize: 100, hasMore: false },
1925
+ meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1926
+ };
1927
+ };
1928
+ return new Subscription(realtime, fetchFn, "/game/leaderboard");
1929
+ }
1930
+ /**
1931
+ * Subscribe to team activity feed (achievements, boss defeats, level ups).
1932
+ * Rolling feed of recent game events.
1933
+ */
1934
+ subscribeActivity(realtime) {
1935
+ const fetchFn = async () => {
1936
+ const res = await this.activity.list({ limit: 50 });
1937
+ const data = res.data.data || [];
1938
+ return {
1939
+ success: true,
1940
+ data,
1941
+ pagination: { total: data.length, page: 1, pageSize: 50, hasMore: false },
1942
+ meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1943
+ };
1944
+ };
1945
+ return new Subscription(realtime, fetchFn, "/game/activity");
1946
+ }
1318
1947
  };
1319
1948
 
1320
1949
  // src/resources/ai.ts
@@ -1377,52 +2006,190 @@ var BillingResource = class {
1377
2006
  }
1378
2007
  };
1379
2008
 
2009
+ // src/offline.ts
2010
+ var STORAGE_KEY = "cstar_offline_queue";
2011
+ var OfflineQueue = class {
2012
+ constructor(httpClient, options) {
2013
+ this.httpClient = httpClient;
2014
+ this.items = [];
2015
+ this.autoFlushEnabled = false;
2016
+ this.onlineHandler = null;
2017
+ this.useLocalStorage = options?.storage === "localStorage" && typeof globalThis !== "undefined" && typeof globalThis.localStorage !== "undefined";
2018
+ if (this.useLocalStorage) {
2019
+ this.loadFromStorage();
2020
+ }
2021
+ }
2022
+ /** Queue a mutation for later execution. Returns a queue item ID. */
2023
+ enqueue(method, path, body) {
2024
+ const id = `q_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2025
+ const item = {
2026
+ id,
2027
+ method,
2028
+ path,
2029
+ body,
2030
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString()
2031
+ };
2032
+ this.items.push(item);
2033
+ this.saveToStorage();
2034
+ return id;
2035
+ }
2036
+ /**
2037
+ * Process all queued mutations in order. Returns results for each item.
2038
+ * Failed items are re-queued at the front to preserve ordering.
2039
+ * Items enqueued during flush are appended after re-queued failures.
2040
+ */
2041
+ async flush() {
2042
+ const results = [];
2043
+ const itemsToProcess = [...this.items];
2044
+ this.items = [];
2045
+ this.saveToStorage();
2046
+ const failedItems = [];
2047
+ for (const item of itemsToProcess) {
2048
+ try {
2049
+ const data = await this.httpClient.request(item.method, item.path, {
2050
+ body: item.body
2051
+ });
2052
+ results.push({ id: item.id, success: true, data });
2053
+ } catch (err) {
2054
+ const message = err instanceof Error ? err.message : String(err);
2055
+ results.push({ id: item.id, success: false, error: message });
2056
+ const statusCode = err.statusCode;
2057
+ const isRetryable = !statusCode || statusCode >= 500;
2058
+ if (isRetryable) {
2059
+ failedItems.push(item);
2060
+ }
2061
+ }
2062
+ }
2063
+ this.items = [...failedItems, ...this.items];
2064
+ this.saveToStorage();
2065
+ return results;
2066
+ }
2067
+ /** Get pending queue items. */
2068
+ get pending() {
2069
+ return [...this.items];
2070
+ }
2071
+ /** Number of items in the queue. */
2072
+ get size() {
2073
+ return this.items.length;
2074
+ }
2075
+ /** Clear all pending items. */
2076
+ clear() {
2077
+ this.items = [];
2078
+ this.saveToStorage();
2079
+ }
2080
+ /**
2081
+ * Enable auto-flush when the browser goes online.
2082
+ * Only works in browser environments with the `online` event.
2083
+ */
2084
+ enableAutoFlush() {
2085
+ if (this.autoFlushEnabled) return;
2086
+ if (typeof globalThis === "undefined" || typeof globalThis.addEventListener !== "function") {
2087
+ return;
2088
+ }
2089
+ this.autoFlushEnabled = true;
2090
+ this.onlineHandler = () => {
2091
+ if (this.items.length > 0) {
2092
+ this.flush().catch(() => {
2093
+ });
2094
+ }
2095
+ };
2096
+ globalThis.addEventListener("online", this.onlineHandler);
2097
+ }
2098
+ /** Disable auto-flush and clean up event listeners. */
2099
+ disableAutoFlush() {
2100
+ if (!this.autoFlushEnabled || !this.onlineHandler) return;
2101
+ if (typeof globalThis !== "undefined" && typeof globalThis.removeEventListener === "function") {
2102
+ globalThis.removeEventListener("online", this.onlineHandler);
2103
+ }
2104
+ this.onlineHandler = null;
2105
+ this.autoFlushEnabled = false;
2106
+ }
2107
+ saveToStorage() {
2108
+ if (!this.useLocalStorage) return;
2109
+ try {
2110
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.items));
2111
+ } catch {
2112
+ }
2113
+ }
2114
+ loadFromStorage() {
2115
+ try {
2116
+ const raw = localStorage.getItem(STORAGE_KEY);
2117
+ if (raw) {
2118
+ this.items = JSON.parse(raw);
2119
+ }
2120
+ } catch {
2121
+ this.items = [];
2122
+ }
2123
+ }
2124
+ };
2125
+
1380
2126
  // src/index.ts
1381
2127
  var CStarClient = class {
1382
2128
  constructor(config) {
1383
- this.client = new HttpClient(config);
1384
- this.tickets = new TicketsResource(this.client);
1385
- this.customers = new CustomersResource(this.client);
1386
- this.articles = new ArticlesResource(this.client);
1387
- this.webhooks = new WebhooksResource(this.client);
1388
- this.automations = new AutomationsResource(this.client);
1389
- this.categories = new CategoriesResource(this.client);
2129
+ this._httpClient = new HttpClient(config);
2130
+ this.tickets = new TicketsResource(this._httpClient);
2131
+ this.customers = new CustomersResource(this._httpClient);
2132
+ this.articles = new ArticlesResource(this._httpClient);
2133
+ this.webhooks = new WebhooksResource(this._httpClient);
2134
+ this.automations = new AutomationsResource(this._httpClient);
2135
+ this.categories = new CategoriesResource(this._httpClient);
1390
2136
  this.community = {
1391
- posts: new CommunityPostsResource(this.client),
1392
- topics: new CommunityTopicsResource(this.client)
2137
+ posts: new CommunityPostsResource(this._httpClient),
2138
+ topics: new CommunityTopicsResource(this._httpClient)
1393
2139
  };
1394
- this.members = new MembersResource(this.client);
1395
- this.settings = new SettingsResource(this.client);
1396
- this.notifications = new NotificationsResource(this.client);
1397
- this.mentions = new MentionsResource(this.client);
1398
- this.attachments = new AttachmentsResource(this.client);
1399
- this.customFields = new CustomFieldsResource(this.client);
1400
- this.sla = new SlaResource(this.client);
1401
- this.analytics = new AnalyticsResource(this.client);
1402
- this.bulk = new BulkResource(this.client);
1403
- this.export = new ExportResource(this.client);
1404
- this.import = new ImportResource(this.client);
1405
- this.auditLog = new AuditLogResource(this.client);
1406
- this.search = new SearchResource(this.client);
1407
- this.tags = new TagsResource(this.client);
1408
- this.activity = new ActivityResource(this.client);
1409
- this.customerGroups = new CustomerGroupsResource(this.client);
1410
- this.game = new GameResource(this.client);
1411
- this.ai = new AiResource(this.client);
1412
- this.views = new ViewsResource(this.client);
1413
- this.billing = new BillingResource(this.client);
2140
+ this.members = new MembersResource(this._httpClient);
2141
+ this.settings = new SettingsResource(this._httpClient);
2142
+ this.notifications = new NotificationsResource(this._httpClient);
2143
+ this.mentions = new MentionsResource(this._httpClient);
2144
+ this.attachments = new AttachmentsResource(this._httpClient);
2145
+ this.customFields = new CustomFieldsResource(this._httpClient);
2146
+ this.sla = new SlaResource(this._httpClient);
2147
+ this.analytics = new AnalyticsResource(this._httpClient);
2148
+ this.bulk = new BulkResource(this._httpClient);
2149
+ this.export = new ExportResource(this._httpClient);
2150
+ this.import = new ImportResource(this._httpClient);
2151
+ this.auditLog = new AuditLogResource(this._httpClient);
2152
+ this.search = new SearchResource(this._httpClient);
2153
+ this.tags = new TagsResource(this._httpClient);
2154
+ this.activity = new ActivityResource(this._httpClient);
2155
+ this.customerGroups = new CustomerGroupsResource(this._httpClient);
2156
+ this.game = new GameResource(this._httpClient);
2157
+ this.ai = new AiResource(this._httpClient);
2158
+ this.views = new ViewsResource(this._httpClient);
2159
+ this.billing = new BillingResource(this._httpClient);
2160
+ this.realtime = new CStarRealtime(this._httpClient);
2161
+ if (config.offline) {
2162
+ const queueOpts = typeof config.offline === "object" ? config.offline : void 0;
2163
+ this.offlineQueue = new OfflineQueue(this._httpClient, queueOpts);
2164
+ this.offlineQueue.enableAutoFlush();
2165
+ } else {
2166
+ this.offlineQueue = null;
2167
+ }
2168
+ }
2169
+ /** Disconnect the realtime stream and clean up. */
2170
+ destroy() {
2171
+ this.realtime.disconnect();
2172
+ if (this.offlineQueue) {
2173
+ this.offlineQueue.disableAutoFlush();
2174
+ }
1414
2175
  }
1415
2176
  /** Whether this client is using test mode keys (sk_test_* or pk_test_*). */
1416
2177
  get isTestMode() {
1417
- return this.client.environment === "test";
2178
+ return this._httpClient.environment === "test";
1418
2179
  }
1419
2180
  };
1420
2181
  export {
1421
2182
  CStarAuthenticationError,
1422
2183
  CStarClient,
2184
+ CStarConflictError,
1423
2185
  CStarError,
1424
2186
  CStarNotFoundError,
2187
+ CStarPermissionError,
1425
2188
  CStarRateLimitError,
2189
+ CStarRealtime,
2190
+ CStarServerError,
1426
2191
  CStarValidationError,
1427
- ErrorType
2192
+ ErrorType,
2193
+ OfflineQueue,
2194
+ Subscription
1428
2195
  };