@rehers/rehers-roleplay-sdk 2.5.7 → 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,8 +17,8 @@ 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 `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
23
  The browser should not mint sessions from raw identity fields in production.
24
24
 
@@ -26,11 +26,18 @@ The browser should not mint sessions from raw identity fields in production.
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
- userToken={userToken}
40
+ getUserToken={getUserToken}
34
41
  onReady={() => console.log("Roleplay SDK ready")}
35
42
  onError={(err) => console.error("Roleplay SDK error", err)}
36
43
  >
@@ -55,7 +62,9 @@ const tokenRes = await fetch("/api/roleplay/user-token", {
55
62
  const userToken = tokenRes.userToken;
56
63
  ```
57
64
 
58
- 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.
59
68
 
60
69
  ---
61
70
 
@@ -221,7 +230,14 @@ import "@rehers/rehers-roleplay-sdk";
221
230
 
222
231
  // Initialize once
223
232
  SeamlessRoleplay.init({
224
- 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
+ },
225
241
  onReady() { console.log("ready"); },
226
242
  });
227
243
 
package/index.d.ts CHANGED
@@ -1,6 +1,12 @@
1
+ export type SeamlessRoleplayUserTokenProvider = () => string | Promise<string>;
2
+
1
3
  interface SeamlessRoleplayInitBase {
2
- /** Short-lived signed JWT minted by your backend for the signed-in user */
3
- 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;
4
10
  /** Override the app origin — where the iframe loads from (for dev/testing only) */
5
11
  origin?: string;
6
12
  /** Called when the SDK session is ready */
@@ -64,7 +70,7 @@ export interface AddToScenarioOptions {
64
70
  }
65
71
 
66
72
  export interface SeamlessRoleplaySDK {
67
- /** Initialize the SDK with a short-lived user token. */
73
+ /** Initialize the SDK with a host-provided user token callback. */
68
74
  init(options: SeamlessRoleplayInitOptions): void;
69
75
  /** Open the roleplay modal for a contact (dialog mode). */
70
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.7",
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({ 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({ 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);
@@ -37,7 +37,7 @@ export function SeamlessRoleplayProvider({ userToken, origin, onReady, onError,
37
37
  mountedRef.current = true;
38
38
  setState({ isReady: false, error: null });
39
39
  const initOptions = {
40
- userToken,
40
+ getUserToken,
41
41
  origin,
42
42
  onReady: () => {
43
43
  var _a;
@@ -60,7 +60,7 @@ export function SeamlessRoleplayProvider({ userToken, origin, onReady, onError,
60
60
  providerMountCount--;
61
61
  sdk.destroy();
62
62
  };
63
- }, [sdk, userToken, origin]);
63
+ }, [sdk, getUserToken, origin]);
64
64
  const contextValue = useMemo(() => ({ ...state, sdk }), [state, sdk]);
65
65
  return (_jsx(SeamlessRoleplayContext.Provider, { value: contextValue, children: children }));
66
66
  }
package/roleplay-sdk.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * SeamlessRoleplay SDK v2
2
+ * SeamlessRoleplay SDK v3
3
3
  *
4
4
  * User-token auth model. No build step required.
5
5
  *
6
6
  * Usage:
7
- * SeamlessRoleplay.init({ userToken: 'jwt...' });
7
+ * SeamlessRoleplay.init({ getUserToken: async () => 'jwt...' });
8
8
  * SeamlessRoleplay.open({ name: '...', domain: '...', company: '...', title: '...' });
9
9
  */
10
10
  (function () {
@@ -16,13 +16,17 @@
16
16
  var SDK_LOG_PREFIX = "[SeamlessRoleplay]";
17
17
 
18
18
  // ── Auth state ────────────────────────────────────────────────────
19
- var userToken = null;
19
+ var getUserToken = null;
20
+ var latestUserToken = null;
21
+ var userTokenRequest = null;
20
22
  var paymentLink = null;
21
23
  var appOrigin = null;
22
24
 
23
25
  var sessionToken = null;
24
26
  var sessionExpiresAt = 0; // epoch ms
27
+ var sessionRefreshBufferMs = 30000;
25
28
  var refreshTimer = null;
29
+ var refreshRetryDelayMs = 5000;
26
30
  var fetchingSession = null; // single-flight Promise
27
31
  var activeSessionXhr = null;
28
32
  var activeInitVersion = 0;
@@ -89,6 +93,16 @@
89
93
  }
90
94
  }
91
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
+
92
106
  // ── Session management ────────────────────────────────────────────
93
107
 
94
108
  function clearSessionRequest() {
@@ -101,88 +115,236 @@
101
115
  fetchingSession = null;
102
116
  }
103
117
 
104
- 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 || {};
105
234
  var requestInitVersion = activeInitVersion;
106
235
 
107
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
+ }
108
249
  return fetchingSession.promise;
109
250
  }
110
251
 
111
- var requestPromise = new Promise(function (resolve, reject) {
112
- var url = getApiOrigin() + "/api/sdk/session";
113
- var body = { userToken: userToken };
252
+ var requestPromise = resolveUserToken().then(function (freshUserToken) {
253
+ if (requestInitVersion !== activeInitVersion) {
254
+ throw makeError("ABORTED", "Stale session request ignored");
255
+ }
114
256
 
115
- var xhr = new XMLHttpRequest();
116
- activeSessionXhr = xhr;
117
- xhr.open("POST", url, true);
118
- xhr.setRequestHeader("Content-Type", "application/json");
119
- xhr.withCredentials = false;
120
- xhr.timeout = SESSION_TIMEOUT_MS;
257
+ return new Promise(function (resolve, reject) {
258
+ var url = getApiOrigin() + "/api/sdk/session";
259
+ var body = { userToken: freshUserToken };
121
260
 
122
- function cleanupRequest() {
123
- if (activeSessionXhr === xhr) {
124
- activeSessionXhr = null;
125
- }
126
- if (fetchingSession && fetchingSession.promise === requestPromise) {
127
- fetchingSession = null;
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;
267
+
268
+ function cleanupRequest() {
269
+ if (activeSessionXhr === xhr) {
270
+ activeSessionXhr = null;
271
+ }
272
+ if (fetchingSession && fetchingSession.promise === requestPromise) {
273
+ fetchingSession = null;
274
+ }
128
275
  }
129
- }
130
276
 
131
- xhr.onload = function () {
132
- cleanupRequest();
277
+ xhr.onload = function () {
278
+ cleanupRequest();
133
279
 
134
- if (requestInitVersion !== activeInitVersion) {
135
- reject({ code: "ABORTED", message: "Stale session response ignored" });
136
- return;
137
- }
280
+ if (requestInitVersion !== activeInitVersion) {
281
+ reject({ code: "ABORTED", message: "Stale session response ignored" });
282
+ return;
283
+ }
138
284
 
139
- var data;
140
- try {
141
- data = JSON.parse(xhr.responseText);
142
- } catch (e) {
143
- reject({ code: "PARSE_ERROR", message: "Invalid response from session endpoint" });
144
- return;
145
- }
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
+ }
146
292
 
147
- if (xhr.status === 200 && data.sessionToken) {
148
- sessionToken = data.sessionToken;
149
- var ttl = (data.expiresIn || 3600) * 1000;
150
- sessionExpiresAt = Date.now() + ttl;
151
- scheduleRefresh(ttl);
152
- resolve({ sessionToken: sessionToken });
153
- return;
154
- }
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
+ }
155
304
 
156
- if (data.error === "USER_NOT_FOUND") {
157
- // Trial mode — not a fatal error
158
- sessionToken = null;
159
- if (data.paymentLink) paymentLink = data.paymentLink;
160
- resolve({ trialMode: true });
161
- return;
162
- }
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
+ }
163
317
 
164
- reject({
165
- code: data.error || "SESSION_ERROR",
166
- message: data.message || "Failed to create session (HTTP " + xhr.status + ")",
167
- });
168
- };
318
+ reject({
319
+ code: data.error || "SESSION_ERROR",
320
+ message: data.message || "Failed to create session (HTTP " + xhr.status + ")",
321
+ });
322
+ };
169
323
 
170
- xhr.onerror = function () {
171
- cleanupRequest();
172
- reject({ code: "NETWORK_ERROR", message: "Network error contacting session endpoint" });
173
- };
324
+ xhr.onerror = function () {
325
+ cleanupRequest();
326
+ reject({ code: "NETWORK_ERROR", message: "Network error contacting session endpoint" });
327
+ };
174
328
 
175
- xhr.ontimeout = function () {
176
- cleanupRequest();
177
- reject({ code: "TIMEOUT", message: "Session request timed out after " + SESSION_TIMEOUT_MS + "ms" });
178
- };
329
+ xhr.ontimeout = function () {
330
+ cleanupRequest();
331
+ reject({ code: "TIMEOUT", message: "Session request timed out after " + SESSION_TIMEOUT_MS + "ms" });
332
+ };
179
333
 
180
- xhr.onabort = function () {
181
- cleanupRequest();
182
- reject({ code: "ABORTED", message: "Session request was aborted" });
183
- };
334
+ xhr.onabort = function () {
335
+ cleanupRequest();
336
+ reject({ code: "ABORTED", message: "Session request was aborted" });
337
+ };
184
338
 
185
- xhr.send(JSON.stringify(body));
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;
186
348
  });
187
349
 
188
350
  fetchingSession = {
@@ -194,7 +356,7 @@
194
356
  }
195
357
 
196
358
  function getSessionToken() {
197
- if (sessionToken && Date.now() < sessionExpiresAt - 30000) {
359
+ if (sessionToken && Date.now() < sessionExpiresAt - sessionRefreshBufferMs) {
198
360
  return Promise.resolve(sessionToken);
199
361
  }
200
362
  return fetchSession().then(function (result) {
@@ -204,11 +366,9 @@
204
366
 
205
367
  function scheduleRefresh(ttlMs) {
206
368
  if (refreshTimer) clearTimeout(refreshTimer);
207
- var delay = Math.max(ttlMs * 0.8, 5000);
369
+ var delay = computeRefreshDelay(ttlMs);
208
370
  refreshTimer = setTimeout(function () {
209
- fetchSession().catch(function () {
210
- // Silent — next open() will retry
211
- });
371
+ fetchSession({ broadcastRefresh: true }).catch(scheduleRefreshRetry);
212
372
  }, delay);
213
373
  }
214
374
 
@@ -451,17 +611,17 @@
451
611
 
452
612
  var SeamlessRoleplay = {
453
613
  /**
454
- * Initialize the SDK with a short-lived user token.
614
+ * Initialize the SDK with a host-provided user token callback.
455
615
  */
456
616
  init: function (opts) {
457
617
  try {
458
618
  var initVersion = activeInitVersion + 1;
459
- var hasUserToken = !!(opts && opts.userToken && String(opts.userToken).trim());
619
+ var hasUserTokenProvider = !!(opts && typeof opts.getUserToken === "function");
460
620
 
461
- if (!opts || !hasUserToken) {
621
+ if (!opts || !hasUserTokenProvider) {
462
622
  var error = {
463
623
  code: "INVALID_INIT",
464
- message: "requires { userToken }",
624
+ message: "requires { getUserToken }",
465
625
  };
466
626
  logError("init", error.message);
467
627
  if (opts && typeof opts.onError === "function") {
@@ -479,9 +639,13 @@
479
639
  clearSessionRequest();
480
640
  sessionToken = null;
481
641
  sessionExpiresAt = 0;
642
+ sessionRefreshBufferMs = 30000;
643
+ refreshRetryDelayMs = 5000;
482
644
  paymentLink = null;
645
+ latestUserToken = null;
646
+ userTokenRequest = null;
483
647
 
484
- userToken = opts.userToken || null;
648
+ getUserToken = opts.getUserToken;
485
649
  appOrigin = opts.origin || null;
486
650
  initCallbacks.onReady = opts.onReady || null;
487
651
  initCallbacks.onError = opts.onError || null;
@@ -809,10 +973,14 @@
809
973
  clearSessionRequest();
810
974
  teardownDialog();
811
975
  teardownMount();
812
- userToken = null;
976
+ getUserToken = null;
977
+ latestUserToken = null;
978
+ userTokenRequest = null;
813
979
  paymentLink = null;
814
980
  sessionToken = null;
815
981
  sessionExpiresAt = 0;
982
+ sessionRefreshBufferMs = 30000;
983
+ refreshRetryDelayMs = 5000;
816
984
  initCallbacks = { onReady: null, onError: null };
817
985
  initCalled = false;
818
986
  } catch (e) {