@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 +43 -25
- package/dist/auth/index.cjs +220 -21
- package/dist/auth/index.d.cts +83 -31
- package/dist/auth/index.d.ts +83 -31
- package/dist/auth/index.js +210 -21
- package/dist/chat/index.cjs +134 -22
- package/dist/chat/index.d.cts +32 -4
- package/dist/chat/index.d.ts +32 -4
- package/dist/chat/index.js +134 -22
- package/dist/index.cjs +101 -40
- package/dist/index.d.cts +477 -157
- package/dist/index.d.ts +477 -157
- package/dist/index.js +101 -40
- package/dist/proactive/index.cjs +46 -33
- package/dist/proactive/index.js +45 -34
- package/dist/webhook/index.d.cts +1 -1
- package/dist/webhook/index.d.ts +1 -1
- package/dist/webhook/node.d.cts +1 -1
- package/dist/webhook/node.d.ts +1 -1
- package/dist/{webhookEvents-1N2i2GcQ.d.cts → webhookEvents-Lqpsj935.d.cts} +0 -1
- package/dist/{webhookEvents-1N2i2GcQ.d.ts → webhookEvents-Lqpsj935.d.ts} +0 -1
- package/package.json +1 -1
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` |
|
|
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_*`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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: '
|
|
202
|
+
name: 'Critical Unresolved',
|
|
203
203
|
entityType: 'tickets',
|
|
204
|
-
filters: { priority: '
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
405
|
-
teamId: 'tm_...', // Required
|
|
406
|
-
baseUrl: 'https://...', // Optional
|
|
407
|
-
version: '2026-03-01', // Optional
|
|
408
|
-
maxRetries: 3, // Optional
|
|
409
|
-
timeout: 30000 // Optional
|
|
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
|
|
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)
|
package/dist/auth/index.cjs
CHANGED
|
@@ -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
|
|
338
|
+
* Create a password-less customer account and email a sign-in link.
|
|
319
339
|
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
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
|
-
*
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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 (
|
|
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
|
-
|
|
341
|
-
accessToken:
|
|
342
|
-
refreshToken:
|
|
480
|
+
...current,
|
|
481
|
+
accessToken: restoredSession.access_token,
|
|
482
|
+
refreshToken: restoredSession.refresh_token
|
|
343
483
|
};
|
|
344
484
|
}
|
|
345
|
-
|
|
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
|
-
/**
|
|
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, {
|
package/dist/auth/index.d.cts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
73
|
-
*
|
|
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
|
-
* //
|
|
80
|
-
* const session = await auth.
|
|
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
|
-
* //
|
|
86
|
-
*
|
|
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
|
|
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
|
-
*
|
|
98
|
-
*
|
|
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
|
|
121
|
+
* @throws CStarRateLimitError when too many signups were attempted
|
|
101
122
|
*/
|
|
102
|
-
signup(params: SignupParams): Promise<SignupResult
|
|
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
|
-
/**
|
|
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
|
|