@cstar.help/js 0.10.3 → 0.12.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.
@@ -9,8 +9,8 @@ var RealtimeManager = class {
9
9
  this.options = options;
10
10
  }
11
11
  /**
12
- * Initialize the Supabase client and authenticate with a realtime token.
13
- * Returns true if successful, false if @supabase/supabase-js is unavailable.
12
+ * Initialize the realtime client and authenticate with a token.
13
+ * Returns true if successful, false if the realtime runtime is unavailable.
14
14
  */
15
15
  init() {
16
16
  if (this.initPromise) return this.initPromise;
@@ -19,15 +19,15 @@ var RealtimeManager = class {
19
19
  }
20
20
  async doInit() {
21
21
  try {
22
+ const tokenData = await this.options.fetchToken();
22
23
  const { createClient } = await import(
23
24
  /* webpackIgnore: true */
24
25
  "@supabase/supabase-js"
25
26
  );
26
- this.client = createClient(this.options.supabaseUrl, this.options.supabaseAnonKey, {
27
+ this.client = createClient(tokenData.realtimeUrl, tokenData.realtimeKey, {
27
28
  auth: { persistSession: false, autoRefreshToken: false },
28
29
  realtime: { params: { eventsPerSecond: 10 } }
29
30
  });
30
- const tokenData = await this.options.fetchToken();
31
31
  this.setToken(tokenData);
32
32
  return true;
33
33
  } catch {
@@ -85,7 +85,8 @@ var RealtimeManager = class {
85
85
  sender_name: row.sender_name ?? "",
86
86
  created_at: row.created_at,
87
87
  read_at: row.read_at ?? null,
88
- attachments: row.attachments ?? []
88
+ attachments: row.attachments ?? [],
89
+ payload: row.payload ?? null
89
90
  };
90
91
  callback(message);
91
92
  }
@@ -93,10 +94,18 @@ var RealtimeManager = class {
93
94
  this.channels.set(channelName, channel);
94
95
  return () => this.removeChannel(channelName);
95
96
  }
96
- /** Get the underlying Supabase client (for sharing with PresenceManager). */
97
+ /** Get the underlying realtime client (for sharing with PresenceManager). */
97
98
  getClient() {
98
99
  return this.client;
99
100
  }
101
+ /**
102
+ * The current Supabase JWT (set by init/refresh). Returned so callers can
103
+ * decode the `sub` claim to learn the customerId — required after a
104
+ * session-mode login where the SDK doesn't have customerId at hand.
105
+ */
106
+ getCurrentToken() {
107
+ return this.currentToken;
108
+ }
100
109
  /** Whether the realtime manager is initialized and has a token. */
101
110
  get isReady() {
102
111
  return this.client !== null && this.currentToken !== null;
@@ -135,7 +144,7 @@ var PresenceManager = class {
135
144
  this.typingClearTimers = /* @__PURE__ */ new Map();
136
145
  this.options = options;
137
146
  }
138
- /** Set the Supabase client (shared from RealtimeManager). */
147
+ /** Set the realtime client (shared from RealtimeManager). */
139
148
  setClient(client) {
140
149
  this.client = client;
141
150
  }
@@ -238,6 +247,43 @@ var PresenceManager = class {
238
247
  }
239
248
  };
240
249
 
250
+ // src/chat/errors.ts
251
+ var ChatClientError = class extends Error {
252
+ constructor(message, opts) {
253
+ super(message);
254
+ this.name = "ChatClientError";
255
+ this.code = opts.code ?? "UNKNOWN";
256
+ this.statusCode = opts.statusCode;
257
+ this.requestId = opts.requestId ?? null;
258
+ }
259
+ };
260
+ var ChatClientAuthError = class extends ChatClientError {
261
+ constructor(opts) {
262
+ super(opts.message, opts);
263
+ this.name = "ChatClientAuthError";
264
+ }
265
+ };
266
+ var ChatClientRateLimitError = class extends ChatClientError {
267
+ constructor(opts) {
268
+ super(opts.message, opts);
269
+ this.name = "ChatClientRateLimitError";
270
+ this.retryAfter = opts.retryAfter ?? null;
271
+ }
272
+ };
273
+ var ChatClientServerError = class extends ChatClientError {
274
+ constructor(opts) {
275
+ super(opts.message, opts);
276
+ this.name = "ChatClientServerError";
277
+ }
278
+ };
279
+ var ChatClientNetworkError = class extends ChatClientError {
280
+ constructor(opts) {
281
+ super(opts.message, { code: "NETWORK_ERROR", statusCode: 0, requestId: null });
282
+ this.name = "ChatClientNetworkError";
283
+ this.originalError = opts.originalError;
284
+ }
285
+ };
286
+
241
287
  // src/chat/index.ts
242
288
  var DEFAULT_BASE_URL = "https://www.cstar.help";
243
289
  var DEFAULT_POLLING_INTERVAL = 3e3;
@@ -246,6 +292,7 @@ var ChatClient = class {
246
292
  this.sessionToken = null;
247
293
  this.teamId = null;
248
294
  this.customerId = null;
295
+ this.authMode = null;
249
296
  this.pollingTimers = /* @__PURE__ */ new Map();
250
297
  this.realtimeManager = null;
251
298
  this.presenceManager = null;
@@ -254,18 +301,17 @@ var ChatClient = class {
254
301
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
255
302
  this.teamSlug = config.teamSlug;
256
303
  this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
257
- this.supabaseUrl = config.supabaseUrl;
258
- this.supabaseAnonKey = config.supabaseAnonKey;
259
- this.conversations = new ConversationsApi(this);
304
+ this.realtimeEnabled = config.realtime !== false;
305
+ this.tickets = new TicketsApi(this);
260
306
  this.messages = new MessagesApi(this);
261
307
  this.community = new ChatCommunityApi(this);
262
308
  }
263
309
  /**
264
310
  * Identify a customer using HMAC signature verification.
265
- * Must be called before accessing conversations or messages.
311
+ * Must be called before accessing tickets or messages.
266
312
  *
267
- * If `supabaseUrl` and `supabaseAnonKey` were provided in the config,
268
- * this will also initialize real-time subscriptions (non-blocking).
313
+ * Real-time subscriptions are initialized automatically (non-blocking)
314
+ * unless `realtime: false` was passed in config.
269
315
  *
270
316
  * @param customer - Customer identity fields
271
317
  * @param signature - HMAC-SHA256 hex signature of the signed fields
@@ -287,7 +333,7 @@ var ChatClient = class {
287
333
  this.sessionToken = response.data.sessionToken;
288
334
  this.teamId = response.data.teamId;
289
335
  this.customerId = response.data.customer.id;
290
- if (this.supabaseUrl && this.supabaseAnonKey) {
336
+ if (this.realtimeEnabled) {
291
337
  this.realtimeInitPromise = this.initRealtime(
292
338
  response.data.customer.id,
293
339
  response.data.customer.name || customer.name || customer.email
@@ -296,8 +342,72 @@ var ChatClient = class {
296
342
  return response.data;
297
343
  }
298
344
  /**
299
- * Subscribe to new messages on a conversation.
300
- * Uses Supabase Realtime (Postgres Changes) when available, falls back to polling.
345
+ * Sign in with cStar's hosted password auth. The SDK keeps the resulting
346
+ * Supabase access token and uses it as Bearer for every subsequent call.
347
+ * Real-time bootstraps automatically (same flow as `identify()`).
348
+ *
349
+ * Use this when your customers don't have a session in your app and you
350
+ * want cStar to handle login. For the case where your app already has a
351
+ * Supabase session, use `loginWithSession({ accessToken })` instead.
352
+ */
353
+ async loginWithCredentials(params) {
354
+ const teamId = await this.resolveTeamIdFromSlug();
355
+ const response = await this.fetch("/api/widget/auth/login", {
356
+ method: "POST",
357
+ body: { email: params.email, password: params.password, teamId }
358
+ });
359
+ if (!response.accessToken) {
360
+ throw new ChatClientAuthError({
361
+ message: "Login succeeded but no session was issued",
362
+ code: "NO_SESSION",
363
+ statusCode: 200,
364
+ requestId: null
365
+ });
366
+ }
367
+ await this.loginWithSession({
368
+ accessToken: response.accessToken,
369
+ teamId,
370
+ customerName: response.customer.name ?? response.customer.email
371
+ });
372
+ return {
373
+ customer: {
374
+ id: this.customerId ?? response.customer.id,
375
+ email: response.customer.email,
376
+ name: response.customer.name
377
+ },
378
+ teamId
379
+ };
380
+ }
381
+ /**
382
+ * Use an existing Supabase access token for widget calls. Skip the cStar
383
+ * login flow entirely; the SDK uses the supplied token as Bearer and
384
+ * bootstraps real-time the same way `identify()` does.
385
+ *
386
+ * The widget API verifies the token via the `widget-session` strategy on
387
+ * every request — there is no client-side validation here.
388
+ */
389
+ async loginWithSession(params) {
390
+ this.sessionToken = params.accessToken;
391
+ if (params.teamId) {
392
+ this.teamId = params.teamId;
393
+ }
394
+ this.authMode = "session";
395
+ if (this.realtimeEnabled) {
396
+ const customerName = params.customerName ?? "";
397
+ this.realtimeInitPromise = this.initRealtimeFromSession(customerName);
398
+ }
399
+ }
400
+ /**
401
+ * Returns the auth mode the client is currently using, or `null` before
402
+ * any authenticated call has resolved one. Useful for branching UI
403
+ * (e.g. to show a "Sign out" button only in session mode).
404
+ */
405
+ getAuthMode() {
406
+ return this.authMode;
407
+ }
408
+ /**
409
+ * Subscribe to new messages on a ticket.
410
+ * Uses real-time delivery when available, falls back to polling.
301
411
  *
302
412
  * @returns Unsubscribe function
303
413
  */
@@ -309,7 +419,7 @@ var ChatClient = class {
309
419
  }
310
420
  /**
311
421
  * Subscribe to typing indicator events.
312
- * Requires Supabase Realtime — returns a no-op if unavailable.
422
+ * Requires real-time — returns a no-op when unavailable.
313
423
  *
314
424
  * @returns Unsubscribe function
315
425
  */
@@ -321,8 +431,8 @@ var ChatClient = class {
321
431
  };
322
432
  }
323
433
  /**
324
- * Send a customer typing indicator (requires realtime).
325
- * No-op if realtime is not available.
434
+ * Send a customer typing indicator (requires real-time).
435
+ * No-op when real-time is unavailable.
326
436
  */
327
437
  async sendTyping(ticketId, isTyping) {
328
438
  if (this.presenceManager) {
@@ -348,8 +458,20 @@ var ChatClient = class {
348
458
  this.sessionToken = null;
349
459
  this.teamId = null;
350
460
  this.customerId = null;
461
+ this.authMode = null;
351
462
  }
352
- /** @internal Make an authenticated request to the widget API. */
463
+ /**
464
+ * @internal Make an authenticated request to the widget API.
465
+ *
466
+ * On any non-2xx response, throws a typed subclass of `ChatClientError`
467
+ * (`ChatClientAuthError`, `ChatClientRateLimitError`, etc.) so consumers
468
+ * can branch on error class instead of parsing message strings.
469
+ *
470
+ * On the way out, reads the `X-Cstar-Auth-Mode` response header (set by
471
+ * widgetHandler on every response) and caches it on the client. After
472
+ * the first authenticated round-trip, `getAuthMode()` returns the
473
+ * server-confirmed mode.
474
+ */
353
475
  async fetch(path, options) {
354
476
  const url = this.buildUrl(path, options.query);
355
477
  const headers = {
@@ -358,17 +480,104 @@ var ChatClient = class {
358
480
  if (this.sessionToken) {
359
481
  headers["Authorization"] = `Bearer ${this.sessionToken}`;
360
482
  }
361
- const response = await globalThis.fetch(url, {
362
- method: options.method,
363
- headers,
364
- body: options.body ? JSON.stringify(options.body) : void 0
365
- });
483
+ let response;
484
+ try {
485
+ response = await globalThis.fetch(url, {
486
+ method: options.method,
487
+ headers,
488
+ body: options.body ? JSON.stringify(options.body) : void 0
489
+ });
490
+ } catch (networkErr) {
491
+ throw new ChatClientNetworkError({
492
+ message: networkErr instanceof Error ? networkErr.message : "Network request failed",
493
+ originalError: networkErr
494
+ });
495
+ }
496
+ const responseHeaders = response.headers && typeof response.headers.get === "function" ? response.headers : null;
497
+ const headerMode = responseHeaders?.get("X-Cstar-Auth-Mode") ?? null;
498
+ if (headerMode === "hmac" || headerMode === "session") {
499
+ this.authMode = headerMode;
500
+ }
501
+ const requestId = responseHeaders?.get("X-Request-Id") ?? null;
366
502
  if (!response.ok) {
367
- const errorBody = await response.json().catch(() => ({ error: { message: "Request failed" } }));
368
- throw new Error(errorBody.error?.message ?? `Widget API error: ${response.status}`);
503
+ const errorBody = await response.json().catch(() => ({ error: { code: "UNKNOWN", message: "Request failed" } }));
504
+ const code = errorBody.error?.code ?? "UNKNOWN";
505
+ const message = errorBody.error?.message ?? `Widget API error: ${response.status}`;
506
+ if (response.status === 401 || response.status === 403) {
507
+ throw new ChatClientAuthError({
508
+ message,
509
+ code,
510
+ statusCode: response.status,
511
+ requestId
512
+ });
513
+ }
514
+ if (response.status === 429) {
515
+ const retryAfterHeader = responseHeaders?.get("Retry-After") ?? null;
516
+ const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : null;
517
+ throw new ChatClientRateLimitError({
518
+ message,
519
+ code,
520
+ statusCode: response.status,
521
+ requestId,
522
+ retryAfter: Number.isFinite(retryAfter ?? NaN) ? retryAfter : null
523
+ });
524
+ }
525
+ if (response.status >= 500) {
526
+ throw new ChatClientServerError({
527
+ message,
528
+ code,
529
+ statusCode: response.status,
530
+ requestId
531
+ });
532
+ }
533
+ throw new ChatClientError(message, {
534
+ code,
535
+ statusCode: response.status,
536
+ requestId
537
+ });
369
538
  }
370
539
  return response.json();
371
540
  }
541
+ /**
542
+ * Look up the team UUID from the configured `teamSlug`. Used by
543
+ * `loginWithCredentials`. HMAC integrators (`identify()`) never hit
544
+ * this path — verify-identity accepts the slug directly.
545
+ *
546
+ * @internal
547
+ */
548
+ async resolveTeamIdFromSlug() {
549
+ const response = await this.fetch(
550
+ "/api/widget/team-by-slug",
551
+ {
552
+ method: "GET",
553
+ query: { teamSlug: this.teamSlug }
554
+ }
555
+ );
556
+ return response.data.teamId;
557
+ }
558
+ /** @internal Bootstrap realtime from a session login (no precomputed customerId). */
559
+ async initRealtimeFromSession(customerName) {
560
+ try {
561
+ this.realtimeManager = new RealtimeManager({
562
+ fetchToken: () => this.fetchRealtimeToken()
563
+ });
564
+ const ready = await this.realtimeManager.init();
565
+ if (!ready) return;
566
+ this.realtimeReady = true;
567
+ const token = this.realtimeManager.getCurrentToken();
568
+ const customerId = token ? decodeJwtSub(token) : null;
569
+ if (customerId) {
570
+ this.customerId = customerId;
571
+ this.presenceManager = new PresenceManager({ customerId, customerName });
572
+ const client = this.realtimeManager.getClient();
573
+ if (client) {
574
+ this.presenceManager.setClient(client);
575
+ }
576
+ }
577
+ } catch {
578
+ this.realtimeReady = false;
579
+ }
580
+ }
372
581
  /** @internal Get the stored team ID for requests that need it. */
373
582
  getTeamId() {
374
583
  return this.teamId;
@@ -386,8 +595,6 @@ var ChatClient = class {
386
595
  async initRealtime(customerId, customerName) {
387
596
  try {
388
597
  this.realtimeManager = new RealtimeManager({
389
- supabaseUrl: this.supabaseUrl,
390
- supabaseAnonKey: this.supabaseAnonKey,
391
598
  fetchToken: () => this.fetchRealtimeToken()
392
599
  });
393
600
  const ready = await this.realtimeManager.init();
@@ -454,28 +661,29 @@ var ChatClient = class {
454
661
  return qs ? `${base}?${qs}` : base;
455
662
  }
456
663
  };
457
- var ConversationsApi = class {
664
+ var TicketsApi = class {
458
665
  constructor(chat) {
459
666
  this.chat = chat;
460
667
  }
461
- /** List conversations for the identified customer. */
668
+ /** List tickets for the identified customer. */
462
669
  async list(params) {
463
670
  const teamId = this.chat.getTeamId();
464
671
  if (!teamId) throw new Error("Not identified. Call chat.identify() first.");
465
- const response = await this.chat.fetch(
466
- "/api/widget/conversations",
467
- {
468
- method: "GET",
469
- query: {
470
- teamId,
471
- limit: params?.limit?.toString(),
472
- cursor: params?.cursor
473
- }
672
+ const response = await this.chat.fetch("/api/widget/conversations", {
673
+ method: "GET",
674
+ query: {
675
+ teamId,
676
+ limit: params?.limit?.toString(),
677
+ cursor: params?.cursor
474
678
  }
475
- );
476
- return response.data;
679
+ });
680
+ return {
681
+ tickets: response.data.tickets ?? response.data.conversations ?? [],
682
+ cursor: response.data.cursor,
683
+ hasMore: response.data.hasMore
684
+ };
477
685
  }
478
- /** Create a new conversation (ticket). */
686
+ /** Create a new ticket. */
479
687
  async create(params) {
480
688
  const teamId = this.chat.getTeamId();
481
689
  if (!teamId) throw new Error("Not identified. Call chat.identify() first.");
@@ -633,7 +841,7 @@ var MessagesApi = class {
633
841
  constructor(chat) {
634
842
  this.chat = chat;
635
843
  }
636
- /** List messages for a conversation. */
844
+ /** List messages for a ticket. */
637
845
  async list(ticketId) {
638
846
  const teamId = this.chat.getTeamId();
639
847
  const response = await this.chat.fetch(
@@ -648,7 +856,7 @@ var MessagesApi = class {
648
856
  );
649
857
  return response.data.messages;
650
858
  }
651
- /** Send a message to a conversation. */
859
+ /** Send a message to a ticket. */
652
860
  async send(ticketId, content) {
653
861
  const teamId = this.chat.getTeamId();
654
862
  const response = await this.chat.fetch(
@@ -665,6 +873,25 @@ var MessagesApi = class {
665
873
  return response.data.message;
666
874
  }
667
875
  };
876
+ function decodeJwtSub(jwt) {
877
+ try {
878
+ const parts = jwt.split(".");
879
+ if (parts.length !== 3) return null;
880
+ const payloadB64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
881
+ const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
882
+ const payload = JSON.parse(
883
+ typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf-8")
884
+ );
885
+ return typeof payload.sub === "string" ? payload.sub : null;
886
+ } catch {
887
+ return null;
888
+ }
889
+ }
668
890
  export {
669
- ChatClient
891
+ ChatClient,
892
+ ChatClientAuthError,
893
+ ChatClientError,
894
+ ChatClientNetworkError,
895
+ ChatClientRateLimitError,
896
+ ChatClientServerError
670
897
  };
package/dist/index.cjs CHANGED
@@ -140,6 +140,16 @@ var DEFAULT_MAX_RETRIES = 3;
140
140
  var DEFAULT_TIMEOUT = 3e4;
141
141
  var HttpClient = class {
142
142
  constructor(config) {
143
+ if (!config || typeof config.apiKey !== "string" || config.apiKey.length === 0) {
144
+ throw new Error(
145
+ "[cStar] CStarClient: apiKey is required. Pass a secret key (sk_live_... / sk_test_...) or a publishable key (pk_live_... / pk_test_...). See https://cstar.help/developers/quickstart"
146
+ );
147
+ }
148
+ if (typeof config.teamId !== "string" || config.teamId.length === 0) {
149
+ throw new Error(
150
+ "[cStar] CStarClient: teamId is required. Find your team ID in Settings \u2192 General or via `cstar status`."
151
+ );
152
+ }
143
153
  this.config = {
144
154
  apiKey: config.apiKey,
145
155
  teamId: config.teamId,
@@ -1956,10 +1966,12 @@ var GameResource = class {
1956
1966
  subscribeLeaderboard(realtime, params) {
1957
1967
  const fetchFn = async () => {
1958
1968
  const res = await this.leaderboard(params);
1969
+ const envelope = res.data;
1970
+ const entries = Array.isArray(envelope) ? envelope : envelope?.entries ?? [];
1959
1971
  return {
1960
1972
  success: true,
1961
- data: res.data,
1962
- pagination: { total: res.data.length, page: 1, pageSize: 100, hasMore: false },
1973
+ data: entries,
1974
+ pagination: { total: entries.length, page: 1, pageSize: 100, hasMore: false },
1963
1975
  meta: { requestId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1964
1976
  };
1965
1977
  };