@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.cjs CHANGED
@@ -22,21 +22,30 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  CStarAuthenticationError: () => CStarAuthenticationError,
24
24
  CStarClient: () => CStarClient,
25
+ CStarConflictError: () => CStarConflictError,
25
26
  CStarError: () => CStarError,
26
27
  CStarNotFoundError: () => CStarNotFoundError,
28
+ CStarPermissionError: () => CStarPermissionError,
27
29
  CStarRateLimitError: () => CStarRateLimitError,
30
+ CStarRealtime: () => CStarRealtime,
31
+ CStarServerError: () => CStarServerError,
28
32
  CStarValidationError: () => CStarValidationError,
29
- ErrorType: () => ErrorType
33
+ ErrorType: () => ErrorType,
34
+ OfflineQueue: () => OfflineQueue,
35
+ Subscription: () => Subscription
30
36
  });
31
37
  module.exports = __toCommonJS(src_exports);
32
38
 
33
39
  // src/errors.ts
34
40
  var ErrorType = {
35
41
  AUTHENTICATION_ERROR: "authentication_error",
42
+ PERMISSION_ERROR: "permission_error",
36
43
  INVALID_REQUEST_ERROR: "invalid_request_error",
37
44
  NOT_FOUND_ERROR: "not_found_error",
45
+ CONFLICT_ERROR: "conflict_error",
38
46
  RATE_LIMIT_ERROR: "rate_limit_error",
39
- API_ERROR: "api_error"
47
+ API_ERROR: "api_error",
48
+ SERVER_ERROR: "server_error"
40
49
  };
41
50
  var CStarError = class extends Error {
42
51
  constructor(statusCode, body) {
@@ -75,6 +84,24 @@ var CStarValidationError = class extends CStarError {
75
84
  this.name = "CStarValidationError";
76
85
  }
77
86
  };
87
+ var CStarPermissionError = class extends CStarError {
88
+ constructor(statusCode, body) {
89
+ super(statusCode, body);
90
+ this.name = "CStarPermissionError";
91
+ }
92
+ };
93
+ var CStarConflictError = class extends CStarError {
94
+ constructor(statusCode, body) {
95
+ super(statusCode, body);
96
+ this.name = "CStarConflictError";
97
+ }
98
+ };
99
+ var CStarServerError = class extends CStarError {
100
+ constructor(statusCode, body) {
101
+ super(statusCode, body);
102
+ this.name = "CStarServerError";
103
+ }
104
+ };
78
105
  function parseError(statusCode, body, retryAfter) {
79
106
  const errorBody = body?.error ?? {
80
107
  type: ErrorType.API_ERROR,
@@ -85,19 +112,28 @@ function parseError(statusCode, body, retryAfter) {
85
112
  switch (errorBody.type) {
86
113
  case ErrorType.AUTHENTICATION_ERROR:
87
114
  return new CStarAuthenticationError(statusCode, errorBody);
115
+ case ErrorType.PERMISSION_ERROR:
116
+ return new CStarPermissionError(statusCode, errorBody);
88
117
  case ErrorType.RATE_LIMIT_ERROR:
89
118
  return new CStarRateLimitError(statusCode, errorBody, retryAfter ?? 60);
90
119
  case ErrorType.NOT_FOUND_ERROR:
91
120
  return new CStarNotFoundError(statusCode, errorBody);
92
121
  case ErrorType.INVALID_REQUEST_ERROR:
93
122
  return new CStarValidationError(statusCode, errorBody);
123
+ case ErrorType.CONFLICT_ERROR:
124
+ return new CStarConflictError(statusCode, errorBody);
125
+ case ErrorType.SERVER_ERROR:
126
+ return new CStarServerError(statusCode, errorBody);
94
127
  default:
128
+ if (statusCode === 403) return new CStarPermissionError(statusCode, errorBody);
129
+ if (statusCode === 409) return new CStarConflictError(statusCode, errorBody);
130
+ if (statusCode >= 500) return new CStarServerError(statusCode, errorBody);
95
131
  return new CStarError(statusCode, errorBody);
96
132
  }
97
133
  }
98
134
 
99
135
  // src/client.ts
100
- var SDK_VERSION = "0.5.0";
136
+ var SDK_VERSION = "0.5.2";
101
137
  var DEFAULT_BASE_URL = "https://www.cstar.help";
102
138
  var DEFAULT_API_VERSION = "2026-03-01";
103
139
  var DEFAULT_MAX_RETRIES = 3;
@@ -154,10 +190,21 @@ var HttpClient = class {
154
190
  }
155
191
  if (response.ok) {
156
192
  if (response.status === 204) return void 0;
157
- return await response.json();
193
+ const json = await response.json();
194
+ const requestId2 = response.headers.get("x-request-id") ?? response.headers.get("x-cstar-request-id");
195
+ if (requestId2 && json && typeof json === "object" && "meta" in json) {
196
+ const existing = json.meta;
197
+ const metaBase = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
198
+ json.meta = { ...metaBase, requestId: requestId2 };
199
+ }
200
+ return json;
158
201
  }
159
202
  const errorBody = await response.json().catch(() => null);
160
203
  const retryAfter = parseInt(response.headers.get("Retry-After") ?? "", 10) || void 0;
204
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("x-cstar-request-id");
205
+ if (requestId && errorBody?.error) {
206
+ errorBody.error.request_id = requestId;
207
+ }
161
208
  if (response.status < 500 && response.status !== 429) {
162
209
  throw parseError(response.status, errorBody, retryAfter);
163
210
  }
@@ -201,6 +248,14 @@ var HttpClient = class {
201
248
  }
202
249
  return headers;
203
250
  }
251
+ /** Get the base URL for building SSE or other endpoints. */
252
+ getBaseUrl() {
253
+ return `${this.config.baseUrl}/api/v1/teams/${this.config.teamId}`;
254
+ }
255
+ /** Get authorization headers for non-fetch requests (e.g., SSE). */
256
+ getAuthHeaders() {
257
+ return this.buildHeaders();
258
+ }
204
259
  /** Exponential backoff: 500ms, 1s, 2s, 4s... capped at 8s, plus jitter. */
205
260
  getBackoffDelay(attempt) {
206
261
  const base = Math.min(500 * Math.pow(2, attempt - 1), 8e3);
@@ -212,6 +267,504 @@ var HttpClient = class {
212
267
  }
213
268
  };
214
269
 
270
+ // src/realtime.ts
271
+ var CStarRealtime = class {
272
+ constructor(httpClient, options) {
273
+ this.httpClient = httpClient;
274
+ this.eventSource = null;
275
+ this.handlers = /* @__PURE__ */ new Map();
276
+ this.anyHandlers = /* @__PURE__ */ new Set();
277
+ this.errorHandlers = /* @__PURE__ */ new Set();
278
+ this.connectionHandlers = /* @__PURE__ */ new Set();
279
+ this.lastEventId = null;
280
+ this.retryCount = 0;
281
+ this.retryTimer = null;
282
+ this._connected = false;
283
+ this.destroyed = false;
284
+ this.connecting = false;
285
+ /** Short-lived SSE token (exchanged from secret key). */
286
+ this.sseToken = null;
287
+ /** When the current SSE token expires (epoch ms). */
288
+ this.sseTokenExpiresAt = 0;
289
+ /** Timer that refreshes the SSE token before it expires. */
290
+ this.tokenRefreshTimer = null;
291
+ this.autoReconnect = options?.autoReconnect ?? true;
292
+ this.maxRetries = options?.maxRetries ?? 10;
293
+ this.types = options?.types;
294
+ }
295
+ /** Subscribe to a specific event type. Supports wildcards (e.g., 'ticket.*'). Returns an unsubscribe function. */
296
+ on(eventType, handler) {
297
+ let set = this.handlers.get(eventType);
298
+ if (!set) {
299
+ set = /* @__PURE__ */ new Set();
300
+ this.handlers.set(eventType, set);
301
+ }
302
+ set.add(handler);
303
+ this.ensureConnected();
304
+ return () => {
305
+ set.delete(handler);
306
+ };
307
+ }
308
+ /** Subscribe to all events. Returns an unsubscribe function. */
309
+ onAny(handler) {
310
+ this.anyHandlers.add(handler);
311
+ this.ensureConnected();
312
+ return () => {
313
+ this.anyHandlers.delete(handler);
314
+ };
315
+ }
316
+ /** Handle connection errors. */
317
+ onError(handler) {
318
+ this.errorHandlers.add(handler);
319
+ return this;
320
+ }
321
+ /** Handle connection state changes. */
322
+ onConnectionChange(handler) {
323
+ this.connectionHandlers.add(handler);
324
+ return this;
325
+ }
326
+ /** Remove a connection state handler. */
327
+ offConnectionChange(handler) {
328
+ this.connectionHandlers.delete(handler);
329
+ return this;
330
+ }
331
+ /** Remove a handler for a specific event type. */
332
+ off(eventType, handler) {
333
+ const set = this.handlers.get(eventType);
334
+ if (set) {
335
+ set.delete(handler);
336
+ if (set.size === 0) this.handlers.delete(eventType);
337
+ }
338
+ return this;
339
+ }
340
+ /** Remove a handler from the "any" subscription. */
341
+ offAny(handler) {
342
+ this.anyHandlers.delete(handler);
343
+ return this;
344
+ }
345
+ /** Start the SSE connection. Called automatically on first handler registration. */
346
+ connect() {
347
+ if (this.eventSource || this.destroyed || this.connecting) return;
348
+ this.connecting = true;
349
+ const headers = this.httpClient.getAuthHeaders();
350
+ const rawKey = headers.Authorization?.replace("Bearer ", "") ?? "";
351
+ if (rawKey.startsWith("sk_") && !this.hasFreshSseToken()) {
352
+ this.exchangeToken().then(() => {
353
+ this.connecting = false;
354
+ this.connect();
355
+ }).catch(() => {
356
+ this.connecting = false;
357
+ this.connectWithToken(rawKey);
358
+ });
359
+ return;
360
+ }
361
+ const token = this.sseToken ?? rawKey;
362
+ this.connectWithToken(token);
363
+ }
364
+ /**
365
+ * Open the EventSource with the given auth token.
366
+ * Extracted so both the happy path and the fallback path share logic.
367
+ */
368
+ connectWithToken(authToken) {
369
+ const baseUrl = this.httpClient.getBaseUrl();
370
+ const params = new URLSearchParams();
371
+ if (this.types && this.types.length > 0) {
372
+ params.set("types", this.types.join(","));
373
+ }
374
+ if (this.lastEventId) {
375
+ params.set("since", this.lastEventId);
376
+ }
377
+ const qs = params.toString();
378
+ const url = `${baseUrl}/events/stream${qs ? `?${qs}` : ""}`;
379
+ if (authToken.startsWith("sk_") && typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined") {
380
+ console.warn(
381
+ "[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."
382
+ );
383
+ }
384
+ const separator = qs ? "&" : "?";
385
+ const authenticatedUrl = authToken ? `${url}${separator}token=${authToken}` : url;
386
+ this.eventSource = new EventSource(authenticatedUrl);
387
+ this.eventSource.onopen = () => {
388
+ this.connecting = false;
389
+ this.retryCount = 0;
390
+ this.setConnected(true);
391
+ };
392
+ this.eventSource.onerror = () => {
393
+ this.connecting = false;
394
+ this.setConnected(false);
395
+ if (this.eventSource) {
396
+ this.eventSource.close();
397
+ this.eventSource = null;
398
+ }
399
+ if (this.autoReconnect && !this.destroyed && this.retryCount < this.maxRetries) {
400
+ this.scheduleReconnect();
401
+ } else if (this.retryCount >= this.maxRetries) {
402
+ this.emitError(new Error(`Realtime connection failed after ${this.maxRetries} retries`));
403
+ }
404
+ };
405
+ this.eventSource.onmessage = (event) => {
406
+ this.handleMessage(event);
407
+ };
408
+ const knownTypes = [
409
+ "ticket.created",
410
+ "ticket.updated",
411
+ "ticket.closed",
412
+ "ticket.assigned",
413
+ "message.created",
414
+ "message.updated",
415
+ "notification.created",
416
+ "mention.created",
417
+ // Gamification events (future)
418
+ "boss.spawned",
419
+ "boss.damaged",
420
+ "boss.defeated",
421
+ "achievement.unlocked",
422
+ "quest.completed",
423
+ "level.up"
424
+ ];
425
+ for (const type of knownTypes) {
426
+ this.eventSource.addEventListener(type, (event) => {
427
+ const msgEvent = event;
428
+ if (msgEvent.lastEventId) {
429
+ this.lastEventId = msgEvent.lastEventId;
430
+ }
431
+ try {
432
+ const parsed = JSON.parse(msgEvent.data);
433
+ const realtimeEvent = {
434
+ type: parsed.type || type,
435
+ id: msgEvent.lastEventId || "",
436
+ data: parsed.data || parsed,
437
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
438
+ teamId: parsed.teamId || ""
439
+ };
440
+ this.dispatch(realtimeEvent);
441
+ } catch {
442
+ }
443
+ });
444
+ }
445
+ }
446
+ /** Stop listening and clean up all resources. */
447
+ disconnect() {
448
+ this.destroyed = true;
449
+ if (this.retryTimer) {
450
+ clearTimeout(this.retryTimer);
451
+ this.retryTimer = null;
452
+ }
453
+ if (this.tokenRefreshTimer) {
454
+ clearTimeout(this.tokenRefreshTimer);
455
+ this.tokenRefreshTimer = null;
456
+ }
457
+ if (this.eventSource) {
458
+ this.eventSource.close();
459
+ this.eventSource = null;
460
+ }
461
+ this.sseToken = null;
462
+ this.sseTokenExpiresAt = 0;
463
+ this.setConnected(false);
464
+ this.handlers.clear();
465
+ this.anyHandlers.clear();
466
+ this.errorHandlers.clear();
467
+ this.connectionHandlers.clear();
468
+ }
469
+ /** Whether the SSE connection is currently open. */
470
+ get connected() {
471
+ return this._connected;
472
+ }
473
+ // ── Internal ──
474
+ ensureConnected() {
475
+ if (!this.eventSource && !this.destroyed) {
476
+ this.connect();
477
+ }
478
+ }
479
+ handleMessage(event) {
480
+ if (event.lastEventId) {
481
+ this.lastEventId = event.lastEventId;
482
+ }
483
+ try {
484
+ const parsed = JSON.parse(event.data);
485
+ if (parsed.type === "connected") return;
486
+ const realtimeEvent = {
487
+ type: parsed.type,
488
+ id: event.lastEventId || "",
489
+ data: parsed.data || parsed,
490
+ timestamp: parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
491
+ teamId: parsed.teamId || ""
492
+ };
493
+ this.dispatch(realtimeEvent);
494
+ } catch {
495
+ }
496
+ }
497
+ dispatch(event) {
498
+ const exactSet = this.handlers.get(event.type);
499
+ if (exactSet) {
500
+ for (const handler of exactSet) handler(event);
501
+ }
502
+ for (const [pattern, handlerSet] of this.handlers) {
503
+ if (pattern.endsWith(".*")) {
504
+ const prefix = pattern.slice(0, -2);
505
+ if (event.type.startsWith(prefix + ".") && pattern !== event.type) {
506
+ for (const handler of handlerSet) handler(event);
507
+ }
508
+ }
509
+ }
510
+ for (const handler of this.anyHandlers) handler(event);
511
+ }
512
+ scheduleReconnect() {
513
+ this.retryCount++;
514
+ const delay = Math.min(500 * Math.pow(2, this.retryCount - 1), 8e3) + Math.random() * 200;
515
+ this.retryTimer = setTimeout(() => {
516
+ this.retryTimer = null;
517
+ if (!this.destroyed) {
518
+ this.connect();
519
+ }
520
+ }, delay);
521
+ }
522
+ setConnected(value) {
523
+ if (this._connected !== value) {
524
+ this._connected = value;
525
+ for (const handler of this.connectionHandlers) handler(value);
526
+ }
527
+ }
528
+ emitError(error) {
529
+ for (const handler of this.errorHandlers) handler(error);
530
+ }
531
+ // ── SSE Token Exchange ──
532
+ /** Whether we hold a token that won't expire for at least 30 seconds. */
533
+ hasFreshSseToken() {
534
+ return !!this.sseToken && this.sseTokenExpiresAt - Date.now() > 3e4;
535
+ }
536
+ /**
537
+ * Exchange the secret API key for a short-lived SSE token via
538
+ * POST /events/token. Schedules automatic refresh before expiry.
539
+ */
540
+ async exchangeToken() {
541
+ const response = await this.httpClient.request("POST", "/events/token");
542
+ const { token, expiresAt, expiresIn } = response.data;
543
+ this.sseToken = token;
544
+ this.sseTokenExpiresAt = new Date(expiresAt).getTime();
545
+ const refreshIn = Math.max((expiresIn - 30) * 1e3, expiresIn / 2 * 1e3);
546
+ this.scheduleTokenRefresh(refreshIn);
547
+ }
548
+ /**
549
+ * Schedule a background token refresh. On success the existing connection
550
+ * keeps working (tokens are validated at connect time). On the next
551
+ * reconnect the fresh token will be used automatically.
552
+ */
553
+ scheduleTokenRefresh(delayMs) {
554
+ if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
555
+ this.tokenRefreshTimer = setTimeout(() => {
556
+ this.tokenRefreshTimer = null;
557
+ if (this.destroyed) return;
558
+ this.exchangeToken().catch(() => {
559
+ });
560
+ }, delayMs);
561
+ }
562
+ };
563
+
564
+ // src/subscription.ts
565
+ var RESOURCE_EVENT_MAP = {
566
+ "/tickets": ["ticket.created", "ticket.updated", "ticket.closed", "ticket.assigned"],
567
+ "/customers": ["customer.created", "customer.updated"],
568
+ "/articles": ["article.created", "article.updated", "article.published"],
569
+ "/messages": ["message.created", "message.updated"],
570
+ "/notifications": ["notification.created"],
571
+ "/mentions": ["mention.created"],
572
+ "/game/boss": ["boss.spawned", "boss.damaged", "boss.defeated"],
573
+ "/game/player": ["achievement.unlocked", "quest.completed", "level.up", "player.updated"],
574
+ "/game/leaderboard": [
575
+ "leaderboard.updated",
576
+ "achievement.unlocked",
577
+ "quest.completed",
578
+ "level.up"
579
+ ],
580
+ "/game/activity": [
581
+ "boss.spawned",
582
+ "boss.defeated",
583
+ "achievement.unlocked",
584
+ "level.up",
585
+ "quest.completed"
586
+ ]
587
+ };
588
+ function getEntityId(item) {
589
+ if (!item || typeof item !== "object") return void 0;
590
+ const obj = item;
591
+ return obj.id ?? obj.userId ?? void 0;
592
+ }
593
+ var Subscription = class {
594
+ constructor(realtime, fetchFn, resourcePath, filterFn) {
595
+ this.realtime = realtime;
596
+ this.fetchFn = fetchFn;
597
+ this.resourcePath = resourcePath;
598
+ this.filterFn = filterFn;
599
+ this._data = [];
600
+ this._loading = true;
601
+ this._error = null;
602
+ this.changeHandlers = /* @__PURE__ */ new Set();
603
+ this.errorEventHandlers = /* @__PURE__ */ new Set();
604
+ this.boundEventHandler = null;
605
+ this.destroyed = false;
606
+ /** Generation counter to prevent stale refresh() from overwriting SSE updates. */
607
+ this.generation = 0;
608
+ this.init();
609
+ }
610
+ /** Current data array (updated in real-time). */
611
+ get data() {
612
+ return this._data;
613
+ }
614
+ /** True during initial data fetch. */
615
+ get loading() {
616
+ return this._loading;
617
+ }
618
+ /** Last error, if any. */
619
+ get error() {
620
+ return this._error;
621
+ }
622
+ on(event, handler) {
623
+ if (event === "change") {
624
+ this.changeHandlers.add(handler);
625
+ } else if (event === "error") {
626
+ this.errorEventHandlers.add(handler);
627
+ }
628
+ return this;
629
+ }
630
+ /** Manually refresh data from the server. */
631
+ async refresh() {
632
+ const gen = ++this.generation;
633
+ try {
634
+ const response = await this.fetchFn();
635
+ if (gen !== this.generation) return;
636
+ this._data = response.data;
637
+ this._error = null;
638
+ this.emitChange();
639
+ } catch (err) {
640
+ if (gen !== this.generation) return;
641
+ this._error = err instanceof Error ? err : new Error(String(err));
642
+ this.emitError(this._error);
643
+ }
644
+ }
645
+ /**
646
+ * Apply an optimistic update to the local data.
647
+ * Returns a rollback function that reverts the change.
648
+ */
649
+ optimisticUpdate(id, changes) {
650
+ const snapshot = [...this._data];
651
+ const index = this._data.findIndex((item) => getEntityId(item) === id);
652
+ if (index !== -1) {
653
+ this._data[index] = { ...this._data[index], ...changes };
654
+ if (this.filterFn && !this.filterFn(this._data[index])) {
655
+ this._data.splice(index, 1);
656
+ }
657
+ this.emitChange();
658
+ }
659
+ return () => {
660
+ this._data = snapshot;
661
+ this.emitChange();
662
+ };
663
+ }
664
+ /** Stop the subscription and clean up all listeners. */
665
+ unsubscribe() {
666
+ this.destroyed = true;
667
+ if (this.boundEventHandler) {
668
+ this.realtime.offAny(this.boundEventHandler);
669
+ this.boundEventHandler = null;
670
+ }
671
+ this.changeHandlers.clear();
672
+ this.errorEventHandlers.clear();
673
+ }
674
+ // ── Internal ──
675
+ async init() {
676
+ this.boundEventHandler = (event) => this.handleEvent(event);
677
+ this.realtime.onAny(this.boundEventHandler);
678
+ const gen = ++this.generation;
679
+ try {
680
+ const response = await this.fetchFn();
681
+ if (gen !== this.generation) return;
682
+ this._data = response.data;
683
+ this._loading = false;
684
+ this._error = null;
685
+ this.emitChange();
686
+ } catch (err) {
687
+ if (gen !== this.generation) return;
688
+ this._loading = false;
689
+ this._error = err instanceof Error ? err : new Error(String(err));
690
+ this.emitError(this._error);
691
+ }
692
+ }
693
+ handleEvent(event) {
694
+ if (this.destroyed) return;
695
+ const eventTypes = RESOURCE_EVENT_MAP[this.resourcePath];
696
+ if (eventTypes) {
697
+ const isRelevant = eventTypes.some((t) => {
698
+ const prefix = t.split(".")[0];
699
+ return event.type.startsWith(prefix + ".");
700
+ });
701
+ if (!isRelevant) return;
702
+ } else {
703
+ const eventPrefix = event.type.split(".")[0];
704
+ const segments = this.resourcePath.replace(/^\//, "").split("/");
705
+ const resourceName = segments[segments.length - 1];
706
+ if (resourceName !== eventPrefix && resourceName !== eventPrefix + "s") return;
707
+ }
708
+ const entityData = this.extractEntity(event);
709
+ if (!entityData || !getEntityId(entityData)) return;
710
+ this.generation++;
711
+ if (event.type.endsWith(".created")) {
712
+ this.handleCreated(entityData);
713
+ } else if (event.type.endsWith(".deleted")) {
714
+ this.handleDeleted(entityData);
715
+ } else if (event.type.endsWith(".updated") || event.type.endsWith(".assigned") || event.type.endsWith(".closed")) {
716
+ this.handleUpdated(entityData);
717
+ }
718
+ }
719
+ handleCreated(entity) {
720
+ if (this.filterFn && !this.filterFn(entity)) return;
721
+ const entityId = getEntityId(entity);
722
+ if (entityId && this._data.some((item) => getEntityId(item) === entityId)) return;
723
+ this._data = [entity, ...this._data];
724
+ this.emitChange();
725
+ }
726
+ handleDeleted(entity) {
727
+ const entityId = getEntityId(entity);
728
+ if (!entityId) return;
729
+ const index = this._data.findIndex((item) => getEntityId(item) === entityId);
730
+ if (index !== -1) {
731
+ this._data = [...this._data.slice(0, index), ...this._data.slice(index + 1)];
732
+ this.emitChange();
733
+ }
734
+ }
735
+ handleUpdated(entity) {
736
+ const entityId = getEntityId(entity);
737
+ const index = entityId ? this._data.findIndex((item) => getEntityId(item) === entityId) : -1;
738
+ if (index !== -1) {
739
+ const updated = { ...this._data[index], ...entity };
740
+ if (this.filterFn && !this.filterFn(updated)) {
741
+ this._data = [...this._data.slice(0, index), ...this._data.slice(index + 1)];
742
+ } else {
743
+ this._data = [...this._data.slice(0, index), updated, ...this._data.slice(index + 1)];
744
+ }
745
+ this.emitChange();
746
+ } else if (this.filterFn && this.filterFn(entity)) {
747
+ this._data = [entity, ...this._data];
748
+ this.emitChange();
749
+ }
750
+ }
751
+ extractEntity(event) {
752
+ const data = event.data;
753
+ const resourceKey = this.resourcePath.replace(/^\//, "").replace(/s$/, "");
754
+ if (data[resourceKey] && typeof data[resourceKey] === "object") {
755
+ return data[resourceKey];
756
+ }
757
+ if (data.id) return data;
758
+ return null;
759
+ }
760
+ emitChange() {
761
+ for (const handler of this.changeHandlers) handler(this._data);
762
+ }
763
+ emitError(error) {
764
+ for (const handler of this.errorEventHandlers) handler(error);
765
+ }
766
+ };
767
+
215
768
  // src/resources/base.ts
216
769
  var BaseResource = class {
217
770
  constructor(client, basePath) {
@@ -270,6 +823,22 @@ var BaseResource = class {
270
823
  page++;
271
824
  }
272
825
  }
826
+ /**
827
+ * Subscribe to real-time changes for this resource type.
828
+ *
829
+ * Fetches initial data, then auto-updates via SSE events.
830
+ * Requires a `CStarRealtime` instance (available from `CStarClient.realtime`).
831
+ *
832
+ * ```typescript
833
+ * const sub = cstar.tickets.subscribe(cstar.realtime, { status: 'open' });
834
+ * sub.on('change', (tickets) => console.log(tickets));
835
+ * // Later: sub.unsubscribe();
836
+ * ```
837
+ */
838
+ subscribe(realtime, params, filterFn) {
839
+ const fetchFn = () => this.list(params);
840
+ return new Subscription(realtime, fetchFn, this.basePath, filterFn);
841
+ }
273
842
  /** Convert params to string values for query string serialization. */
274
843
  serializeQuery(params) {
275
844
  const result = {};
@@ -354,7 +923,7 @@ var WebhooksResource = class extends BaseResource {
354
923
  );
355
924
  }
356
925
  /** Subscribe to an event (REST Hooks / Zapier protocol). */
357
- async subscribe(params, options) {
926
+ async restSubscribe(params, options) {
358
927
  return this.client.request("POST", "/webhooks/subscribe", {
359
928
  body: params,
360
929
  idempotencyKey: options?.idempotencyKey
@@ -1347,6 +1916,72 @@ var GameResource = class {
1347
1916
  async config() {
1348
1917
  return this.client.request("GET", "/game/config");
1349
1918
  }
1919
+ // ── Real-time Subscriptions ──
1920
+ /**
1921
+ * Subscribe to live boss battle state.
1922
+ * Updates on boss.spawned, boss.damaged, boss.defeated events.
1923
+ */
1924
+ subscribeBoss(realtime) {
1925
+ const fetchFn = async () => {
1926
+ const res = await this.boss.current();
1927
+ return {
1928
+ success: true,
1929
+ data: res.data.active ? [res.data] : [],
1930
+ pagination: { total: res.data.active ? 1 : 0, page: 1, pageSize: 1, hasMore: false },
1931
+ meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1932
+ };
1933
+ };
1934
+ return new Subscription(realtime, fetchFn, "/game/boss");
1935
+ }
1936
+ /**
1937
+ * Subscribe to player stats (XP, gold, level, streaks).
1938
+ * Updates on achievement.unlocked, quest.completed, level.up events.
1939
+ */
1940
+ subscribePlayer(realtime, userId) {
1941
+ const fetchFn = async () => {
1942
+ const res = await this.player({ userId });
1943
+ return {
1944
+ success: true,
1945
+ data: [res.data],
1946
+ pagination: { total: 1, page: 1, pageSize: 1, hasMore: false },
1947
+ meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1948
+ };
1949
+ };
1950
+ return new Subscription(realtime, fetchFn, "/game/player");
1951
+ }
1952
+ /**
1953
+ * Subscribe to the team leaderboard.
1954
+ * Updates when scores change.
1955
+ */
1956
+ subscribeLeaderboard(realtime, params) {
1957
+ const fetchFn = async () => {
1958
+ const res = await this.leaderboard(params);
1959
+ return {
1960
+ success: true,
1961
+ data: res.data,
1962
+ pagination: { total: res.data.length, page: 1, pageSize: 100, hasMore: false },
1963
+ meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1964
+ };
1965
+ };
1966
+ return new Subscription(realtime, fetchFn, "/game/leaderboard");
1967
+ }
1968
+ /**
1969
+ * Subscribe to team activity feed (achievements, boss defeats, level ups).
1970
+ * Rolling feed of recent game events.
1971
+ */
1972
+ subscribeActivity(realtime) {
1973
+ const fetchFn = async () => {
1974
+ const res = await this.activity.list({ limit: 50 });
1975
+ const data = res.data.data || [];
1976
+ return {
1977
+ success: true,
1978
+ data,
1979
+ pagination: { total: data.length, page: 1, pageSize: 50, hasMore: false },
1980
+ meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1981
+ };
1982
+ };
1983
+ return new Subscription(realtime, fetchFn, "/game/activity");
1984
+ }
1350
1985
  };
1351
1986
 
1352
1987
  // src/resources/ai.ts
@@ -1409,53 +2044,191 @@ var BillingResource = class {
1409
2044
  }
1410
2045
  };
1411
2046
 
2047
+ // src/offline.ts
2048
+ var STORAGE_KEY = "cstar_offline_queue";
2049
+ var OfflineQueue = class {
2050
+ constructor(httpClient, options) {
2051
+ this.httpClient = httpClient;
2052
+ this.items = [];
2053
+ this.autoFlushEnabled = false;
2054
+ this.onlineHandler = null;
2055
+ this.useLocalStorage = options?.storage === "localStorage" && typeof globalThis !== "undefined" && typeof globalThis.localStorage !== "undefined";
2056
+ if (this.useLocalStorage) {
2057
+ this.loadFromStorage();
2058
+ }
2059
+ }
2060
+ /** Queue a mutation for later execution. Returns a queue item ID. */
2061
+ enqueue(method, path, body) {
2062
+ const id = `q_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2063
+ const item = {
2064
+ id,
2065
+ method,
2066
+ path,
2067
+ body,
2068
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString()
2069
+ };
2070
+ this.items.push(item);
2071
+ this.saveToStorage();
2072
+ return id;
2073
+ }
2074
+ /**
2075
+ * Process all queued mutations in order. Returns results for each item.
2076
+ * Failed items are re-queued at the front to preserve ordering.
2077
+ * Items enqueued during flush are appended after re-queued failures.
2078
+ */
2079
+ async flush() {
2080
+ const results = [];
2081
+ const itemsToProcess = [...this.items];
2082
+ this.items = [];
2083
+ this.saveToStorage();
2084
+ const failedItems = [];
2085
+ for (const item of itemsToProcess) {
2086
+ try {
2087
+ const data = await this.httpClient.request(item.method, item.path, {
2088
+ body: item.body
2089
+ });
2090
+ results.push({ id: item.id, success: true, data });
2091
+ } catch (err) {
2092
+ const message = err instanceof Error ? err.message : String(err);
2093
+ results.push({ id: item.id, success: false, error: message });
2094
+ const statusCode = err.statusCode;
2095
+ const isRetryable = !statusCode || statusCode >= 500;
2096
+ if (isRetryable) {
2097
+ failedItems.push(item);
2098
+ }
2099
+ }
2100
+ }
2101
+ this.items = [...failedItems, ...this.items];
2102
+ this.saveToStorage();
2103
+ return results;
2104
+ }
2105
+ /** Get pending queue items. */
2106
+ get pending() {
2107
+ return [...this.items];
2108
+ }
2109
+ /** Number of items in the queue. */
2110
+ get size() {
2111
+ return this.items.length;
2112
+ }
2113
+ /** Clear all pending items. */
2114
+ clear() {
2115
+ this.items = [];
2116
+ this.saveToStorage();
2117
+ }
2118
+ /**
2119
+ * Enable auto-flush when the browser goes online.
2120
+ * Only works in browser environments with the `online` event.
2121
+ */
2122
+ enableAutoFlush() {
2123
+ if (this.autoFlushEnabled) return;
2124
+ if (typeof globalThis === "undefined" || typeof globalThis.addEventListener !== "function") {
2125
+ return;
2126
+ }
2127
+ this.autoFlushEnabled = true;
2128
+ this.onlineHandler = () => {
2129
+ if (this.items.length > 0) {
2130
+ this.flush().catch(() => {
2131
+ });
2132
+ }
2133
+ };
2134
+ globalThis.addEventListener("online", this.onlineHandler);
2135
+ }
2136
+ /** Disable auto-flush and clean up event listeners. */
2137
+ disableAutoFlush() {
2138
+ if (!this.autoFlushEnabled || !this.onlineHandler) return;
2139
+ if (typeof globalThis !== "undefined" && typeof globalThis.removeEventListener === "function") {
2140
+ globalThis.removeEventListener("online", this.onlineHandler);
2141
+ }
2142
+ this.onlineHandler = null;
2143
+ this.autoFlushEnabled = false;
2144
+ }
2145
+ saveToStorage() {
2146
+ if (!this.useLocalStorage) return;
2147
+ try {
2148
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.items));
2149
+ } catch {
2150
+ }
2151
+ }
2152
+ loadFromStorage() {
2153
+ try {
2154
+ const raw = localStorage.getItem(STORAGE_KEY);
2155
+ if (raw) {
2156
+ this.items = JSON.parse(raw);
2157
+ }
2158
+ } catch {
2159
+ this.items = [];
2160
+ }
2161
+ }
2162
+ };
2163
+
1412
2164
  // src/index.ts
1413
2165
  var CStarClient = class {
1414
2166
  constructor(config) {
1415
- this.client = new HttpClient(config);
1416
- this.tickets = new TicketsResource(this.client);
1417
- this.customers = new CustomersResource(this.client);
1418
- this.articles = new ArticlesResource(this.client);
1419
- this.webhooks = new WebhooksResource(this.client);
1420
- this.automations = new AutomationsResource(this.client);
1421
- this.categories = new CategoriesResource(this.client);
2167
+ this._httpClient = new HttpClient(config);
2168
+ this.tickets = new TicketsResource(this._httpClient);
2169
+ this.customers = new CustomersResource(this._httpClient);
2170
+ this.articles = new ArticlesResource(this._httpClient);
2171
+ this.webhooks = new WebhooksResource(this._httpClient);
2172
+ this.automations = new AutomationsResource(this._httpClient);
2173
+ this.categories = new CategoriesResource(this._httpClient);
1422
2174
  this.community = {
1423
- posts: new CommunityPostsResource(this.client),
1424
- topics: new CommunityTopicsResource(this.client)
2175
+ posts: new CommunityPostsResource(this._httpClient),
2176
+ topics: new CommunityTopicsResource(this._httpClient)
1425
2177
  };
1426
- this.members = new MembersResource(this.client);
1427
- this.settings = new SettingsResource(this.client);
1428
- this.notifications = new NotificationsResource(this.client);
1429
- this.mentions = new MentionsResource(this.client);
1430
- this.attachments = new AttachmentsResource(this.client);
1431
- this.customFields = new CustomFieldsResource(this.client);
1432
- this.sla = new SlaResource(this.client);
1433
- this.analytics = new AnalyticsResource(this.client);
1434
- this.bulk = new BulkResource(this.client);
1435
- this.export = new ExportResource(this.client);
1436
- this.import = new ImportResource(this.client);
1437
- this.auditLog = new AuditLogResource(this.client);
1438
- this.search = new SearchResource(this.client);
1439
- this.tags = new TagsResource(this.client);
1440
- this.activity = new ActivityResource(this.client);
1441
- this.customerGroups = new CustomerGroupsResource(this.client);
1442
- this.game = new GameResource(this.client);
1443
- this.ai = new AiResource(this.client);
1444
- this.views = new ViewsResource(this.client);
1445
- this.billing = new BillingResource(this.client);
2178
+ this.members = new MembersResource(this._httpClient);
2179
+ this.settings = new SettingsResource(this._httpClient);
2180
+ this.notifications = new NotificationsResource(this._httpClient);
2181
+ this.mentions = new MentionsResource(this._httpClient);
2182
+ this.attachments = new AttachmentsResource(this._httpClient);
2183
+ this.customFields = new CustomFieldsResource(this._httpClient);
2184
+ this.sla = new SlaResource(this._httpClient);
2185
+ this.analytics = new AnalyticsResource(this._httpClient);
2186
+ this.bulk = new BulkResource(this._httpClient);
2187
+ this.export = new ExportResource(this._httpClient);
2188
+ this.import = new ImportResource(this._httpClient);
2189
+ this.auditLog = new AuditLogResource(this._httpClient);
2190
+ this.search = new SearchResource(this._httpClient);
2191
+ this.tags = new TagsResource(this._httpClient);
2192
+ this.activity = new ActivityResource(this._httpClient);
2193
+ this.customerGroups = new CustomerGroupsResource(this._httpClient);
2194
+ this.game = new GameResource(this._httpClient);
2195
+ this.ai = new AiResource(this._httpClient);
2196
+ this.views = new ViewsResource(this._httpClient);
2197
+ this.billing = new BillingResource(this._httpClient);
2198
+ this.realtime = new CStarRealtime(this._httpClient);
2199
+ if (config.offline) {
2200
+ const queueOpts = typeof config.offline === "object" ? config.offline : void 0;
2201
+ this.offlineQueue = new OfflineQueue(this._httpClient, queueOpts);
2202
+ this.offlineQueue.enableAutoFlush();
2203
+ } else {
2204
+ this.offlineQueue = null;
2205
+ }
2206
+ }
2207
+ /** Disconnect the realtime stream and clean up. */
2208
+ destroy() {
2209
+ this.realtime.disconnect();
2210
+ if (this.offlineQueue) {
2211
+ this.offlineQueue.disableAutoFlush();
2212
+ }
1446
2213
  }
1447
2214
  /** Whether this client is using test mode keys (sk_test_* or pk_test_*). */
1448
2215
  get isTestMode() {
1449
- return this.client.environment === "test";
2216
+ return this._httpClient.environment === "test";
1450
2217
  }
1451
2218
  };
1452
2219
  // Annotate the CommonJS export names for ESM import in node:
1453
2220
  0 && (module.exports = {
1454
2221
  CStarAuthenticationError,
1455
2222
  CStarClient,
2223
+ CStarConflictError,
1456
2224
  CStarError,
1457
2225
  CStarNotFoundError,
2226
+ CStarPermissionError,
1458
2227
  CStarRateLimitError,
2228
+ CStarRealtime,
2229
+ CStarServerError,
1459
2230
  CStarValidationError,
1460
- ErrorType
2231
+ ErrorType,
2232
+ OfflineQueue,
2233
+ Subscription
1461
2234
  });