@cstar.help/js 0.17.5 → 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 +27 -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 +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/index.cjs +89 -38
- package/dist/index.d.cts +443 -150
- package/dist/index.d.ts +443 -150
- package/dist/index.js +89 -38
- 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/dist/auth/index.d.ts
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
|
|
package/dist/auth/index.js
CHANGED
|
@@ -269,45 +269,196 @@ function parseError(statusCode, body, retryAfter) {
|
|
|
269
269
|
|
|
270
270
|
// src/auth/index.ts
|
|
271
271
|
var DEFAULT_BASE_URL = "https://www.cstar.help";
|
|
272
|
+
var PRODUCTION_ORIGINS = /* @__PURE__ */ new Set(["https://www.cstar.help", "https://cstar.help"]);
|
|
273
|
+
var PRODUCTION_SUPABASE_URL = "https://kmorvgcubqthkfrasbny.supabase.co";
|
|
274
|
+
var PRODUCTION_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imttb3J2Z2N1YnF0aGtmcmFzYm55Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjUzMzczNjIsImV4cCI6MjA4MDkxMzM2Mn0.e0RJbMpZr-8FbJmAGhdw86Ym9M4MCfvX9Qm6Va3d_N8";
|
|
275
|
+
var PKCE_STORAGE_KEY = "cstar_widget_pkce";
|
|
276
|
+
var MIN_PASSWORD_LENGTH = 8;
|
|
277
|
+
var CONFIRMATION_TYPES = /* @__PURE__ */ new Set(["magiclink", "signup", "recovery"]);
|
|
272
278
|
var AuthClient = class {
|
|
273
279
|
constructor(config) {
|
|
274
280
|
this.session = null;
|
|
281
|
+
this.pkceClient = null;
|
|
275
282
|
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
276
283
|
this.teamSlug = config.teamSlug;
|
|
284
|
+
const isProduction = PRODUCTION_ORIGINS.has(this.baseUrl);
|
|
285
|
+
this.supabaseUrl = config.supabaseUrl ?? (isProduction ? PRODUCTION_SUPABASE_URL : null);
|
|
286
|
+
this.supabaseKey = config.supabaseKey ?? (isProduction ? PRODUCTION_SUPABASE_KEY : null);
|
|
277
287
|
}
|
|
278
288
|
/**
|
|
279
|
-
* Create a
|
|
289
|
+
* Create a password-less customer account and email a sign-in link.
|
|
280
290
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
291
|
+
* The response is the same whether or not the email already has an
|
|
292
|
+
* account. Nothing is signed in yet: the customer clicks the link, and the
|
|
293
|
+
* page it returns to calls `completeSignIn()`. Signup takes no password,
|
|
294
|
+
* because one chosen before the link proves the address could belong to
|
|
295
|
+
* someone else. Once signed in, the customer can call `setPassword()` to
|
|
296
|
+
* enable `login()`.
|
|
283
297
|
*
|
|
284
|
-
*
|
|
298
|
+
* Runs in a browser: the PKCE verifier is stored in this page's
|
|
299
|
+
* localStorage so `completeSignIn()` can finish the exchange.
|
|
300
|
+
*
|
|
301
|
+
* @throws CStarRateLimitError when too many signups were attempted
|
|
285
302
|
*/
|
|
286
303
|
async signup(params) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
name: params.name
|
|
296
|
-
}
|
|
304
|
+
const returnUrl = params.returnUrl ?? (typeof window !== "undefined" && window.location ? window.location.href : void 0);
|
|
305
|
+
const prepared = await this.fetch("/api/widget/auth/signup", {
|
|
306
|
+
method: "POST",
|
|
307
|
+
body: {
|
|
308
|
+
teamId: this.teamSlug,
|
|
309
|
+
email: params.email,
|
|
310
|
+
name: params.name,
|
|
311
|
+
returnUrl
|
|
297
312
|
}
|
|
298
|
-
);
|
|
299
|
-
if (
|
|
313
|
+
});
|
|
314
|
+
if (!prepared?.emailRedirectTo) {
|
|
315
|
+
throw parsePublicError(500, null, "Sign-up prepare returned no redirect URL.");
|
|
316
|
+
}
|
|
317
|
+
const client = await this.getPkceClient();
|
|
318
|
+
const { error } = await client.auth.signInWithOtp({
|
|
319
|
+
email: params.email,
|
|
320
|
+
options: { emailRedirectTo: prepared.emailRedirectTo, shouldCreateUser: false }
|
|
321
|
+
});
|
|
322
|
+
if (error) {
|
|
323
|
+
if (error.status === 429) {
|
|
324
|
+
throw parsePublicError(
|
|
325
|
+
429,
|
|
326
|
+
{ error: "Too many sign-up requests. Please wait a few minutes and try again." },
|
|
327
|
+
"Too many sign-up requests"
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
throw parsePublicError(
|
|
331
|
+
502,
|
|
332
|
+
{
|
|
333
|
+
error: "Couldn't send a confirmation email. Try again, or sign in if you already have an account."
|
|
334
|
+
},
|
|
335
|
+
"Confirmation email failed"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
success: true,
|
|
340
|
+
message: prepared.message ?? "Check your inbox to finish signing up.",
|
|
341
|
+
requiresConfirmation: true
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Finish sign-in on the page the confirmation link returns to.
|
|
346
|
+
*
|
|
347
|
+
* Reads `?code=` and `?type=` from the URL, removes them from the address
|
|
348
|
+
* bar, exchanges the code, and stores the session. Returns null when the URL
|
|
349
|
+
* holds no cStar confirmation code or the exchange fails.
|
|
350
|
+
*
|
|
351
|
+
* @param url Defaults to `window.location.href`.
|
|
352
|
+
*/
|
|
353
|
+
async completeSignIn(url) {
|
|
354
|
+
const href = url ?? (typeof window !== "undefined" && window.location ? window.location.href : null);
|
|
355
|
+
if (!href) return null;
|
|
356
|
+
const parsed = new URL(href);
|
|
357
|
+
const code = parsed.searchParams.get("code");
|
|
358
|
+
const type = parsed.searchParams.get("type");
|
|
359
|
+
if (!code || !type || !CONFIRMATION_TYPES.has(type)) return null;
|
|
360
|
+
if (url === void 0 && typeof window !== "undefined" && window.history?.replaceState) {
|
|
361
|
+
parsed.searchParams.delete("code");
|
|
362
|
+
parsed.searchParams.delete("type");
|
|
363
|
+
try {
|
|
364
|
+
window.history.replaceState(null, "", parsed.pathname + parsed.search + parsed.hash);
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const client = await this.getPkceClient();
|
|
369
|
+
const exchanged = await client.auth.exchangeCodeForSession(code);
|
|
370
|
+
const supabaseSession = exchanged?.data?.session;
|
|
371
|
+
if (exchanged?.error || !supabaseSession) return null;
|
|
372
|
+
const result = await this.fetch("/api/widget/auth/magic-link/exchange", {
|
|
373
|
+
method: "POST",
|
|
374
|
+
body: {
|
|
375
|
+
teamId: this.teamSlug,
|
|
376
|
+
accessToken: supabaseSession.access_token,
|
|
377
|
+
refreshToken: supabaseSession.refresh_token
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
if (!result.customer || !result.accessToken || !result.refreshToken) return null;
|
|
381
|
+
this.session = {
|
|
382
|
+
customer: result.customer,
|
|
383
|
+
accessToken: result.accessToken,
|
|
384
|
+
refreshToken: result.refreshToken
|
|
385
|
+
};
|
|
386
|
+
return this.session;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Choose a password for the signed-in customer, so later visits can use
|
|
390
|
+
* `login()` instead of an emailed link.
|
|
391
|
+
*
|
|
392
|
+
* Needs a session from `completeSignIn()` or `login()`. This is the same
|
|
393
|
+
* Supabase Auth password update the hosted widget makes after a first
|
|
394
|
+
* email sign-in, and it clears the widget's "choose a password" prompt.
|
|
395
|
+
* The customer stays signed in.
|
|
396
|
+
*
|
|
397
|
+
* @throws CStarValidationError when the password is under 8 characters or is rejected
|
|
398
|
+
* @throws CStarAuthenticationError when there is no session or it has expired
|
|
399
|
+
*/
|
|
400
|
+
async setPassword(password) {
|
|
401
|
+
if (!password || password.length < MIN_PASSWORD_LENGTH) {
|
|
402
|
+
throw parsePublicError(
|
|
403
|
+
400,
|
|
404
|
+
{ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` },
|
|
405
|
+
"Invalid password"
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const current = this.session;
|
|
409
|
+
if (!current) {
|
|
410
|
+
throw parsePublicError(
|
|
411
|
+
401,
|
|
412
|
+
{ error: "Sign in before choosing a password." },
|
|
413
|
+
"Not signed in"
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const client = await this.getPkceClient();
|
|
417
|
+
const restored = await client.auth.setSession({
|
|
418
|
+
access_token: current.accessToken,
|
|
419
|
+
refresh_token: current.refreshToken
|
|
420
|
+
});
|
|
421
|
+
const restoredSession = restored?.data?.session;
|
|
422
|
+
if (restored?.error || !restoredSession) {
|
|
423
|
+
throw parsePublicError(
|
|
424
|
+
401,
|
|
425
|
+
{ error: "Your session has expired. Sign in again to choose a password." },
|
|
426
|
+
"Session expired"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (this.session === current) {
|
|
300
430
|
this.session = {
|
|
301
|
-
|
|
302
|
-
accessToken:
|
|
303
|
-
refreshToken:
|
|
431
|
+
...current,
|
|
432
|
+
accessToken: restoredSession.access_token,
|
|
433
|
+
refreshToken: restoredSession.refresh_token
|
|
304
434
|
};
|
|
305
435
|
}
|
|
306
|
-
|
|
436
|
+
const { error } = await client.auth.updateUser({
|
|
437
|
+
password,
|
|
438
|
+
data: { widget_password_pending: false }
|
|
439
|
+
});
|
|
440
|
+
if (error) {
|
|
441
|
+
if (error.status === 429) {
|
|
442
|
+
throw parsePublicError(
|
|
443
|
+
429,
|
|
444
|
+
{ error: "Too many password changes. Please wait a few minutes and try again." },
|
|
445
|
+
"Too many password changes"
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
const rejected = error.status === 400 || error.status === 422;
|
|
449
|
+
throw parsePublicError(
|
|
450
|
+
rejected ? 400 : 502,
|
|
451
|
+
{ error: "That password could not be saved. Please try another one." },
|
|
452
|
+
"Password not saved"
|
|
453
|
+
);
|
|
454
|
+
}
|
|
307
455
|
}
|
|
308
456
|
/**
|
|
309
457
|
* Log in an existing customer with email and password.
|
|
310
458
|
*
|
|
459
|
+
* Signup is password-less, so this works only after the customer has
|
|
460
|
+
* chosen a password with `setPassword()` or in the hosted widget.
|
|
461
|
+
*
|
|
311
462
|
* On success, stores the session internally so subsequent
|
|
312
463
|
* ChatClient requests can use `auth.getSession()` for tokens.
|
|
313
464
|
*
|
|
@@ -331,9 +482,18 @@ var AuthClient = class {
|
|
|
331
482
|
}
|
|
332
483
|
return response;
|
|
333
484
|
}
|
|
334
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* Clear the session: the in-memory one and, when a sign-in link or
|
|
487
|
+
* setPassword() restored it through Supabase Auth, the copy that client
|
|
488
|
+
* keeps in browser storage, so the next person on the device is signed out.
|
|
489
|
+
*/
|
|
335
490
|
logout() {
|
|
336
491
|
this.session = null;
|
|
492
|
+
const pkceClient = this.pkceClient;
|
|
493
|
+
if (pkceClient) {
|
|
494
|
+
void pkceClient.then((client) => client.auth.signOut({ scope: "local" })).catch(() => {
|
|
495
|
+
});
|
|
496
|
+
}
|
|
337
497
|
}
|
|
338
498
|
/** Get the current session, or null if not authenticated. */
|
|
339
499
|
getSession() {
|
|
@@ -351,6 +511,35 @@ var AuthClient = class {
|
|
|
351
511
|
get accessToken() {
|
|
352
512
|
return this.session?.accessToken ?? null;
|
|
353
513
|
}
|
|
514
|
+
getPkceClient() {
|
|
515
|
+
const { supabaseUrl, supabaseKey } = this;
|
|
516
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
517
|
+
return Promise.reject(
|
|
518
|
+
parsePublicError(
|
|
519
|
+
400,
|
|
520
|
+
{
|
|
521
|
+
error: "AuthClient needs supabaseUrl and supabaseKey when baseUrl is not https://www.cstar.help."
|
|
522
|
+
},
|
|
523
|
+
"Missing Supabase config"
|
|
524
|
+
)
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
this.pkceClient ?? (this.pkceClient = import(
|
|
528
|
+
/* webpackIgnore: true */
|
|
529
|
+
"@supabase/supabase-js"
|
|
530
|
+
).then(
|
|
531
|
+
({ createClient }) => createClient(supabaseUrl, supabaseKey, {
|
|
532
|
+
auth: {
|
|
533
|
+
flowType: "pkce",
|
|
534
|
+
persistSession: true,
|
|
535
|
+
autoRefreshToken: false,
|
|
536
|
+
storageKey: PKCE_STORAGE_KEY,
|
|
537
|
+
detectSessionInUrl: false
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
));
|
|
541
|
+
return this.pkceClient;
|
|
542
|
+
}
|
|
354
543
|
async fetch(path, options) {
|
|
355
544
|
const url = `${this.baseUrl}${path}`;
|
|
356
545
|
const response = await globalThis.fetch(url, {
|
package/dist/chat/index.cjs
CHANGED
|
@@ -475,6 +475,8 @@ var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
|
475
475
|
var ChatClient = class {
|
|
476
476
|
constructor(config) {
|
|
477
477
|
this.sessionToken = null;
|
|
478
|
+
this.refreshToken = null;
|
|
479
|
+
this.refreshPromise = null;
|
|
478
480
|
this.teamId = null;
|
|
479
481
|
this.customerId = null;
|
|
480
482
|
this.authMode = null;
|
|
@@ -567,6 +569,7 @@ var ChatClient = class {
|
|
|
567
569
|
this.applySession(
|
|
568
570
|
{
|
|
569
571
|
accessToken: response.accessToken,
|
|
572
|
+
refreshToken: response.refreshToken ?? void 0,
|
|
570
573
|
teamId,
|
|
571
574
|
customerName: response.customer.name ?? response.customer.email
|
|
572
575
|
},
|
|
@@ -597,6 +600,7 @@ var ChatClient = class {
|
|
|
597
600
|
applySession(params, generation) {
|
|
598
601
|
this.assertSession(generation);
|
|
599
602
|
this.sessionToken = params.accessToken;
|
|
603
|
+
this.refreshToken = params.refreshToken ?? null;
|
|
600
604
|
if (params.teamId) {
|
|
601
605
|
this.teamId = params.teamId;
|
|
602
606
|
}
|
|
@@ -706,6 +710,8 @@ var ChatClient = class {
|
|
|
706
710
|
this.pollingSubscriptions.clear();
|
|
707
711
|
this.realtimeMessageSubscriptions.clear();
|
|
708
712
|
this.sessionToken = null;
|
|
713
|
+
this.refreshToken = null;
|
|
714
|
+
this.refreshPromise = null;
|
|
709
715
|
this.teamId = null;
|
|
710
716
|
this.customerId = null;
|
|
711
717
|
this.authMode = null;
|
|
@@ -737,7 +743,7 @@ var ChatClient = class {
|
|
|
737
743
|
* the first authenticated round-trip, `getAuthMode()` returns the
|
|
738
744
|
* server-confirmed mode.
|
|
739
745
|
*/
|
|
740
|
-
async fetch(path, options) {
|
|
746
|
+
async fetch(path, options, retriedAfterRefresh = false) {
|
|
741
747
|
const generation = this.sessionGeneration;
|
|
742
748
|
const assertRequest = () => {
|
|
743
749
|
this.assertSession(generation);
|
|
@@ -748,8 +754,10 @@ var ChatClient = class {
|
|
|
748
754
|
const headers = {
|
|
749
755
|
"Content-Type": "application/json"
|
|
750
756
|
};
|
|
751
|
-
if (
|
|
752
|
-
|
|
757
|
+
if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
758
|
+
const sentToken = this.sessionToken;
|
|
759
|
+
if (sentToken) {
|
|
760
|
+
headers["Authorization"] = `Bearer ${sentToken}`;
|
|
753
761
|
}
|
|
754
762
|
let response;
|
|
755
763
|
try {
|
|
@@ -777,11 +785,17 @@ var ChatClient = class {
|
|
|
777
785
|
}
|
|
778
786
|
assertRequest();
|
|
779
787
|
const requestId = responseHeaders?.get("X-Request-Id") ?? null;
|
|
788
|
+
if (response.status === 401 && sentToken && this.refreshToken && !retriedAfterRefresh) {
|
|
789
|
+
const refreshed = await this.refreshSession(sentToken);
|
|
790
|
+
assertRequest();
|
|
791
|
+
if (refreshed) return this.fetch(path, options, true);
|
|
792
|
+
}
|
|
780
793
|
if (!response.ok) {
|
|
781
|
-
const errorBody = await response.json().catch(() =>
|
|
794
|
+
const errorBody = await response.json().catch(() => null);
|
|
782
795
|
assertRequest();
|
|
783
|
-
const
|
|
784
|
-
const
|
|
796
|
+
const structured = errorBody?.error && typeof errorBody.error === "object" ? errorBody.error : null;
|
|
797
|
+
const code = structured?.code ?? "UNKNOWN";
|
|
798
|
+
const message = structured?.message ?? (typeof errorBody?.error === "string" ? errorBody.error : null) ?? (typeof errorBody?.message === "string" ? errorBody.message : null) ?? `Widget API error: ${response.status}`;
|
|
785
799
|
if (response.status === 401 || response.status === 403) {
|
|
786
800
|
throw new ChatClientAuthError({
|
|
787
801
|
message,
|
|
@@ -822,9 +836,52 @@ var ChatClient = class {
|
|
|
822
836
|
return body;
|
|
823
837
|
} catch (error) {
|
|
824
838
|
assertRequest();
|
|
839
|
+
if (options.idempotencyKey)
|
|
840
|
+
throw new ChatClientNetworkError({
|
|
841
|
+
message: "Could not read the message acknowledgment",
|
|
842
|
+
originalError: error
|
|
843
|
+
});
|
|
825
844
|
throw error;
|
|
826
845
|
}
|
|
827
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Renew the access token with the stored refresh token. Concurrent 401s
|
|
849
|
+
* share one refresh. Resolves true when a newer token is in place.
|
|
850
|
+
*/
|
|
851
|
+
refreshSession(rejectedToken) {
|
|
852
|
+
if (this.sessionToken && this.sessionToken !== rejectedToken) return Promise.resolve(true);
|
|
853
|
+
if (!this.refreshPromise) {
|
|
854
|
+
const pending = this.requestRefresh().finally(() => {
|
|
855
|
+
if (this.refreshPromise === pending) this.refreshPromise = null;
|
|
856
|
+
});
|
|
857
|
+
this.refreshPromise = pending;
|
|
858
|
+
}
|
|
859
|
+
return this.refreshPromise;
|
|
860
|
+
}
|
|
861
|
+
async requestRefresh() {
|
|
862
|
+
const generation = this.sessionGeneration;
|
|
863
|
+
const refreshToken = this.refreshToken;
|
|
864
|
+
if (!refreshToken) return false;
|
|
865
|
+
try {
|
|
866
|
+
const response = await globalThis.fetch(`${this.baseUrl}/api/widget/auth/refresh`, {
|
|
867
|
+
method: "POST",
|
|
868
|
+
headers: { "Content-Type": "application/json" },
|
|
869
|
+
body: JSON.stringify({ refreshToken })
|
|
870
|
+
});
|
|
871
|
+
if (generation !== this.sessionGeneration) return false;
|
|
872
|
+
if (!response.ok) {
|
|
873
|
+
if (response.status === 401) this.refreshToken = null;
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
const body = await response.json();
|
|
877
|
+
if (generation !== this.sessionGeneration || !body.accessToken) return false;
|
|
878
|
+
this.sessionToken = body.accessToken;
|
|
879
|
+
this.refreshToken = body.refreshToken ?? refreshToken;
|
|
880
|
+
return true;
|
|
881
|
+
} catch {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
828
885
|
/**
|
|
829
886
|
* Look up the team UUID from the configured `teamSlug`. Used by
|
|
830
887
|
* `loginWithCredentials`. HMAC integrators (`identify()`) never hit
|
|
@@ -1297,26 +1354,80 @@ var MessagesApi = class {
|
|
|
1297
1354
|
assertCurrent();
|
|
1298
1355
|
return response.data.messages;
|
|
1299
1356
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
|
|
1357
|
+
/**
|
|
1358
|
+
* Send one logical reply. Network and server failures retry twice with the
|
|
1359
|
+
* same key. A separate send call is a new intent unless a retained key is supplied.
|
|
1360
|
+
*/
|
|
1361
|
+
async send(ticketId, content, options = {}) {
|
|
1302
1362
|
const assertCurrent = this.chat.captureSession();
|
|
1363
|
+
options.signal?.throwIfAborted();
|
|
1364
|
+
const key = options.idempotencyKey ?? createMessageRequestKey();
|
|
1365
|
+
if (!key || key.length > 250 || key.trim() !== key || /[^\x20-\x7e]/.test(key))
|
|
1366
|
+
throw new ChatClientError({
|
|
1367
|
+
code: "INVALID_IDEMPOTENCY_KEY",
|
|
1368
|
+
message: "Idempotency-Key must contain 1 to 250 printable characters, without surrounding whitespace."
|
|
1369
|
+
});
|
|
1303
1370
|
const teamId = await this.chat.ensureTeamId();
|
|
1304
1371
|
assertCurrent();
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1372
|
+
for (let attempt = 0; ; attempt++) {
|
|
1373
|
+
assertCurrent();
|
|
1374
|
+
options.signal?.throwIfAborted();
|
|
1375
|
+
try {
|
|
1376
|
+
const response = await this.chat.fetch(
|
|
1377
|
+
"/api/widget/messages",
|
|
1378
|
+
{
|
|
1379
|
+
method: "POST",
|
|
1380
|
+
body: { teamId, ticketId, content },
|
|
1381
|
+
idempotencyKey: key,
|
|
1382
|
+
signal: options.signal
|
|
1383
|
+
}
|
|
1384
|
+
);
|
|
1385
|
+
assertCurrent();
|
|
1386
|
+
if (typeof response?.data?.message?.id !== "string" || !response.data.message.id)
|
|
1387
|
+
throw new ChatClientNetworkError({
|
|
1388
|
+
message: "Message acknowledgment was incomplete",
|
|
1389
|
+
originalError: new Error(
|
|
1390
|
+
"The reply may already be saved. Retry with the same send identity."
|
|
1391
|
+
)
|
|
1392
|
+
});
|
|
1393
|
+
return response.data.message;
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
assertCurrent();
|
|
1396
|
+
options.signal?.throwIfAborted();
|
|
1397
|
+
if (error instanceof ChatClientError) error.idempotencyKey = key;
|
|
1398
|
+
if (attempt >= 2 || !(error instanceof ChatClientNetworkError || error instanceof ChatClientServerError))
|
|
1399
|
+
throw error;
|
|
1400
|
+
await waitForMessageRetry(250 * 2 ** attempt, options.signal);
|
|
1314
1401
|
}
|
|
1315
|
-
|
|
1316
|
-
assertCurrent();
|
|
1317
|
-
return response.data.message;
|
|
1402
|
+
}
|
|
1318
1403
|
}
|
|
1319
1404
|
};
|
|
1405
|
+
function createMessageRequestKey() {
|
|
1406
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
1407
|
+
if (typeof globalThis.crypto?.getRandomValues !== "function")
|
|
1408
|
+
throw new ChatClientError({
|
|
1409
|
+
code: "RANDOM_UNAVAILABLE",
|
|
1410
|
+
message: "Secure random values are required to send a message safely."
|
|
1411
|
+
});
|
|
1412
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
1413
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1414
|
+
}
|
|
1415
|
+
function waitForMessageRetry(delay, signal) {
|
|
1416
|
+
signal?.throwIfAborted();
|
|
1417
|
+
return new Promise((resolve, reject) => {
|
|
1418
|
+
const finish = () => {
|
|
1419
|
+
signal?.removeEventListener("abort", abort);
|
|
1420
|
+
resolve();
|
|
1421
|
+
};
|
|
1422
|
+
const timer = setTimeout(finish, delay);
|
|
1423
|
+
const abort = () => {
|
|
1424
|
+
clearTimeout(timer);
|
|
1425
|
+
signal?.removeEventListener("abort", abort);
|
|
1426
|
+
reject(signal?.reason);
|
|
1427
|
+
};
|
|
1428
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1320
1431
|
function decodeJwtSub(jwt) {
|
|
1321
1432
|
try {
|
|
1322
1433
|
const parts = jwt.split(".");
|