@cstar.help/js 0.17.4 → 0.18.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.
- package/README.md +45 -26
- package/dist/auth/index.cjs +220 -21
- package/dist/auth/index.d.cts +84 -32
- package/dist/auth/index.d.ts +84 -32
- package/dist/auth/index.js +210 -21
- package/dist/chat/index.cjs +131 -20
- package/dist/chat/index.d.cts +32 -4
- package/dist/chat/index.d.ts +32 -4
- package/dist/chat/index.js +131 -20
- package/dist/community/index.d.cts +1 -1
- package/dist/community/index.d.ts +1 -1
- package/dist/errors-bW8CwTn4.d.cts +121 -0
- package/dist/errors-bW8CwTn4.d.ts +121 -0
- package/dist/index.cjs +94 -41
- package/dist/index.d.cts +454 -153
- package/dist/index.d.ts +454 -153
- package/dist/index.js +94 -41
- package/dist/library/index.d.cts +1 -1
- package/dist/library/index.d.ts +1 -1
- package/dist/proactive/index.cjs +46 -33
- package/dist/proactive/index.js +45 -34
- package/dist/quickhelp/index.d.cts +1 -1
- package/dist/quickhelp/index.d.ts +1 -1
- 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/dist/errors-b5A4wfAg.d.cts +0 -398
- package/dist/errors-b5A4wfAg.d.ts +0 -398
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.
|
|
@@ -71,19 +71,36 @@ for await (const ticket of cstar.tickets.listAutoPaginating({ status: 'open' }))
|
|
|
71
71
|
|
|
72
72
|
Created, retrieved and updated resources are returned directly. List methods return `{ data, pagination, hasMore }`. Search returns `{ results, total, query }`. Read `cstar.lastMeta.requestId` immediately after a call when you need its request ID.
|
|
73
73
|
|
|
74
|
+
### Private notes and message history
|
|
75
|
+
|
|
76
|
+
Run message management on your server with a secret key. Internal notes stay inside the support team.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const note = await cstar.tickets.messages.create('tkt_123', {
|
|
80
|
+
content: 'The customer has already tried resetting their password.',
|
|
81
|
+
messageType: 'internal'
|
|
82
|
+
});
|
|
83
|
+
console.log(note.isInternal);
|
|
84
|
+
|
|
85
|
+
const page = await cstar.tickets.messages.list('tkt_123', { page: 2, pageSize: 25 });
|
|
86
|
+
console.log(page.data, page.pagination.hasMore);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The `isInternal` boolean is also accepted when creating a note. If you supply both privacy options, they must agree. Message history is paginated, so request the next page while `hasMore` is true.
|
|
90
|
+
|
|
74
91
|
### Full Resource Reference
|
|
75
92
|
|
|
76
|
-
**Core Resources
|
|
93
|
+
**Core Resources**: Tickets, customers, and knowledge base.
|
|
77
94
|
|
|
78
95
|
| Resource | Methods |
|
|
79
96
|
| ------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
80
97
|
| `cstar.tickets` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` `.listAutoPaginating()` |
|
|
81
|
-
| `cstar.tickets.messages` | `.list(ticketId)` `.create(ticketId, params)`
|
|
98
|
+
| `cstar.tickets.messages` | `.list(ticketId, params?)` `.create(ticketId, params)` |
|
|
82
99
|
| `cstar.customers` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` `.listAutoPaginating()` |
|
|
83
100
|
| `cstar.articles` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` `.listAutoPaginating()` |
|
|
84
101
|
| `cstar.categories` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` |
|
|
85
102
|
|
|
86
|
-
**Team & Configuration
|
|
103
|
+
**Team & Configuration**: Members, settings, and customization.
|
|
87
104
|
|
|
88
105
|
| Resource | Methods |
|
|
89
106
|
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -93,9 +110,9 @@ Created, retrieved and updated resources are returned directly. List methods ret
|
|
|
93
110
|
| `cstar.settings.statuses` | `.list()` `.create(params)` `.update(key, params)` `.del(key)` `.reorder(keys)` |
|
|
94
111
|
| `cstar.settings.quickReplies` | `.list()` `.create(params)` `.update(id, params)` `.del(id)` `.reorder(ids)` |
|
|
95
112
|
| `cstar.customFields` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` `.reorder(ids)` |
|
|
96
|
-
| `cstar.notifications` | `.list()` `.unreadCount()` `.markRead(id)` `.markAllRead()`
|
|
113
|
+
| `cstar.notifications` | `.list({ userId })` `.unreadCount(userId)` `.markRead(id, userId)` `.markAllRead(userId)` |
|
|
97
114
|
|
|
98
|
-
**Operations
|
|
115
|
+
**Operations**: Analytics, bulk actions, and data management.
|
|
99
116
|
|
|
100
117
|
| Resource | Methods |
|
|
101
118
|
| ---------------------- | --------------------------------------------------------------------------------------- |
|
|
@@ -110,7 +127,7 @@ Created, retrieved and updated resources are returned directly. List methods ret
|
|
|
110
127
|
| `cstar.auditLog` | `.list(params?)` |
|
|
111
128
|
| `cstar.customerGroups` | `.list()` `.get(id)` `.create(params)` `.update(id, params)` `.del(id)` |
|
|
112
129
|
|
|
113
|
-
**Gamification
|
|
130
|
+
**Gamification**: The full game system.
|
|
114
131
|
|
|
115
132
|
| Resource | Methods |
|
|
116
133
|
| ------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
@@ -127,9 +144,9 @@ Created, retrieved and updated resources are returned directly. List methods ret
|
|
|
127
144
|
|
|
128
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`.
|
|
129
146
|
|
|
130
|
-
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.
|
|
131
148
|
|
|
132
|
-
**Advanced
|
|
149
|
+
**Advanced**: AI, automations, views, billing, webhooks.
|
|
133
150
|
|
|
134
151
|
| Resource | Methods |
|
|
135
152
|
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -182,9 +199,9 @@ const csv = await cstar.export.create({ resources: ['tickets'], format: 'csv' })
|
|
|
182
199
|
|
|
183
200
|
// Saved views
|
|
184
201
|
const view = await cstar.views.create({
|
|
185
|
-
name: '
|
|
202
|
+
name: 'Critical Unresolved',
|
|
186
203
|
entityType: 'tickets',
|
|
187
|
-
filters: { priority: '
|
|
204
|
+
filters: { priority: 'critical', status: 'open' }
|
|
188
205
|
});
|
|
189
206
|
const { data: results } = await cstar.views.execute(view.id);
|
|
190
207
|
|
|
@@ -287,7 +304,9 @@ function closeChat() {
|
|
|
287
304
|
|
|
288
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.
|
|
289
306
|
|
|
290
|
-
|
|
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.
|
|
291
310
|
|
|
292
311
|
```typescript
|
|
293
312
|
import {
|
|
@@ -321,7 +340,7 @@ try {
|
|
|
321
340
|
|
|
322
341
|
## Client-Side: Public Knowledge Base
|
|
323
342
|
|
|
324
|
-
Search and browse your public help center
|
|
343
|
+
Search and browse your public help center. **No authentication required**.
|
|
325
344
|
|
|
326
345
|
```typescript
|
|
327
346
|
import { LibraryClient } from '@cstar.help/js/library';
|
|
@@ -337,7 +356,7 @@ const article = await library.article('how-to-reset-password');
|
|
|
337
356
|
|
|
338
357
|
## Client-Side: Public Community
|
|
339
358
|
|
|
340
|
-
Browse your public community forum
|
|
359
|
+
Browse your public community forum. **No authentication required**.
|
|
341
360
|
|
|
342
361
|
```typescript
|
|
343
362
|
import { CommunityClient } from '@cstar.help/js/community';
|
|
@@ -356,17 +375,17 @@ const { post, comments } = await community.post('dark-mode-support');
|
|
|
356
375
|
Verify incoming webhook signatures on your server.
|
|
357
376
|
|
|
358
377
|
```typescript
|
|
359
|
-
import { constructEvent } from '@cstar.help/js/webhook';
|
|
378
|
+
import { constructEvent } from '@cstar.help/js/webhook/node';
|
|
360
379
|
|
|
361
|
-
// In your webhook handler
|
|
380
|
+
// In your webhook handler (Node). It throws if the signature is invalid.
|
|
362
381
|
const event = constructEvent(rawBody, signature, 'whsec_your_secret');
|
|
363
382
|
|
|
364
383
|
switch (event.type) {
|
|
365
384
|
case 'ticket.created':
|
|
366
|
-
console.log('New ticket:', event.data.title);
|
|
385
|
+
console.log('New ticket:', event.data.ticket.title);
|
|
367
386
|
break;
|
|
368
387
|
case 'customer.created':
|
|
369
|
-
console.log('New customer:', event.data.email);
|
|
388
|
+
console.log('New customer:', event.data.customer.email);
|
|
370
389
|
break;
|
|
371
390
|
}
|
|
372
391
|
```
|
|
@@ -384,12 +403,12 @@ const event = await constructEventAsync(rawBody, signature, secret);
|
|
|
384
403
|
|
|
385
404
|
```typescript
|
|
386
405
|
const cstar = new CStarClient({
|
|
387
|
-
apiKey: 'sk_live_...', // Required
|
|
388
|
-
teamId: 'tm_...', // Required
|
|
389
|
-
baseUrl: 'https://...', // Optional
|
|
390
|
-
version: '2026-03-01', // Optional
|
|
391
|
-
maxRetries: 3, // Optional
|
|
392
|
-
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
|
|
393
412
|
});
|
|
394
413
|
```
|
|
395
414
|
|
|
@@ -417,7 +436,7 @@ const cstar = new CStarClient({
|
|
|
417
436
|
});
|
|
418
437
|
|
|
419
438
|
console.log(cstar.isTestMode); // true
|
|
420
|
-
// All operations use test data
|
|
439
|
+
// All operations use test data, so no real customers are affected
|
|
421
440
|
```
|
|
422
441
|
|
|
423
442
|
## Error Handling
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as ApiErrorBody, C as CStarAuthenticationError, a as CStarConflictError, b as CStarError, c as CStarFeatureDisabledError, d as CStarNotFoundError, e as CStarPermissionError, f as CStarRateLimitError, g as CStarRealtimeUnavailableError, h as CStarServerError, i as CStarValidationError, E as ErrorType, p as parseError, j as parsePublicError } from '../errors-
|
|
1
|
+
export { A as ApiErrorBody, C as CStarAuthenticationError, a as CStarConflictError, b as CStarError, c as CStarFeatureDisabledError, d as CStarNotFoundError, e as CStarPermissionError, f as CStarRateLimitError, g as CStarRealtimeUnavailableError, h as CStarServerError, i as CStarValidationError, E as ErrorType, p as parseError, j as parsePublicError } from '../errors-bW8CwTn4.cjs';
|
|
2
2
|
|
|
3
3
|
/** Configuration for the AuthClient. */
|
|
4
4
|
interface AuthConfig {
|
|
@@ -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
|
|