@cstar.help/js 0.17.5 → 0.18.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/README.md CHANGED
@@ -26,14 +26,14 @@ cStar has one package with separate entry points for different workflows. Start
26
26
  | `@cstar.help/js/library` | Public knowledge base | Browser or server | No | None |
27
27
  | `@cstar.help/js/community` | Public community forum | Browser or server | No | None |
28
28
  | `@cstar.help/js/webhook` | Webhook signature verification | Server only | No | Signing secret |
29
- | `@cstar.help/js/auth` | Authentication utilities | Server only | Yes | Secret (`sk_*`) |
29
+ | `@cstar.help/js/auth` | Customer signup and login | Browser | No | None (`teamSlug`) |
30
30
 
31
31
  ### The Security Model
32
32
 
33
33
  The server API and customer chat authenticate differently:
34
34
 
35
35
  - **Secret API keys** (`sk_live_*`, `sk_test_*`) authorize team API operations within the key's permissions. Keep them on your server.
36
- - **Publishable API keys** (`pk_live_*`, `pk_test_*`) provide limited read-only access to API endpoints that accept them. They do not identify a customer or authorize customer chat.
36
+ - **Publishable API keys** (`pk_live_*`, `pk_test_*`) are for the public SDKs (`LibraryClient`, `CommunityClient`) and `GET /auth/whoami`. Team API endpoints refuse them with 403: `publishable_key_forbidden` on reads and `publishable_key_write` on writes, so `CStarClient` needs a secret key. They do not identify a customer or authorize customer chat.
37
37
  - **ChatClient** takes a `teamSlug`, then authenticates a customer through HMAC identity verification or a customer session. It has no API-key option. Keep HMAC signing secrets on your server too.
38
38
 
39
39
  > Building a support dashboard? Use `CStarClient` on your backend. Adding a conversation to your website? Use `ChatClient` and one of the customer authentication options below.
@@ -90,7 +90,7 @@ The `isInternal` boolean is also accepted when creating a note. If you supply bo
90
90
 
91
91
  ### Full Resource Reference
92
92
 
93
- **Core Resources** — Tickets, customers, and knowledge base.
93
+ **Core Resources**: Tickets, customers, and knowledge base.
94
94
 
95
95
  | Resource | Methods |
96
96
  | ------------------------ | ----------------------------------------------------------------------------------------------- |
@@ -100,7 +100,7 @@ The `isInternal` boolean is also accepted when creating a note. If you supply bo
100
100
  | `cstar.articles` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` `.listAutoPaginating()` |
101
101
  | `cstar.categories` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` |
102
102
 
103
- **Team & Configuration** — Members, settings, and customization.
103
+ **Team & Configuration**: Members, settings, and customization.
104
104
 
105
105
  | Resource | Methods |
106
106
  | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -110,9 +110,9 @@ The `isInternal` boolean is also accepted when creating a note. If you supply bo
110
110
  | `cstar.settings.statuses` | `.list()` `.create(params)` `.update(key, params)` `.del(key)` `.reorder(keys)` |
111
111
  | `cstar.settings.quickReplies` | `.list()` `.create(params)` `.update(id, params)` `.del(id)` `.reorder(ids)` |
112
112
  | `cstar.customFields` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` `.reorder(ids)` |
113
- | `cstar.notifications` | `.list()` `.unreadCount()` `.markRead(id)` `.markAllRead()` |
113
+ | `cstar.notifications` | `.list({ userId })` `.unreadCount(userId)` `.markRead(id, userId)` `.markAllRead(userId)` |
114
114
 
115
- **Operations** — Analytics, bulk actions, and data management.
115
+ **Operations**: Analytics, bulk actions, and data management.
116
116
 
117
117
  | Resource | Methods |
118
118
  | ---------------------- | --------------------------------------------------------------------------------------- |
@@ -127,7 +127,7 @@ The `isInternal` boolean is also accepted when creating a note. If you supply bo
127
127
  | `cstar.auditLog` | `.list(params?)` |
128
128
  | `cstar.customerGroups` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` |
129
129
 
130
- **Gamification** — The full game system.
130
+ **Gamification**: The full game system.
131
131
 
132
132
  | Resource | Methods |
133
133
  | ------------------------- | --------------------------------------------------------------------------------------------------- |
@@ -144,9 +144,9 @@ The `isInternal` boolean is also accepted when creating a note. If you supply bo
144
144
 
145
145
  Game writes belong on your server with a secret API key. Use user and team UUIDs from cStar. One-ups and high fives default their sender to the key creator. Puzzle scoreboards require a `puzzleId`; streak boards use `{ type: 'streaks' }`. Cosmetic lists require a `userId`.
146
146
 
147
- Cosmetic and season lists return `{ data, pagination, hasMore }`. One-up and high-five lists return `{ object: 'list', data }`. Today's quests return `{ quests, completedToday, totalQuests, allComplete, completionBonus, resetsAt }`. Single resources return bare objects.
147
+ Cosmetic and season lists return `{ data, pagination, hasMore }`. One-up and high-five lists return `{ object: 'list', data }`. Today's quests return `{ quests, completedToday, totalQuests, allComplete, resetsAt }` (each quest pays its own `xpReward` and `goldReward`; there is no all-quests bonus). Single resources return bare objects.
148
148
 
149
- **Advanced** — AI, automations, views, billing, webhooks.
149
+ **Advanced**: AI, automations, views, billing, webhooks.
150
150
 
151
151
  | Resource | Methods |
152
152
  | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -199,9 +199,9 @@ const csv = await cstar.export.create({ resources: ['tickets'], format: 'csv' })
199
199
 
200
200
  // Saved views
201
201
  const view = await cstar.views.create({
202
- name: 'Urgent Unresolved',
202
+ name: 'Critical Unresolved',
203
203
  entityType: 'tickets',
204
- filters: { priority: 'urgent', status: 'open' }
204
+ filters: { priority: 'critical', status: 'open' }
205
205
  });
206
206
  const { data: results } = await cstar.views.execute(view.id);
207
207
 
@@ -304,7 +304,9 @@ function closeChat() {
304
304
 
305
305
  HTTP and network failures use typed subclasses of `ChatClientError`. A server response may provide a `requestId` through the `X-Request-Id` header; it is `null` when unavailable. Other failures, such as malformed response JSON, can still throw ordinary errors.
306
306
 
307
- A lost response may follow a successful send. Keep the unsent text available and check the conversation before offering another send. Do not automatically resend a message because a network or server error occurred.
307
+ Customer message sends generate a new retry key for each intentional reply and retry network or server failures at most twice. Each retry uses the same key and returns the original saved reply. A terminal `ChatClientError` retains that key in `error.idempotencyKey`. Reuse it with `chat.messages.send(ticketId, content, { idempotencyKey })` for a manual retry of the same reply. Keep the draft and key together; use a new key when the message or ticket changes. Receipts last 24 hours and stay scoped to the customer, team and live/test mode.
308
+
309
+ Pass `{ signal }` to cancel a send or its retry delay. For a cancellable UI, generate and retain your own `idempotencyKey` before sending. A cancelled request may already have been saved. Conversation history includes `client_request_key` so your UI can reconcile a pending bubble with its confirmed reply. Separate sends with identical text remain separate messages. These message retries do not apply to ticket creation or other customer writes.
308
310
 
309
311
  ```typescript
310
312
  import {
@@ -338,7 +340,7 @@ try {
338
340
 
339
341
  ## Client-Side: Public Knowledge Base
340
342
 
341
- Search and browse your public help center — **no authentication required**.
343
+ Search and browse your public help center. **No authentication required**.
342
344
 
343
345
  ```typescript
344
346
  import { LibraryClient } from '@cstar.help/js/library';
@@ -354,7 +356,7 @@ const article = await library.article('how-to-reset-password');
354
356
 
355
357
  ## Client-Side: Public Community
356
358
 
357
- Browse your public community forum — **no authentication required**.
359
+ Browse your public community forum. **No authentication required**.
358
360
 
359
361
  ```typescript
360
362
  import { CommunityClient } from '@cstar.help/js/community';
@@ -373,17 +375,17 @@ const { post, comments } = await community.post('dark-mode-support');
373
375
  Verify incoming webhook signatures on your server.
374
376
 
375
377
  ```typescript
376
- import { constructEvent } from '@cstar.help/js/webhook';
378
+ import { constructEvent } from '@cstar.help/js/webhook/node';
377
379
 
378
- // In your webhook handler
380
+ // In your webhook handler (Node). It throws if the signature is invalid.
379
381
  const event = constructEvent(rawBody, signature, 'whsec_your_secret');
380
382
 
381
383
  switch (event.type) {
382
384
  case 'ticket.created':
383
- console.log('New ticket:', event.data.title);
385
+ console.log('New ticket:', event.data.ticket.title);
384
386
  break;
385
387
  case 'customer.created':
386
- console.log('New customer:', event.data.email);
388
+ console.log('New customer:', event.data.customer.email);
387
389
  break;
388
390
  }
389
391
  ```
@@ -401,12 +403,12 @@ const event = await constructEventAsync(rawBody, signature, secret);
401
403
 
402
404
  ```typescript
403
405
  const cstar = new CStarClient({
404
- apiKey: 'sk_live_...', // Required — must be a secret key (sk_*)
405
- teamId: 'tm_...', // Required — your team ID
406
- baseUrl: 'https://...', // Optional — defaults to https://www.cstar.help
407
- version: '2026-03-01', // Optional — API version header
408
- maxRetries: 3, // Optional — retry attempts for retry-safe failures and 429
409
- timeout: 30000 // Optional — request timeout in ms
406
+ apiKey: 'sk_live_...', // Required: must be a secret key (sk_*)
407
+ teamId: 'tm_...', // Required: your team ID
408
+ baseUrl: 'https://...', // Optional: defaults to https://www.cstar.help
409
+ version: '2026-03-01', // Optional: API version header
410
+ maxRetries: 3, // Optional: retry attempts for retry-safe failures and 429
411
+ timeout: 30000 // Optional: request timeout in ms
410
412
  });
411
413
  ```
412
414
 
@@ -434,7 +436,7 @@ const cstar = new CStarClient({
434
436
  });
435
437
 
436
438
  console.log(cstar.isTestMode); // true
437
- // All operations use test data — no real customers affected
439
+ // All operations use test data, so no real customers are affected
438
440
  ```
439
441
 
440
442
  ## Error Handling
@@ -478,6 +480,22 @@ const ticket = await cstar.tickets.create(
478
480
  );
479
481
  ```
480
482
 
483
+ For 24 hours, the same request with the same key returns the first successful
484
+ response. The same key with a different body throws `CStarValidationError`
485
+ (`idempotency_key_reused`, 422). While the first request is still running, the
486
+ API answers `409 idempotency_key_in_use` with `Retry-After`. From 0.18.1
487
+ (unreleased), the client waits and retries within `maxRetries`; 0.18.0 throws
488
+ `CStarConflictError`.
489
+
490
+ The API ignores the key on `webhooks.create`, `webhooks.listen` and
491
+ `export.create`, so a key does not make their retries safe: the client still
492
+ retries a network error or 5xx. Leave the key off those calls (without one the
493
+ client does not retry them), or use a client with `maxRetries: 0`. Check
494
+ `webhooks.list()` before you retry `webhooks.create`. A repeated `export.create`
495
+ only reads your data again (nothing is stored). A repeated `webhooks.listen` is a
496
+ second CLI listener: remove it with `webhooks.unlisten(id)`, or it is swept after
497
+ 5 minutes without a heartbeat.
498
+
481
499
  ## Requirements
482
500
 
483
501
  - Node.js 22+ (matching the supported Supabase dependency)
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/auth/index.ts
@@ -308,45 +318,196 @@ function parseError(statusCode, body, retryAfter) {
308
318
 
309
319
  // src/auth/index.ts
310
320
  var DEFAULT_BASE_URL = "https://www.cstar.help";
321
+ var PRODUCTION_ORIGINS = /* @__PURE__ */ new Set(["https://www.cstar.help", "https://cstar.help"]);
322
+ var PRODUCTION_SUPABASE_URL = "https://kmorvgcubqthkfrasbny.supabase.co";
323
+ var PRODUCTION_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imttb3J2Z2N1YnF0aGtmcmFzYm55Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjUzMzczNjIsImV4cCI6MjA4MDkxMzM2Mn0.e0RJbMpZr-8FbJmAGhdw86Ym9M4MCfvX9Qm6Va3d_N8";
324
+ var PKCE_STORAGE_KEY = "cstar_widget_pkce";
325
+ var MIN_PASSWORD_LENGTH = 8;
326
+ var CONFIRMATION_TYPES = /* @__PURE__ */ new Set(["magiclink", "signup", "recovery"]);
311
327
  var AuthClient = class {
312
328
  constructor(config) {
313
329
  this.session = null;
330
+ this.pkceClient = null;
314
331
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
315
332
  this.teamSlug = config.teamSlug;
333
+ const isProduction = PRODUCTION_ORIGINS.has(this.baseUrl);
334
+ this.supabaseUrl = config.supabaseUrl ?? (isProduction ? PRODUCTION_SUPABASE_URL : null);
335
+ this.supabaseKey = config.supabaseKey ?? (isProduction ? PRODUCTION_SUPABASE_KEY : null);
316
336
  }
317
337
  /**
318
- * Create a new customer account.
338
+ * Create a password-less customer account and email a sign-in link.
319
339
  *
320
- * If the team requires email confirmation, returns a `SignupConfirmationResult`
321
- * instead of tokens. Check for `requiresConfirmation` in the response.
340
+ * The response is the same whether or not the email already has an
341
+ * account. Nothing is signed in yet: the customer clicks the link, and the
342
+ * page it returns to calls `completeSignIn()`. Signup takes no password,
343
+ * because one chosen before the link proves the address could belong to
344
+ * someone else. Once signed in, the customer can call `setPassword()` to
345
+ * enable `login()`.
322
346
  *
323
- * @throws Error on validation failure, duplicate email, or server error
347
+ * Runs in a browser: the PKCE verifier is stored in this page's
348
+ * localStorage so `completeSignIn()` can finish the exchange.
349
+ *
350
+ * @throws CStarRateLimitError when too many signups were attempted
324
351
  */
325
352
  async signup(params) {
326
- const response = await this.fetch(
327
- "/api/widget/auth/signup",
328
- {
329
- method: "POST",
330
- body: {
331
- teamId: this.teamSlug,
332
- email: params.email,
333
- password: params.password,
334
- name: params.name
335
- }
353
+ const returnUrl = params.returnUrl ?? (typeof window !== "undefined" && window.location ? window.location.href : void 0);
354
+ const prepared = await this.fetch("/api/widget/auth/signup", {
355
+ method: "POST",
356
+ body: {
357
+ teamId: this.teamSlug,
358
+ email: params.email,
359
+ name: params.name,
360
+ returnUrl
336
361
  }
337
- );
338
- if ("accessToken" in response && response.accessToken && response.customer) {
362
+ });
363
+ if (!prepared?.emailRedirectTo) {
364
+ throw parsePublicError(500, null, "Sign-up prepare returned no redirect URL.");
365
+ }
366
+ const client = await this.getPkceClient();
367
+ const { error } = await client.auth.signInWithOtp({
368
+ email: params.email,
369
+ options: { emailRedirectTo: prepared.emailRedirectTo, shouldCreateUser: false }
370
+ });
371
+ if (error) {
372
+ if (error.status === 429) {
373
+ throw parsePublicError(
374
+ 429,
375
+ { error: "Too many sign-up requests. Please wait a few minutes and try again." },
376
+ "Too many sign-up requests"
377
+ );
378
+ }
379
+ throw parsePublicError(
380
+ 502,
381
+ {
382
+ error: "Couldn't send a confirmation email. Try again, or sign in if you already have an account."
383
+ },
384
+ "Confirmation email failed"
385
+ );
386
+ }
387
+ return {
388
+ success: true,
389
+ message: prepared.message ?? "Check your inbox to finish signing up.",
390
+ requiresConfirmation: true
391
+ };
392
+ }
393
+ /**
394
+ * Finish sign-in on the page the confirmation link returns to.
395
+ *
396
+ * Reads `?code=` and `?type=` from the URL, removes them from the address
397
+ * bar, exchanges the code, and stores the session. Returns null when the URL
398
+ * holds no cStar confirmation code or the exchange fails.
399
+ *
400
+ * @param url Defaults to `window.location.href`.
401
+ */
402
+ async completeSignIn(url) {
403
+ const href = url ?? (typeof window !== "undefined" && window.location ? window.location.href : null);
404
+ if (!href) return null;
405
+ const parsed = new URL(href);
406
+ const code = parsed.searchParams.get("code");
407
+ const type = parsed.searchParams.get("type");
408
+ if (!code || !type || !CONFIRMATION_TYPES.has(type)) return null;
409
+ if (url === void 0 && typeof window !== "undefined" && window.history?.replaceState) {
410
+ parsed.searchParams.delete("code");
411
+ parsed.searchParams.delete("type");
412
+ try {
413
+ window.history.replaceState(null, "", parsed.pathname + parsed.search + parsed.hash);
414
+ } catch {
415
+ }
416
+ }
417
+ const client = await this.getPkceClient();
418
+ const exchanged = await client.auth.exchangeCodeForSession(code);
419
+ const supabaseSession = exchanged?.data?.session;
420
+ if (exchanged?.error || !supabaseSession) return null;
421
+ const result = await this.fetch("/api/widget/auth/magic-link/exchange", {
422
+ method: "POST",
423
+ body: {
424
+ teamId: this.teamSlug,
425
+ accessToken: supabaseSession.access_token,
426
+ refreshToken: supabaseSession.refresh_token
427
+ }
428
+ });
429
+ if (!result.customer || !result.accessToken || !result.refreshToken) return null;
430
+ this.session = {
431
+ customer: result.customer,
432
+ accessToken: result.accessToken,
433
+ refreshToken: result.refreshToken
434
+ };
435
+ return this.session;
436
+ }
437
+ /**
438
+ * Choose a password for the signed-in customer, so later visits can use
439
+ * `login()` instead of an emailed link.
440
+ *
441
+ * Needs a session from `completeSignIn()` or `login()`. This is the same
442
+ * Supabase Auth password update the hosted widget makes after a first
443
+ * email sign-in, and it clears the widget's "choose a password" prompt.
444
+ * The customer stays signed in.
445
+ *
446
+ * @throws CStarValidationError when the password is under 8 characters or is rejected
447
+ * @throws CStarAuthenticationError when there is no session or it has expired
448
+ */
449
+ async setPassword(password) {
450
+ if (!password || password.length < MIN_PASSWORD_LENGTH) {
451
+ throw parsePublicError(
452
+ 400,
453
+ { error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` },
454
+ "Invalid password"
455
+ );
456
+ }
457
+ const current = this.session;
458
+ if (!current) {
459
+ throw parsePublicError(
460
+ 401,
461
+ { error: "Sign in before choosing a password." },
462
+ "Not signed in"
463
+ );
464
+ }
465
+ const client = await this.getPkceClient();
466
+ const restored = await client.auth.setSession({
467
+ access_token: current.accessToken,
468
+ refresh_token: current.refreshToken
469
+ });
470
+ const restoredSession = restored?.data?.session;
471
+ if (restored?.error || !restoredSession) {
472
+ throw parsePublicError(
473
+ 401,
474
+ { error: "Your session has expired. Sign in again to choose a password." },
475
+ "Session expired"
476
+ );
477
+ }
478
+ if (this.session === current) {
339
479
  this.session = {
340
- customer: response.customer,
341
- accessToken: response.accessToken,
342
- refreshToken: response.refreshToken
480
+ ...current,
481
+ accessToken: restoredSession.access_token,
482
+ refreshToken: restoredSession.refresh_token
343
483
  };
344
484
  }
345
- return response;
485
+ const { error } = await client.auth.updateUser({
486
+ password,
487
+ data: { widget_password_pending: false }
488
+ });
489
+ if (error) {
490
+ if (error.status === 429) {
491
+ throw parsePublicError(
492
+ 429,
493
+ { error: "Too many password changes. Please wait a few minutes and try again." },
494
+ "Too many password changes"
495
+ );
496
+ }
497
+ const rejected = error.status === 400 || error.status === 422;
498
+ throw parsePublicError(
499
+ rejected ? 400 : 502,
500
+ { error: "That password could not be saved. Please try another one." },
501
+ "Password not saved"
502
+ );
503
+ }
346
504
  }
347
505
  /**
348
506
  * Log in an existing customer with email and password.
349
507
  *
508
+ * Signup is password-less, so this works only after the customer has
509
+ * chosen a password with `setPassword()` or in the hosted widget.
510
+ *
350
511
  * On success, stores the session internally so subsequent
351
512
  * ChatClient requests can use `auth.getSession()` for tokens.
352
513
  *
@@ -370,9 +531,18 @@ var AuthClient = class {
370
531
  }
371
532
  return response;
372
533
  }
373
- /** Clear the stored session. */
534
+ /**
535
+ * Clear the session: the in-memory one and, when a sign-in link or
536
+ * setPassword() restored it through Supabase Auth, the copy that client
537
+ * keeps in browser storage, so the next person on the device is signed out.
538
+ */
374
539
  logout() {
375
540
  this.session = null;
541
+ const pkceClient = this.pkceClient;
542
+ if (pkceClient) {
543
+ void pkceClient.then((client) => client.auth.signOut({ scope: "local" })).catch(() => {
544
+ });
545
+ }
376
546
  }
377
547
  /** Get the current session, or null if not authenticated. */
378
548
  getSession() {
@@ -390,6 +560,35 @@ var AuthClient = class {
390
560
  get accessToken() {
391
561
  return this.session?.accessToken ?? null;
392
562
  }
563
+ getPkceClient() {
564
+ const { supabaseUrl, supabaseKey } = this;
565
+ if (!supabaseUrl || !supabaseKey) {
566
+ return Promise.reject(
567
+ parsePublicError(
568
+ 400,
569
+ {
570
+ error: "AuthClient needs supabaseUrl and supabaseKey when baseUrl is not https://www.cstar.help."
571
+ },
572
+ "Missing Supabase config"
573
+ )
574
+ );
575
+ }
576
+ this.pkceClient ?? (this.pkceClient = import(
577
+ /* webpackIgnore: true */
578
+ "@supabase/supabase-js"
579
+ ).then(
580
+ ({ createClient }) => createClient(supabaseUrl, supabaseKey, {
581
+ auth: {
582
+ flowType: "pkce",
583
+ persistSession: true,
584
+ autoRefreshToken: false,
585
+ storageKey: PKCE_STORAGE_KEY,
586
+ detectSessionInUrl: false
587
+ }
588
+ })
589
+ ));
590
+ return this.pkceClient;
591
+ }
393
592
  async fetch(path, options) {
394
593
  const url = `${this.baseUrl}${path}`;
395
594
  const response = await globalThis.fetch(url, {
@@ -6,6 +6,14 @@ interface AuthConfig {
6
6
  teamSlug: string;
7
7
  /** Override the base URL (defaults to https://www.cstar.help). */
8
8
  baseUrl?: string;
9
+ /**
10
+ * Supabase project URL used to send the signup confirmation email.
11
+ * Defaults to cStar's production project. Required when `baseUrl` points
12
+ * anywhere other than production, so the email comes from the same project.
13
+ */
14
+ supabaseUrl?: string;
15
+ /** Supabase publishable (anon) key that pairs with `supabaseUrl`. */
16
+ supabaseKey?: string;
9
17
  }
10
18
  /** A customer identity returned from auth endpoints. */
11
19
  interface AuthCustomer {
@@ -17,26 +25,33 @@ interface AuthCustomer {
17
25
  interface SignupParams {
18
26
  /** Customer email address. */
19
27
  email: string;
20
- /** Password (minimum 6 characters). */
21
- password: string;
22
28
  /** Customer display name. */
23
29
  name?: string;
30
+ /**
31
+ * Page the confirmation link returns to. Defaults to the current page in a
32
+ * browser. Its origin must be on the team's allowed redirect list, or cStar
33
+ * falls back to the hosted widget page.
34
+ */
35
+ returnUrl?: string;
24
36
  }
25
- /** Response from a successful signup. */
37
+ /**
38
+ * Response from signup. It is identical for new and existing emails, so a
39
+ * signup form cannot be used to discover who already has an account. The
40
+ * customer confirms through the emailed link; call `completeSignIn()` on the
41
+ * page the link returns to.
42
+ */
26
43
  interface SignupResult {
27
- customer: AuthCustomer;
28
- /** Access token for authenticated requests. Null if email confirmation is required. */
29
- accessToken: string | null;
30
- /** Refresh token for renewing the session. Null if email confirmation is required. */
31
- refreshToken: string | null;
32
- }
33
- /** Response when email confirmation is required after signup. */
34
- interface SignupConfirmationResult {
35
44
  success: true;
36
45
  message: string;
37
46
  requiresConfirmation: true;
38
47
  }
39
- /** Parameters for logging in an existing customer. */
48
+ /** @deprecated Signup always requires confirmation now. Use `SignupResult`. */
49
+ type SignupConfirmationResult = SignupResult;
50
+ /**
51
+ * Parameters for logging in an existing customer. Signup is password-less,
52
+ * so this works only after the customer has chosen a password (see
53
+ * `AuthClient.setPassword()`).
54
+ */
40
55
  interface LoginParams {
41
56
  /** Customer email address. */
42
57
  email: string;
@@ -69,47 +84,83 @@ interface AuthSession {
69
84
  *
70
85
  * const auth = new AuthClient({ teamSlug: 'acme' });
71
86
  *
72
- * // Sign up a new customer
73
- * const result = await auth.signup({
74
- * email: 'jane@example.com',
75
- * password: 'securepass123',
76
- * name: 'Jane Doe',
77
- * });
87
+ * // Sign up: cStar emails a sign-in link. No password yet.
88
+ * await auth.signup({ email: 'jane@example.com', name: 'Jane Doe' });
78
89
  *
79
- * // Log in an existing customer
80
- * const session = await auth.login({
81
- * email: 'jane@example.com',
82
- * password: 'securepass123',
83
- * });
90
+ * // On the page the link returns to
91
+ * const session = await auth.completeSignIn();
84
92
  *
85
- * // Check current session
86
- * const current = auth.getSession();
93
+ * // Optional: choose a password for faster sign-in next time
94
+ * await auth.setPassword('securepass123');
95
+ *
96
+ * // Later visits: log in with that password
97
+ * await auth.login({ email: 'jane@example.com', password: 'securepass123' });
87
98
  * ```
88
99
  */
89
100
  declare class AuthClient {
90
101
  private readonly baseUrl;
91
102
  private readonly teamSlug;
103
+ private readonly supabaseUrl;
104
+ private readonly supabaseKey;
92
105
  private session;
106
+ private pkceClient;
93
107
  constructor(config: AuthConfig);
94
108
  /**
95
- * Create a new customer account.
109
+ * Create a password-less customer account and email a sign-in link.
110
+ *
111
+ * The response is the same whether or not the email already has an
112
+ * account. Nothing is signed in yet: the customer clicks the link, and the
113
+ * page it returns to calls `completeSignIn()`. Signup takes no password,
114
+ * because one chosen before the link proves the address could belong to
115
+ * someone else. Once signed in, the customer can call `setPassword()` to
116
+ * enable `login()`.
96
117
  *
97
- * If the team requires email confirmation, returns a `SignupConfirmationResult`
98
- * instead of tokens. Check for `requiresConfirmation` in the response.
118
+ * Runs in a browser: the PKCE verifier is stored in this page's
119
+ * localStorage so `completeSignIn()` can finish the exchange.
99
120
  *
100
- * @throws Error on validation failure, duplicate email, or server error
121
+ * @throws CStarRateLimitError when too many signups were attempted
101
122
  */
102
- signup(params: SignupParams): Promise<SignupResult | SignupConfirmationResult>;
123
+ signup(params: SignupParams): Promise<SignupResult>;
124
+ /**
125
+ * Finish sign-in on the page the confirmation link returns to.
126
+ *
127
+ * Reads `?code=` and `?type=` from the URL, removes them from the address
128
+ * bar, exchanges the code, and stores the session. Returns null when the URL
129
+ * holds no cStar confirmation code or the exchange fails.
130
+ *
131
+ * @param url Defaults to `window.location.href`.
132
+ */
133
+ completeSignIn(url?: string): Promise<AuthSession | null>;
134
+ /**
135
+ * Choose a password for the signed-in customer, so later visits can use
136
+ * `login()` instead of an emailed link.
137
+ *
138
+ * Needs a session from `completeSignIn()` or `login()`. This is the same
139
+ * Supabase Auth password update the hosted widget makes after a first
140
+ * email sign-in, and it clears the widget's "choose a password" prompt.
141
+ * The customer stays signed in.
142
+ *
143
+ * @throws CStarValidationError when the password is under 8 characters or is rejected
144
+ * @throws CStarAuthenticationError when there is no session or it has expired
145
+ */
146
+ setPassword(password: string): Promise<void>;
103
147
  /**
104
148
  * Log in an existing customer with email and password.
105
149
  *
150
+ * Signup is password-less, so this works only after the customer has
151
+ * chosen a password with `setPassword()` or in the hosted widget.
152
+ *
106
153
  * On success, stores the session internally so subsequent
107
154
  * ChatClient requests can use `auth.getSession()` for tokens.
108
155
  *
109
156
  * @throws Error on invalid credentials, unconfirmed email, or server error
110
157
  */
111
158
  login(params: LoginParams): Promise<LoginResult>;
112
- /** Clear the stored session. */
159
+ /**
160
+ * Clear the session: the in-memory one and, when a sign-in link or
161
+ * setPassword() restored it through Supabase Auth, the copy that client
162
+ * keeps in browser storage, so the next person on the device is signed out.
163
+ */
113
164
  logout(): void;
114
165
  /** Get the current session, or null if not authenticated. */
115
166
  getSession(): AuthSession | null;
@@ -119,6 +170,7 @@ declare class AuthClient {
119
170
  get isAuthenticated(): boolean;
120
171
  /** Get the current access token, or null. Useful for passing to ChatClient. */
121
172
  get accessToken(): string | null;
173
+ private getPkceClient;
122
174
  private fetch;
123
175
  }
124
176