@rehers/rehers-roleplay-sdk 2.5.1 → 2.5.3

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
@@ -6,26 +6,32 @@
6
6
  npm install @rehers/rehers-roleplay-sdk
7
7
  ```
8
8
 
9
+ For a working end-to-end example in this repo, use `sdk-demo/`.
10
+
9
11
  ---
10
12
 
11
13
  ## 1. Wrap your app with the Provider
12
14
 
13
15
  Add this once, above all your routes. It initializes the SDK for the logged-in Seamless user.
14
16
 
17
+ Production flow:
18
+
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
22
+
23
+ The browser should not mint sessions from raw `userId`, `userEmail`, or `userRole` in production.
24
+
15
25
  ```tsx
16
26
  import { SeamlessRoleplayProvider } from "@rehers/rehers-roleplay-sdk/react";
17
27
 
18
28
  function App() {
19
- // You already have the logged-in user from your auth/session.
20
- // The SDK needs their id and email.
21
- const me = useCurrentUser(); // however you get the logged-in user
29
+ const userToken = useRoleplayUserToken(); // fetched from your backend
22
30
 
23
31
  return (
24
32
  <SeamlessRoleplayProvider
25
33
  publishableKey="pk_live_..."
26
- userId={String(me.id)}
27
- userEmail={me.username}
28
- userRole={me.orgRole}
34
+ userToken={userToken}
29
35
  onReady={() => console.log("Roleplay SDK ready")}
30
36
  onError={(err) => console.error("Roleplay SDK error", err)}
31
37
  >
@@ -39,17 +45,15 @@ function App() {
39
45
  }
40
46
  ```
41
47
 
42
- If you fetch the user via API:
48
+ If you need a backend shape, the secure flow looks like this:
43
49
 
44
50
  ```ts
45
- const res = await fetch("https://api.seamless.ai/api/users/me", {
51
+ const tokenRes = await fetch("/api/roleplay/user-token", {
52
+ method: "POST",
46
53
  credentials: "include",
47
54
  }).then((r) => r.json());
48
55
 
49
- const me = res.data ?? res;
50
- // me.id → pass as userId (convert to string)
51
- // me.username → pass as userEmail
52
- // me.orgRole → pass as userRole directly
56
+ const userToken = tokenRes.userToken;
53
57
  ```
54
58
 
55
59
  That's the only setup. Everything below just works.
@@ -219,8 +223,7 @@ import "@rehers/rehers-roleplay-sdk";
219
223
  // Initialize once
220
224
  SeamlessRoleplay.init({
221
225
  publishableKey: "pk_live_...",
222
- userId: "...",
223
- userEmail: "...",
226
+ userToken: "...",
224
227
  onReady() { console.log("ready"); },
225
228
  });
226
229
 
package/index.d.ts CHANGED
@@ -1,13 +1,9 @@
1
- export interface SeamlessRoleplayInitOptions {
1
+ interface SeamlessRoleplayInitBase {
2
2
  /** Publishable API key (starts with pk_live_ or pk_test_) */
3
3
  publishableKey: string;
4
- /** Logged-in Seamless user ID. Pass String(me.id) from GET /api/users/me */
5
- userId: string;
6
- /** Logged-in Seamless user email. Pass me.username from GET /api/users/me */
7
- userEmail: string;
8
4
  /** Optional user role for syncing permissions ("owner" | "admin" | "member") */
9
5
  userRole?: "owner" | "admin" | "member";
10
- /** Optional signed JWT for identity verification */
6
+ /** Optional short-lived signed JWT for identity verification */
11
7
  userToken?: string;
12
8
  /** Override the app origin — where the iframe loads from (for dev/testing only) */
13
9
  origin?: string;
@@ -17,6 +13,23 @@ export interface SeamlessRoleplayInitOptions {
17
13
  onError?: (error: { code: string; message: string }) => void;
18
14
  }
19
15
 
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
+ });
32
+
20
33
  export interface SeamlessRoleplayOpenData {
21
34
  /** Full name of the contact */
22
35
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehers/rehers-roleplay-sdk",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "Seamless Roleplay SDK — embed roleplay call sessions via a modal + iframe",
5
5
  "main": "roleplay-sdk.js",
6
6
  "types": "index.d.ts",
@@ -32,6 +32,6 @@
32
32
  "license": "UNLICENSED",
33
33
  "repository": {
34
34
  "type": "git",
35
- "url": "https://github.com/rehers/seamless-frontend-independent"
35
+ "url": "git+https://github.com/rehers/seamless-frontend-independent.git"
36
36
  }
37
37
  }
package/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import type { SeamlessRoleplaySDK, AddToScenarioContact, AddToScenarioCompleteData } from "@rehers/rehers-roleplay-sdk";
2
+ import type { SeamlessRoleplaySDK, AddToScenarioContact, AddToScenarioCompleteData, SeamlessRoleplayInitOptions } from "@rehers/rehers-roleplay-sdk";
3
3
  import "@rehers/rehers-roleplay-sdk";
4
4
  export type { AddToScenarioContact, AddToScenarioCompleteData };
5
5
  interface SeamlessRoleplayContextValue {
@@ -10,20 +10,9 @@ interface SeamlessRoleplayContextValue {
10
10
  } | null;
11
11
  sdk: SeamlessRoleplaySDK;
12
12
  }
13
- export interface SeamlessRoleplayProviderProps {
14
- publishableKey: string;
15
- userId: string;
16
- userEmail: string;
17
- userRole?: "owner" | "admin" | "member";
18
- userToken?: string;
19
- origin?: string;
20
- onReady?: () => void;
21
- onError?: (error: {
22
- code: string;
23
- message: string;
24
- }) => void;
13
+ export type SeamlessRoleplayProviderProps = SeamlessRoleplayInitOptions & {
25
14
  children: ReactNode;
26
- }
15
+ };
27
16
  export declare function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, userRole, userToken, origin, onReady, onError, children, }: SeamlessRoleplayProviderProps): import("react/jsx-runtime").JSX.Element;
28
17
  export declare function useSeamlessRoleplay(): SeamlessRoleplayContextValue;
29
18
  export interface RoleplayDialogProps {
package/react.js CHANGED
@@ -36,28 +36,51 @@ export function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, us
36
36
  }
37
37
  mountedRef.current = true;
38
38
  setState({ isReady: false, error: null });
39
- sdk.init({
40
- publishableKey,
41
- userId,
42
- userEmail,
43
- userRole,
44
- userToken,
45
- origin,
46
- onReady: () => {
47
- var _a;
48
- if (!mountedRef.current)
49
- return;
50
- setState({ isReady: true, error: null });
51
- (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
52
- },
53
- onError: (err) => {
54
- var _a;
55
- if (!mountedRef.current)
56
- return;
57
- setState({ isReady: false, error: err });
58
- (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err);
59
- },
60
- });
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
+ };
83
+ sdk.init(initOptions);
61
84
  return () => {
62
85
  mountedRef.current = false;
63
86
  providerMountCount--;
package/roleplay-sdk.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Publishable-key auth model. No build step required.
5
5
  *
6
6
  * Usage:
7
- * SeamlessRoleplay.init({ publishableKey: 'pk_live_...', userId: 'user_789' });
7
+ * SeamlessRoleplay.init({ publishableKey: 'pk_live_...', userToken: 'jwt...' });
8
8
  * SeamlessRoleplay.open({ name: '...', domain: '...', company: '...', title: '...' });
9
9
  */
10
10
  (function () {
@@ -28,6 +28,8 @@
28
28
  var sessionExpiresAt = 0; // epoch ms
29
29
  var refreshTimer = null;
30
30
  var fetchingSession = null; // single-flight Promise
31
+ var activeSessionXhr = null;
32
+ var activeInitVersion = 0;
31
33
 
32
34
  var initCallbacks = { onReady: null, onError: null };
33
35
  var initCalled = false;
@@ -71,6 +73,16 @@
71
73
  return DEFAULT_API_ORIGIN;
72
74
  }
73
75
 
76
+ function buildIframeSrc(path) {
77
+ var targetPath = path || "/embed/roleplay-call";
78
+ var url = new URL(targetPath, getOrigin());
79
+ // Force the embedded app to drop any stale auth before it handles the
80
+ // SDK session-init message. Without this, hosted app sessions can leak
81
+ // through when switching users or entering trial mode in the demo.
82
+ url.searchParams.set("seamlessResetAuth", "1");
83
+ return url.toString();
84
+ }
85
+
74
86
  function sendMsg(iframeEl, msg) {
75
87
  try {
76
88
  if (iframeEl && iframeEl.contentWindow) {
@@ -83,25 +95,54 @@
83
95
 
84
96
  // ── Session management ────────────────────────────────────────────
85
97
 
98
+ function clearSessionRequest() {
99
+ if (activeSessionXhr) {
100
+ try {
101
+ activeSessionXhr.abort();
102
+ } catch (_) {}
103
+ activeSessionXhr = null;
104
+ }
105
+ fetchingSession = null;
106
+ }
107
+
86
108
  function fetchSession() {
87
- if (fetchingSession) return fetchingSession;
109
+ var requestInitVersion = activeInitVersion;
110
+
111
+ if (fetchingSession && fetchingSession.initVersion === requestInitVersion) {
112
+ return fetchingSession.promise;
113
+ }
88
114
 
89
- fetchingSession = new Promise(function (resolve, reject) {
115
+ var requestPromise = new Promise(function (resolve, reject) {
90
116
  var url = getApiOrigin() + "/api/sdk/session";
91
- var body = { userId: userId };
92
- if (userEmail) body.userEmail = userEmail;
93
- if (userRole) body.userRole = userRole;
94
- if (userToken) body.userToken = userToken;
117
+ var body = userToken
118
+ ? { userToken: userToken }
119
+ : { userId: userId, userEmail: userEmail, userRole: userRole };
95
120
 
96
121
  var xhr = new XMLHttpRequest();
122
+ activeSessionXhr = xhr;
97
123
  xhr.open("POST", url, true);
98
124
  xhr.setRequestHeader("Content-Type", "application/json");
99
125
  xhr.setRequestHeader("X-Publishable-Key", publishableKey);
100
126
  xhr.withCredentials = false;
101
127
  xhr.timeout = SESSION_TIMEOUT_MS;
102
128
 
129
+ function cleanupRequest() {
130
+ if (activeSessionXhr === xhr) {
131
+ activeSessionXhr = null;
132
+ }
133
+ if (fetchingSession && fetchingSession.promise === requestPromise) {
134
+ fetchingSession = null;
135
+ }
136
+ }
137
+
103
138
  xhr.onload = function () {
104
- fetchingSession = null;
139
+ cleanupRequest();
140
+
141
+ if (requestInitVersion !== activeInitVersion) {
142
+ reject({ code: "ABORTED", message: "Stale session response ignored" });
143
+ return;
144
+ }
145
+
105
146
  var data;
106
147
  try {
107
148
  data = JSON.parse(xhr.responseText);
@@ -134,24 +175,29 @@
134
175
  };
135
176
 
136
177
  xhr.onerror = function () {
137
- fetchingSession = null;
178
+ cleanupRequest();
138
179
  reject({ code: "NETWORK_ERROR", message: "Network error contacting session endpoint" });
139
180
  };
140
181
 
141
182
  xhr.ontimeout = function () {
142
- fetchingSession = null;
183
+ cleanupRequest();
143
184
  reject({ code: "TIMEOUT", message: "Session request timed out after " + SESSION_TIMEOUT_MS + "ms" });
144
185
  };
145
186
 
146
187
  xhr.onabort = function () {
147
- fetchingSession = null;
188
+ cleanupRequest();
148
189
  reject({ code: "ABORTED", message: "Session request was aborted" });
149
190
  };
150
191
 
151
192
  xhr.send(JSON.stringify(body));
152
193
  });
153
194
 
154
- return fetchingSession;
195
+ fetchingSession = {
196
+ initVersion: requestInitVersion,
197
+ promise: requestPromise,
198
+ };
199
+
200
+ return requestPromise;
155
201
  }
156
202
 
157
203
  function getSessionToken() {
@@ -381,7 +427,7 @@
381
427
 
382
428
  function createIframe(path) {
383
429
  var iframeEl = document.createElement("iframe");
384
- iframeEl.src = getOrigin() + (path || "/embed/roleplay-call");
430
+ iframeEl.src = buildIframeSrc(path);
385
431
  iframeEl.allow = "camera; microphone; display-capture; autoplay";
386
432
  iframeEl.style.width = "100%";
387
433
  iframeEl.style.height = "100%";
@@ -398,8 +444,19 @@
398
444
  */
399
445
  init: function (opts) {
400
446
  try {
401
- if (!opts || !opts.publishableKey || !opts.userId || !opts.userEmail) {
402
- logError("init", "requires { publishableKey, userId, userEmail }");
447
+ var initVersion = activeInitVersion + 1;
448
+ var hasUserToken = !!(opts && opts.userToken && String(opts.userToken).trim());
449
+ var hasLegacyIdentity = !!(opts && opts.userId && opts.userEmail);
450
+
451
+ if (!opts || !opts.publishableKey || (!hasUserToken && !hasLegacyIdentity)) {
452
+ var error = {
453
+ code: "INVALID_INIT",
454
+ message: "requires { publishableKey, userToken } or legacy { publishableKey, userId, userEmail }",
455
+ };
456
+ logError("init", error.message);
457
+ if (opts && typeof opts.onError === "function") {
458
+ try { opts.onError(error); } catch (_) {}
459
+ }
403
460
  return;
404
461
  }
405
462
 
@@ -408,12 +465,14 @@
408
465
  clearTimeout(refreshTimer);
409
466
  refreshTimer = null;
410
467
  }
411
- fetchingSession = null;
468
+ activeInitVersion = initVersion;
469
+ clearSessionRequest();
412
470
  sessionToken = null;
413
471
  sessionExpiresAt = 0;
472
+ paymentLink = null;
414
473
 
415
474
  publishableKey = opts.publishableKey;
416
- userId = opts.userId;
475
+ userId = opts.userId || null;
417
476
  userEmail = opts.userEmail || null;
418
477
  userRole = opts.userRole || null;
419
478
  userToken = opts.userToken || null;
@@ -425,6 +484,7 @@
425
484
  // Fetch session immediately
426
485
  fetchSession()
427
486
  .then(function (result) {
487
+ if (initVersion !== activeInitVersion) return;
428
488
  if (result.trialMode && !paymentLink) {
429
489
  // User not found and no payment link from server — still call onReady
430
490
  // The open() will show an error state in the iframe
@@ -432,6 +492,8 @@
432
492
  if (initCallbacks.onReady) initCallbacks.onReady();
433
493
  })
434
494
  .catch(function (err) {
495
+ if (initVersion !== activeInitVersion) return;
496
+ if (err && err.code === "ABORTED") return;
435
497
  if (initCallbacks.onError) {
436
498
  initCallbacks.onError({ code: err.code || "INIT_ERROR", message: err.message || "Initialization failed" });
437
499
  }
@@ -650,7 +712,7 @@
650
712
  cs.boxShadow = "0 25px 60px rgba(0,0,0,0.3)";
651
713
 
652
714
  var iframeEl = document.createElement("iframe");
653
- iframeEl.src = getOrigin() + "/embed/add-to-scenario";
715
+ iframeEl.src = buildIframeSrc("/embed/add-to-scenario");
654
716
  iframeEl.style.width = "100%";
655
717
  iframeEl.style.height = "100%";
656
718
  iframeEl.style.border = "none";
@@ -733,10 +795,12 @@
733
795
  */
734
796
  destroy: function () {
735
797
  try {
798
+ activeInitVersion++;
736
799
  if (refreshTimer) {
737
800
  clearTimeout(refreshTimer);
738
801
  refreshTimer = null;
739
802
  }
803
+ clearSessionRequest();
740
804
  teardownDialog();
741
805
  teardownMount();
742
806
  publishableKey = null;
@@ -747,7 +811,6 @@
747
811
  paymentLink = null;
748
812
  sessionToken = null;
749
813
  sessionExpiresAt = 0;
750
- fetchingSession = null;
751
814
  initCallbacks = { onReady: null, onError: null };
752
815
  initCalled = false;
753
816
  } catch (e) {