@rehers/rehers-roleplay-sdk 2.5.6 → 3.0.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 CHANGED
@@ -17,21 +17,27 @@ Add this once, above all your routes. It initializes the SDK for the logged-in S
17
17
  Production flow:
18
18
 
19
19
  1. Your backend requests a short-lived `userToken` from `POST /api/seamless/auth/user-token`
20
- 2. Your frontend receives that `userToken`
21
- 3. You pass `publishableKey` + `userToken` into the SDK
20
+ 2. Your frontend provides a `getUserToken()` callback that calls your backend route
21
+ 3. The SDK calls `getUserToken()` on startup and before the iframe session expires
22
22
 
23
- The browser should not mint sessions from raw `userId`, `userEmail`, or `userRole` in production.
23
+ The browser should not mint sessions from raw identity fields in production.
24
24
 
25
25
  ```tsx
26
26
  import { SeamlessRoleplayProvider } from "@rehers/rehers-roleplay-sdk/react";
27
27
 
28
28
  function App() {
29
- const userToken = useRoleplayUserToken(); // fetched from your backend
29
+ async function getUserToken() {
30
+ const tokenRes = await fetch("/api/roleplay/user-token", {
31
+ method: "POST",
32
+ credentials: "include",
33
+ }).then((r) => r.json());
34
+
35
+ return tokenRes.userToken;
36
+ }
30
37
 
31
38
  return (
32
39
  <SeamlessRoleplayProvider
33
- publishableKey="pk_live_..."
34
- userToken={userToken}
40
+ getUserToken={getUserToken}
35
41
  onReady={() => console.log("Roleplay SDK ready")}
36
42
  onError={(err) => console.error("Roleplay SDK error", err)}
37
43
  >
@@ -56,7 +62,9 @@ const tokenRes = await fetch("/api/roleplay/user-token", {
56
62
  const userToken = tokenRes.userToken;
57
63
  ```
58
64
 
59
- That's the only setup. Everything below just works.
65
+ The SDK owns session refresh timing and will call `getUserToken()` again before
66
+ the embedded app session expires. That's the only setup. Everything below just
67
+ works.
60
68
 
61
69
  ---
62
70
 
@@ -222,8 +230,14 @@ import "@rehers/rehers-roleplay-sdk";
222
230
 
223
231
  // Initialize once
224
232
  SeamlessRoleplay.init({
225
- publishableKey: "pk_live_...",
226
- userToken: "...",
233
+ getUserToken: async () => {
234
+ const res = await fetch("/api/roleplay/user-token", {
235
+ method: "POST",
236
+ credentials: "include",
237
+ });
238
+ const data = await res.json();
239
+ return data.userToken;
240
+ },
227
241
  onReady() { console.log("ready"); },
228
242
  });
229
243
 
package/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
+ export type SeamlessRoleplayUserTokenProvider = () => string | Promise<string>;
2
+
1
3
  interface SeamlessRoleplayInitBase {
2
- /** Publishable API key (starts with pk_live_ or pk_test_) */
3
- publishableKey: string;
4
- /** Optional user role for syncing permissions ("owner" | "admin" | "member") */
5
- userRole?: "owner" | "admin" | "member";
6
- /** Optional short-lived signed JWT for identity verification */
7
- userToken?: string;
4
+ /**
5
+ * Returns a fresh short-lived signed user JWT minted by your backend for the
6
+ * currently signed-in Seamless user. The SDK calls this on startup and before
7
+ * its iframe session expires.
8
+ */
9
+ getUserToken: SeamlessRoleplayUserTokenProvider;
8
10
  /** Override the app origin — where the iframe loads from (for dev/testing only) */
9
11
  origin?: string;
10
12
  /** Called when the SDK session is ready */
@@ -13,22 +15,7 @@ interface SeamlessRoleplayInitBase {
13
15
  onError?: (error: { code: string; message: string }) => void;
14
16
  }
15
17
 
16
- export type SeamlessRoleplayInitOptions =
17
- | (SeamlessRoleplayInitBase & {
18
- /** Preferred production auth path: signed Seamless bootstrap token */
19
- userToken: string;
20
- /** Optional fallback fields kept for local demos/internal tools */
21
- userId?: string;
22
- userEmail?: string;
23
- })
24
- | (SeamlessRoleplayInitBase & {
25
- /** Legacy/demo fallback path when no signed token is available */
26
- userToken?: string;
27
- /** Logged-in Seamless user ID. Pass String(me.id) from GET /api/users/me */
28
- userId: string;
29
- /** Logged-in Seamless user email. Pass me.username from GET /api/users/me */
30
- userEmail: string;
31
- });
18
+ export type SeamlessRoleplayInitOptions = SeamlessRoleplayInitBase;
32
19
 
33
20
  export interface SeamlessRoleplayOpenData {
34
21
  /** Full name of the contact */
@@ -83,7 +70,7 @@ export interface AddToScenarioOptions {
83
70
  }
84
71
 
85
72
  export interface SeamlessRoleplaySDK {
86
- /** Initialize the SDK with a publishable key. */
73
+ /** Initialize the SDK with a host-provided user token callback. */
87
74
  init(options: SeamlessRoleplayInitOptions): void;
88
75
  /** Open the roleplay modal for a contact (dialog mode). */
89
76
  open(data: SeamlessRoleplayOpenData): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehers/rehers-roleplay-sdk",
3
- "version": "2.5.6",
3
+ "version": "3.0.0",
4
4
  "sideEffects": true,
5
5
  "description": "Seamless Roleplay SDK — embed roleplay call sessions via a modal + iframe",
6
6
  "main": "roleplay-sdk.js",
package/react.d.ts CHANGED
@@ -13,7 +13,7 @@ interface SeamlessRoleplayContextValue {
13
13
  export type SeamlessRoleplayProviderProps = SeamlessRoleplayInitOptions & {
14
14
  children: ReactNode;
15
15
  };
16
- export declare function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, userRole, userToken, origin, onReady, onError, children, }: SeamlessRoleplayProviderProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function SeamlessRoleplayProvider({ getUserToken, origin, onReady, onError, children, }: SeamlessRoleplayProviderProps): import("react/jsx-runtime").JSX.Element;
17
17
  export declare function useSeamlessRoleplay(): SeamlessRoleplayContextValue;
18
18
  export interface RoleplayDialogProps {
19
19
  open: boolean;
package/react.js CHANGED
@@ -22,7 +22,7 @@ function getSDK() {
22
22
  }
23
23
  const SeamlessRoleplayContext = createContext(null);
24
24
  let providerMountCount = 0;
25
- export function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, userRole, userToken, origin, onReady, onError, children, }) {
25
+ export function SeamlessRoleplayProvider({ getUserToken, origin, onReady, onError, children, }) {
26
26
  const [state, setState] = useState({ isReady: false, error: null });
27
27
  const mountedRef = useRef(false);
28
28
  const onReadyRef = useCallbackRef(onReady);
@@ -36,57 +36,31 @@ export function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, us
36
36
  }
37
37
  mountedRef.current = true;
38
38
  setState({ isReady: false, error: null });
39
- const initOptions = userToken
40
- ? {
41
- publishableKey,
42
- userToken,
43
- userId,
44
- userEmail,
45
- userRole,
46
- origin,
47
- onReady: () => {
48
- var _a;
49
- if (!mountedRef.current)
50
- return;
51
- setState({ isReady: true, error: null });
52
- (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
53
- },
54
- onError: (err) => {
55
- var _a;
56
- if (!mountedRef.current)
57
- return;
58
- setState({ isReady: false, error: err });
59
- (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err);
60
- },
61
- }
62
- : {
63
- publishableKey,
64
- userId: userId || "",
65
- userEmail: userEmail || "",
66
- userRole,
67
- origin,
68
- onReady: () => {
69
- var _a;
70
- if (!mountedRef.current)
71
- return;
72
- setState({ isReady: true, error: null });
73
- (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
74
- },
75
- onError: (err) => {
76
- var _a;
77
- if (!mountedRef.current)
78
- return;
79
- setState({ isReady: false, error: err });
80
- (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err);
81
- },
82
- };
39
+ const initOptions = {
40
+ getUserToken,
41
+ origin,
42
+ onReady: () => {
43
+ var _a;
44
+ if (!mountedRef.current)
45
+ return;
46
+ setState({ isReady: true, error: null });
47
+ (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
48
+ },
49
+ onError: (err) => {
50
+ var _a;
51
+ if (!mountedRef.current)
52
+ return;
53
+ setState({ isReady: false, error: err });
54
+ (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err);
55
+ },
56
+ };
83
57
  sdk.init(initOptions);
84
58
  return () => {
85
59
  mountedRef.current = false;
86
60
  providerMountCount--;
87
61
  sdk.destroy();
88
62
  };
89
- }, [sdk, publishableKey, userId, userEmail, userRole, userToken, origin]);
63
+ }, [sdk, getUserToken, origin]);
90
64
  const contextValue = useMemo(() => ({ ...state, sdk }), [state, sdk]);
91
65
  return (_jsx(SeamlessRoleplayContext.Provider, { value: contextValue, children: children }));
92
66
  }
package/roleplay-sdk.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * SeamlessRoleplay SDK v2
2
+ * SeamlessRoleplay SDK v3
3
3
  *
4
- * Publishable-key auth model. No build step required.
4
+ * User-token auth model. No build step required.
5
5
  *
6
6
  * Usage:
7
- * SeamlessRoleplay.init({ publishableKey: 'pk_live_...', userToken: 'jwt...' });
7
+ * SeamlessRoleplay.init({ getUserToken: async () => 'jwt...' });
8
8
  * SeamlessRoleplay.open({ name: '...', domain: '...', company: '...', title: '...' });
9
9
  */
10
10
  (function () {
@@ -16,17 +16,17 @@
16
16
  var SDK_LOG_PREFIX = "[SeamlessRoleplay]";
17
17
 
18
18
  // ── Auth state ────────────────────────────────────────────────────
19
- var publishableKey = null;
20
- var userId = null;
21
- var userEmail = null;
22
- var userRole = null;
23
- var userToken = null;
19
+ var getUserToken = null;
20
+ var latestUserToken = null;
21
+ var userTokenRequest = null;
24
22
  var paymentLink = null;
25
23
  var appOrigin = null;
26
24
 
27
25
  var sessionToken = null;
28
26
  var sessionExpiresAt = 0; // epoch ms
27
+ var sessionRefreshBufferMs = 30000;
29
28
  var refreshTimer = null;
29
+ var refreshRetryDelayMs = 5000;
30
30
  var fetchingSession = null; // single-flight Promise
31
31
  var activeSessionXhr = null;
32
32
  var activeInitVersion = 0;
@@ -93,6 +93,16 @@
93
93
  }
94
94
  }
95
95
 
96
+ function normalizeToken(value) {
97
+ if (typeof value !== "string") return null;
98
+ var trimmed = value.trim();
99
+ return trimmed ? trimmed : null;
100
+ }
101
+
102
+ function makeError(code, message) {
103
+ return { code: code, message: message };
104
+ }
105
+
96
106
  // ── Session management ────────────────────────────────────────────
97
107
 
98
108
  function clearSessionRequest() {
@@ -105,91 +115,236 @@
105
115
  fetchingSession = null;
106
116
  }
107
117
 
108
- function fetchSession() {
118
+ function resolveUserToken() {
119
+ if (typeof getUserToken !== "function") {
120
+ return Promise.reject(
121
+ makeError("INVALID_INIT", "requires { getUserToken }")
122
+ );
123
+ }
124
+
125
+ if (userTokenRequest) return userTokenRequest;
126
+
127
+ userTokenRequest = Promise.resolve()
128
+ .then(function () {
129
+ return getUserToken();
130
+ })
131
+ .then(
132
+ function (value) {
133
+ userTokenRequest = null;
134
+ var token = normalizeToken(value);
135
+ if (!token) {
136
+ throw makeError(
137
+ "USER_TOKEN_ERROR",
138
+ "getUserToken() did not return a valid userToken"
139
+ );
140
+ }
141
+ latestUserToken = token;
142
+ return token;
143
+ },
144
+ function (err) {
145
+ userTokenRequest = null;
146
+ throw makeError(
147
+ (err && err.code) || "USER_TOKEN_ERROR",
148
+ (err && err.message) || "Failed to get a fresh userToken"
149
+ );
150
+ }
151
+ );
152
+
153
+ return userTokenRequest;
154
+ }
155
+
156
+ function dispatchRefreshToTarget(targetIframe) {
157
+ if (!sessionToken) return;
158
+ sendMsg(targetIframe, {
159
+ type: "seamless-session-refresh",
160
+ sessionToken: sessionToken,
161
+ });
162
+ }
163
+
164
+ function broadcastSessionRefresh() {
165
+ if (mountIframe) dispatchRefreshToTarget(mountIframe);
166
+ if (dialogIframe) dispatchRefreshToTarget(dialogIframe);
167
+ }
168
+
169
+ function notifySessionRefreshFailed(err) {
170
+ var error = {
171
+ code: "SESSION_REFRESH_FAILED",
172
+ message:
173
+ (err && err.message) ||
174
+ "Failed to refresh the SDK session before it expired",
175
+ };
176
+
177
+ try {
178
+ if (initCallbacks.onError) initCallbacks.onError(error);
179
+ } catch (_) {}
180
+ try {
181
+ if (mountCallbacks.onError) mountCallbacks.onError(error);
182
+ } catch (_) {}
183
+ try {
184
+ if (dialogCallbacks.onError) dialogCallbacks.onError(error);
185
+ } catch (_) {}
186
+ try {
187
+ if (dialogAddToScenarioCallbacks.onError) {
188
+ dialogAddToScenarioCallbacks.onError(error);
189
+ }
190
+ } catch (_) {}
191
+ }
192
+
193
+ function computeRefreshBuffer(ttlMs) {
194
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) return 30000;
195
+ return Math.min(30000, Math.max(1000, Math.floor(ttlMs * 0.1)));
196
+ }
197
+
198
+ function computeRefreshDelay(ttlMs) {
199
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) return 5000;
200
+
201
+ var eightyPercent = Math.floor(ttlMs * 0.8);
202
+ var oneMinuteBeforeExpiry = ttlMs - 60000;
203
+ var target =
204
+ oneMinuteBeforeExpiry > 0
205
+ ? Math.min(eightyPercent, oneMinuteBeforeExpiry)
206
+ : Math.floor(ttlMs * 0.5);
207
+
208
+ return Math.max(target, 1000);
209
+ }
210
+
211
+ function scheduleRefreshRetry(err) {
212
+ if (!initCalled || !sessionExpiresAt) return;
213
+ if (refreshTimer) clearTimeout(refreshTimer);
214
+
215
+ var remainingMs = sessionExpiresAt - Date.now();
216
+ if (remainingMs <= sessionRefreshBufferMs) {
217
+ notifySessionRefreshFailed(err);
218
+ return;
219
+ }
220
+
221
+ var delay = Math.min(
222
+ refreshRetryDelayMs,
223
+ Math.max(1000, remainingMs - sessionRefreshBufferMs)
224
+ );
225
+ refreshRetryDelayMs = Math.min(refreshRetryDelayMs * 2, 60000);
226
+
227
+ refreshTimer = setTimeout(function () {
228
+ fetchSession({ broadcastRefresh: true }).catch(scheduleRefreshRetry);
229
+ }, delay);
230
+ }
231
+
232
+ function fetchSession(options) {
233
+ options = options || {};
109
234
  var requestInitVersion = activeInitVersion;
110
235
 
111
236
  if (fetchingSession && fetchingSession.initVersion === requestInitVersion) {
237
+ if (options.broadcastRefresh) {
238
+ return fetchingSession.promise.then(function (result) {
239
+ if (
240
+ requestInitVersion === activeInitVersion &&
241
+ result &&
242
+ result.sessionToken
243
+ ) {
244
+ broadcastSessionRefresh();
245
+ }
246
+ return result;
247
+ });
248
+ }
112
249
  return fetchingSession.promise;
113
250
  }
114
251
 
115
- var requestPromise = new Promise(function (resolve, reject) {
116
- var url = getApiOrigin() + "/api/sdk/session";
117
- var body = userToken
118
- ? { userToken: userToken }
119
- : { userId: userId, userEmail: userEmail, userRole: userRole };
120
-
121
- var xhr = new XMLHttpRequest();
122
- activeSessionXhr = xhr;
123
- xhr.open("POST", url, true);
124
- xhr.setRequestHeader("Content-Type", "application/json");
125
- xhr.setRequestHeader("X-Publishable-Key", publishableKey);
126
- xhr.withCredentials = false;
127
- xhr.timeout = SESSION_TIMEOUT_MS;
128
-
129
- function cleanupRequest() {
130
- if (activeSessionXhr === xhr) {
131
- activeSessionXhr = null;
132
- }
133
- if (fetchingSession && fetchingSession.promise === requestPromise) {
134
- fetchingSession = null;
135
- }
252
+ var requestPromise = resolveUserToken().then(function (freshUserToken) {
253
+ if (requestInitVersion !== activeInitVersion) {
254
+ throw makeError("ABORTED", "Stale session request ignored");
136
255
  }
137
256
 
138
- xhr.onload = function () {
139
- cleanupRequest();
257
+ return new Promise(function (resolve, reject) {
258
+ var url = getApiOrigin() + "/api/sdk/session";
259
+ var body = { userToken: freshUserToken };
140
260
 
141
- if (requestInitVersion !== activeInitVersion) {
142
- reject({ code: "ABORTED", message: "Stale session response ignored" });
143
- return;
144
- }
261
+ var xhr = new XMLHttpRequest();
262
+ activeSessionXhr = xhr;
263
+ xhr.open("POST", url, true);
264
+ xhr.setRequestHeader("Content-Type", "application/json");
265
+ xhr.withCredentials = false;
266
+ xhr.timeout = SESSION_TIMEOUT_MS;
145
267
 
146
- var data;
147
- try {
148
- data = JSON.parse(xhr.responseText);
149
- } catch (e) {
150
- reject({ code: "PARSE_ERROR", message: "Invalid response from session endpoint" });
151
- return;
268
+ function cleanupRequest() {
269
+ if (activeSessionXhr === xhr) {
270
+ activeSessionXhr = null;
271
+ }
272
+ if (fetchingSession && fetchingSession.promise === requestPromise) {
273
+ fetchingSession = null;
274
+ }
152
275
  }
153
276
 
154
- if (xhr.status === 200 && data.sessionToken) {
155
- sessionToken = data.sessionToken;
156
- var ttl = (data.expiresIn || 3600) * 1000;
157
- sessionExpiresAt = Date.now() + ttl;
158
- scheduleRefresh(ttl);
159
- resolve({ sessionToken: sessionToken });
160
- return;
161
- }
277
+ xhr.onload = function () {
278
+ cleanupRequest();
162
279
 
163
- if (data.error === "USER_NOT_FOUND") {
164
- // Trial mode not a fatal error
165
- sessionToken = null;
166
- if (data.paymentLink) paymentLink = data.paymentLink;
167
- resolve({ trialMode: true });
168
- return;
169
- }
280
+ if (requestInitVersion !== activeInitVersion) {
281
+ reject({ code: "ABORTED", message: "Stale session response ignored" });
282
+ return;
283
+ }
170
284
 
171
- reject({
172
- code: data.error || "SESSION_ERROR",
173
- message: data.message || "Failed to create session (HTTP " + xhr.status + ")",
174
- });
175
- };
285
+ var data;
286
+ try {
287
+ data = JSON.parse(xhr.responseText);
288
+ } catch (e) {
289
+ reject({ code: "PARSE_ERROR", message: "Invalid response from session endpoint" });
290
+ return;
291
+ }
176
292
 
177
- xhr.onerror = function () {
178
- cleanupRequest();
179
- reject({ code: "NETWORK_ERROR", message: "Network error contacting session endpoint" });
180
- };
293
+ if (xhr.status === 200 && data.sessionToken) {
294
+ sessionToken = data.sessionToken;
295
+ var ttl = (data.expiresIn || 3600) * 1000;
296
+ sessionRefreshBufferMs = computeRefreshBuffer(ttl);
297
+ sessionExpiresAt = Date.now() + ttl;
298
+ refreshRetryDelayMs = 5000;
299
+ scheduleRefresh(ttl);
300
+ if (options.broadcastRefresh) broadcastSessionRefresh();
301
+ resolve({ sessionToken: sessionToken });
302
+ return;
303
+ }
181
304
 
182
- xhr.ontimeout = function () {
183
- cleanupRequest();
184
- reject({ code: "TIMEOUT", message: "Session request timed out after " + SESSION_TIMEOUT_MS + "ms" });
185
- };
305
+ if (data.error === "USER_NOT_FOUND") {
306
+ // Trial mode — not a fatal error
307
+ sessionToken = null;
308
+ sessionExpiresAt = 0;
309
+ if (refreshTimer) {
310
+ clearTimeout(refreshTimer);
311
+ refreshTimer = null;
312
+ }
313
+ if (data.paymentLink) paymentLink = data.paymentLink;
314
+ resolve({ trialMode: true });
315
+ return;
316
+ }
186
317
 
187
- xhr.onabort = function () {
188
- cleanupRequest();
189
- reject({ code: "ABORTED", message: "Session request was aborted" });
190
- };
318
+ reject({
319
+ code: data.error || "SESSION_ERROR",
320
+ message: data.message || "Failed to create session (HTTP " + xhr.status + ")",
321
+ });
322
+ };
323
+
324
+ xhr.onerror = function () {
325
+ cleanupRequest();
326
+ reject({ code: "NETWORK_ERROR", message: "Network error contacting session endpoint" });
327
+ };
328
+
329
+ xhr.ontimeout = function () {
330
+ cleanupRequest();
331
+ reject({ code: "TIMEOUT", message: "Session request timed out after " + SESSION_TIMEOUT_MS + "ms" });
332
+ };
191
333
 
192
- xhr.send(JSON.stringify(body));
334
+ xhr.onabort = function () {
335
+ cleanupRequest();
336
+ reject({ code: "ABORTED", message: "Session request was aborted" });
337
+ };
338
+
339
+ xhr.send(JSON.stringify(body));
340
+ });
341
+ });
342
+
343
+ requestPromise = requestPromise.catch(function (err) {
344
+ if (fetchingSession && fetchingSession.promise === requestPromise) {
345
+ fetchingSession = null;
346
+ }
347
+ throw err;
193
348
  });
194
349
 
195
350
  fetchingSession = {
@@ -201,7 +356,7 @@
201
356
  }
202
357
 
203
358
  function getSessionToken() {
204
- if (sessionToken && Date.now() < sessionExpiresAt - 30000) {
359
+ if (sessionToken && Date.now() < sessionExpiresAt - sessionRefreshBufferMs) {
205
360
  return Promise.resolve(sessionToken);
206
361
  }
207
362
  return fetchSession().then(function (result) {
@@ -211,11 +366,9 @@
211
366
 
212
367
  function scheduleRefresh(ttlMs) {
213
368
  if (refreshTimer) clearTimeout(refreshTimer);
214
- var delay = Math.max(ttlMs * 0.8, 5000);
369
+ var delay = computeRefreshDelay(ttlMs);
215
370
  refreshTimer = setTimeout(function () {
216
- fetchSession().catch(function () {
217
- // Silent — next open() will retry
218
- });
371
+ fetchSession({ broadcastRefresh: true }).catch(scheduleRefreshRetry);
219
372
  }, delay);
220
373
  }
221
374
 
@@ -274,8 +427,6 @@
274
427
  var msg = {
275
428
  type: "seamless-add-to-scenario-init",
276
429
  sessionToken: sessionToken,
277
- publishableKey: publishableKey,
278
- userId: userId,
279
430
  contacts: atsContacts,
280
431
  };
281
432
  if (paymentLink) msg.paymentLink = paymentLink;
@@ -460,18 +611,17 @@
460
611
 
461
612
  var SeamlessRoleplay = {
462
613
  /**
463
- * Initialize the SDK with a publishable key.
614
+ * Initialize the SDK with a host-provided user token callback.
464
615
  */
465
616
  init: function (opts) {
466
617
  try {
467
618
  var initVersion = activeInitVersion + 1;
468
- var hasUserToken = !!(opts && opts.userToken && String(opts.userToken).trim());
469
- var hasLegacyIdentity = !!(opts && opts.userId && opts.userEmail);
619
+ var hasUserTokenProvider = !!(opts && typeof opts.getUserToken === "function");
470
620
 
471
- if (!opts || !opts.publishableKey || (!hasUserToken && !hasLegacyIdentity)) {
621
+ if (!opts || !hasUserTokenProvider) {
472
622
  var error = {
473
623
  code: "INVALID_INIT",
474
- message: "requires { publishableKey, userToken } or legacy { publishableKey, userId, userEmail }",
624
+ message: "requires { getUserToken }",
475
625
  };
476
626
  logError("init", error.message);
477
627
  if (opts && typeof opts.onError === "function") {
@@ -489,13 +639,13 @@
489
639
  clearSessionRequest();
490
640
  sessionToken = null;
491
641
  sessionExpiresAt = 0;
642
+ sessionRefreshBufferMs = 30000;
643
+ refreshRetryDelayMs = 5000;
492
644
  paymentLink = null;
645
+ latestUserToken = null;
646
+ userTokenRequest = null;
493
647
 
494
- publishableKey = opts.publishableKey;
495
- userId = opts.userId || null;
496
- userEmail = opts.userEmail || null;
497
- userRole = opts.userRole || null;
498
- userToken = opts.userToken || null;
648
+ getUserToken = opts.getUserToken;
499
649
  appOrigin = opts.origin || null;
500
650
  initCallbacks.onReady = opts.onReady || null;
501
651
  initCallbacks.onError = opts.onError || null;
@@ -823,14 +973,14 @@
823
973
  clearSessionRequest();
824
974
  teardownDialog();
825
975
  teardownMount();
826
- publishableKey = null;
827
- userId = null;
828
- userEmail = null;
829
- userRole = null;
830
- userToken = null;
976
+ getUserToken = null;
977
+ latestUserToken = null;
978
+ userTokenRequest = null;
831
979
  paymentLink = null;
832
980
  sessionToken = null;
833
981
  sessionExpiresAt = 0;
982
+ sessionRefreshBufferMs = 30000;
983
+ refreshRetryDelayMs = 5000;
834
984
  initCallbacks = { onReady: null, onError: null };
835
985
  initCalled = false;
836
986
  } catch (e) {