@genex-ai/embed-sdk 0.2.0 → 0.3.1
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/dist/{chunk-KXUDNW6P.js → chunk-S7JLPCNE.js} +189 -26
- package/dist/index.d.ts +43 -19
- package/dist/index.js +5 -3
- package/dist/sentry.js +5 -1
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
var PROTOCOL_VERSION = 1;
|
|
3
3
|
var RETRY_FLAG = "genex:embed:retry";
|
|
4
|
+
var POPOVER_DISMISSED_FLAG = "genex:guest:popover-dismissed";
|
|
4
5
|
var config = null;
|
|
5
6
|
var state = "pending";
|
|
6
7
|
var user = null;
|
|
@@ -9,6 +10,7 @@ var colyseusUrl;
|
|
|
9
10
|
var parentOrigin = null;
|
|
10
11
|
var initialized = false;
|
|
11
12
|
var redeeming = false;
|
|
13
|
+
var requestingGuest = false;
|
|
12
14
|
var handshakeTimeoutMs = 1e4;
|
|
13
15
|
var refreshDelayMs = 10 * 6e4;
|
|
14
16
|
var refreshRetryMs = 6e4;
|
|
@@ -17,7 +19,9 @@ var refreshTimer;
|
|
|
17
19
|
var messageHandler;
|
|
18
20
|
var listeners = /* @__PURE__ */ new Map();
|
|
19
21
|
var authWaiters = [];
|
|
22
|
+
var playerWaiters = [];
|
|
20
23
|
var overlayEl = null;
|
|
24
|
+
var popoverEl = null;
|
|
21
25
|
function win() {
|
|
22
26
|
return globalThis.window;
|
|
23
27
|
}
|
|
@@ -46,6 +50,19 @@ function clearRetryFlag() {
|
|
|
46
50
|
} catch {
|
|
47
51
|
}
|
|
48
52
|
}
|
|
53
|
+
function readPopoverDismissed() {
|
|
54
|
+
try {
|
|
55
|
+
return win()?.sessionStorage?.getItem(POPOVER_DISMISSED_FLAG) === "1";
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function writePopoverDismissed() {
|
|
61
|
+
try {
|
|
62
|
+
win()?.sessionStorage?.setItem(POPOVER_DISMISSED_FLAG, "1");
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
49
66
|
function initEmbed(cfg) {
|
|
50
67
|
const w = win();
|
|
51
68
|
if (!w) return;
|
|
@@ -92,6 +109,15 @@ function waitForAuth() {
|
|
|
92
109
|
authWaiters.push({ resolve, reject });
|
|
93
110
|
});
|
|
94
111
|
}
|
|
112
|
+
function waitForPlayer() {
|
|
113
|
+
if ((state === "authenticated" || state === "guest") && user) {
|
|
114
|
+
return Promise.resolve({ user, guest: state === "guest" });
|
|
115
|
+
}
|
|
116
|
+
if (state === "blocked") return Promise.reject(new Error("genex embed auth: blocked"));
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
playerWaiters.push({ resolve, reject });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
95
121
|
function on(event, cb) {
|
|
96
122
|
let set = listeners.get(event);
|
|
97
123
|
if (!set) {
|
|
@@ -120,21 +146,33 @@ function startEmbeddedHandshake(w) {
|
|
|
120
146
|
w.addEventListener("message", messageHandler);
|
|
121
147
|
w.parent.postMessage({ type: "genex:embed:ready", v: PROTOCOL_VERSION }, "*");
|
|
122
148
|
handshakeTimer = setTimeout(() => {
|
|
123
|
-
|
|
149
|
+
void requestGuestSession();
|
|
124
150
|
}, handshakeTimeoutMs);
|
|
125
151
|
}
|
|
152
|
+
function acceptsTicket() {
|
|
153
|
+
return (state === "pending" || state === "guest") && !redeeming;
|
|
154
|
+
}
|
|
126
155
|
async function handleParentMessage(event) {
|
|
127
|
-
if (!config
|
|
156
|
+
if (!config) return;
|
|
128
157
|
const data = event.data;
|
|
129
|
-
if (!data || data.
|
|
130
|
-
|
|
158
|
+
if (!data || data.v !== PROTOCOL_VERSION) return;
|
|
159
|
+
const isTicket = data.type === "genex:embed:ticket";
|
|
160
|
+
const isGuest = data.type === "genex:embed:guest";
|
|
161
|
+
if (!isTicket && !isGuest) return;
|
|
162
|
+
if (isTicket && (!acceptsTicket() || typeof data.ticket !== "string" || !data.ticket)) return;
|
|
163
|
+
if (isGuest && (state !== "pending" || redeeming || requestingGuest)) return;
|
|
131
164
|
if (!config.dashboardOrigins.includes(event.origin)) {
|
|
132
165
|
const fresh = await fetchDashboardOrigins();
|
|
133
|
-
if (
|
|
166
|
+
if (isTicket && !acceptsTicket()) return;
|
|
167
|
+
if (isGuest && (state !== "pending" || redeeming || requestingGuest)) return;
|
|
134
168
|
if (!fresh.includes(event.origin)) return;
|
|
135
169
|
}
|
|
136
170
|
if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
|
|
137
171
|
parentOrigin = event.origin;
|
|
172
|
+
if (isGuest) {
|
|
173
|
+
await requestGuestSession();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
138
176
|
await redeemTicket(data.ticket);
|
|
139
177
|
}
|
|
140
178
|
function postToParent(message, targetOrigin) {
|
|
@@ -146,41 +184,52 @@ function postToParent(message, targetOrigin) {
|
|
|
146
184
|
}
|
|
147
185
|
}
|
|
148
186
|
async function startStandaloneFlow(w) {
|
|
149
|
-
const
|
|
150
|
-
if (ticket) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await
|
|
187
|
+
const frag = readFragment(w);
|
|
188
|
+
if (frag.ticket) {
|
|
189
|
+
await redeemTicket(frag.ticket);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (frag.guest) {
|
|
193
|
+
await requestGuestSession();
|
|
156
194
|
return;
|
|
157
195
|
}
|
|
158
196
|
showOverlay("redirecting");
|
|
159
|
-
await redirectToAuthorize(w);
|
|
197
|
+
await redirectToAuthorize(w, { guestOk: true });
|
|
160
198
|
}
|
|
161
199
|
var stashedTicketHash = null;
|
|
162
200
|
function _stashTicketFromUrl() {
|
|
163
201
|
const w = win();
|
|
164
202
|
if (!w) return;
|
|
165
203
|
const hash = w.location.hash;
|
|
166
|
-
if (!hash || !hash.includes("
|
|
204
|
+
if (!hash || !hash.includes("genex_")) return;
|
|
167
205
|
stashedTicketHash = hash;
|
|
168
206
|
try {
|
|
169
207
|
w.history.replaceState(null, "", w.location.pathname + w.location.search);
|
|
170
208
|
} catch {
|
|
171
209
|
}
|
|
172
210
|
}
|
|
173
|
-
function
|
|
211
|
+
function readFragment(w) {
|
|
174
212
|
const hash = stashedTicketHash ?? w.location.hash;
|
|
175
213
|
stashedTicketHash = null;
|
|
176
|
-
if (!hash || hash.length < 2) return null;
|
|
214
|
+
if (!hash || hash.length < 2) return { ticket: null, guest: false };
|
|
215
|
+
let ticket = null;
|
|
216
|
+
let guest = false;
|
|
177
217
|
try {
|
|
178
|
-
|
|
218
|
+
const params = new URLSearchParams(hash.slice(1));
|
|
219
|
+
ticket = params.get("genex_ticket");
|
|
220
|
+
guest = params.get("genex_guest") === "1";
|
|
179
221
|
} catch {
|
|
180
|
-
return null;
|
|
222
|
+
return { ticket: null, guest: false };
|
|
223
|
+
}
|
|
224
|
+
if (ticket || guest) {
|
|
225
|
+
try {
|
|
226
|
+
w.history.replaceState(null, "", w.location.pathname + w.location.search);
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
181
229
|
}
|
|
230
|
+
return { ticket, guest };
|
|
182
231
|
}
|
|
183
|
-
async function redirectToAuthorize(w) {
|
|
232
|
+
async function redirectToAuthorize(w, opts) {
|
|
184
233
|
if (!config) return;
|
|
185
234
|
const origins = await fetchDashboardOrigins();
|
|
186
235
|
const origin = origins[0] ?? config.dashboardOrigins[0];
|
|
@@ -189,7 +238,8 @@ async function redirectToAuthorize(w) {
|
|
|
189
238
|
return;
|
|
190
239
|
}
|
|
191
240
|
const returnTo = w.location.origin + w.location.pathname;
|
|
192
|
-
const
|
|
241
|
+
const guestParam = opts?.guestOk ? "&guest=ok" : "";
|
|
242
|
+
const url = `${origin}/play/authorize?returnTo=${encodeURIComponent(returnTo)}&slug=${encodeURIComponent(config.slug)}${guestParam}`;
|
|
193
243
|
w.location.replace(url);
|
|
194
244
|
}
|
|
195
245
|
async function fetchDashboardOrigins() {
|
|
@@ -206,7 +256,8 @@ async function fetchDashboardOrigins() {
|
|
|
206
256
|
return config.dashboardOrigins;
|
|
207
257
|
}
|
|
208
258
|
async function redeemTicket(ticket) {
|
|
209
|
-
if (!config || redeeming || state !== "pending") return;
|
|
259
|
+
if (!config || redeeming || state !== "pending" && state !== "guest") return;
|
|
260
|
+
const upgradingFromGuest = state === "guest";
|
|
210
261
|
redeeming = true;
|
|
211
262
|
try {
|
|
212
263
|
const res = await doFetch(`${config.apiUrl}/api/embed/session`, {
|
|
@@ -216,7 +267,7 @@ async function redeemTicket(ticket) {
|
|
|
216
267
|
});
|
|
217
268
|
if (!res.ok) {
|
|
218
269
|
emit("error", { error: new Error(`ticket redemption failed (${res.status})`) });
|
|
219
|
-
await handleRedeemFailure();
|
|
270
|
+
if (!upgradingFromGuest) await handleRedeemFailure();
|
|
220
271
|
return;
|
|
221
272
|
}
|
|
222
273
|
const body = await res.json();
|
|
@@ -227,26 +278,75 @@ async function redeemTicket(ticket) {
|
|
|
227
278
|
state = "authenticated";
|
|
228
279
|
clearRetryFlag();
|
|
229
280
|
removeOverlay();
|
|
281
|
+
removeGuestPopover();
|
|
230
282
|
scheduleRefresh();
|
|
231
283
|
if (isEmbedded() && parentOrigin) {
|
|
232
284
|
postToParent({ type: "genex:embed:authenticated", v: PROTOCOL_VERSION }, parentOrigin);
|
|
233
285
|
}
|
|
234
286
|
const ctx = { user };
|
|
235
287
|
for (const waiter of authWaiters.splice(0)) waiter.resolve(ctx);
|
|
288
|
+
for (const waiter of playerWaiters.splice(0)) waiter.resolve({ user, guest: false });
|
|
236
289
|
emit("authenticated", ctx);
|
|
237
290
|
} catch (error) {
|
|
238
291
|
emit("error", { error });
|
|
239
|
-
await handleRedeemFailure();
|
|
292
|
+
if (!upgradingFromGuest) await handleRedeemFailure();
|
|
240
293
|
} finally {
|
|
241
294
|
redeeming = false;
|
|
242
295
|
}
|
|
243
296
|
}
|
|
297
|
+
async function requestGuestSession() {
|
|
298
|
+
if (!config || requestingGuest || redeeming) return;
|
|
299
|
+
if (state !== "pending" && state !== "guest") return;
|
|
300
|
+
requestingGuest = true;
|
|
301
|
+
try {
|
|
302
|
+
const res = await doFetch(`${config.apiUrl}/api/embed/guest-session`, {
|
|
303
|
+
method: "POST",
|
|
304
|
+
headers: { "Content-Type": "application/json" },
|
|
305
|
+
body: JSON.stringify({ slug: config.slug })
|
|
306
|
+
});
|
|
307
|
+
if (!res.ok) {
|
|
308
|
+
emit("error", { error: new Error(`guest session failed (${res.status})`) });
|
|
309
|
+
enterBlocked();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const body = await res.json();
|
|
313
|
+
embedToken = body.embedToken;
|
|
314
|
+
user = body.user;
|
|
315
|
+
colyseusUrl = body.colyseus?.url;
|
|
316
|
+
enterGuest();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
emit("error", { error });
|
|
319
|
+
enterBlocked();
|
|
320
|
+
} finally {
|
|
321
|
+
requestingGuest = false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function enterGuest() {
|
|
325
|
+
const firstEntry = state !== "guest";
|
|
326
|
+
state = "guest";
|
|
327
|
+
if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
|
|
328
|
+
removeOverlay();
|
|
329
|
+
scheduleRefresh();
|
|
330
|
+
if (isEmbedded()) {
|
|
331
|
+
postToParent(
|
|
332
|
+
{ type: "genex:embed:authenticated", v: PROTOCOL_VERSION, guest: true },
|
|
333
|
+
parentOrigin ?? "*"
|
|
334
|
+
);
|
|
335
|
+
} else if (firstEntry && !readPopoverDismissed()) {
|
|
336
|
+
showGuestPopover();
|
|
337
|
+
}
|
|
338
|
+
if (user) {
|
|
339
|
+
const ctx = { user };
|
|
340
|
+
for (const waiter of playerWaiters.splice(0)) waiter.resolve({ user, guest: true });
|
|
341
|
+
emit("guest", ctx);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
244
344
|
async function handleRedeemFailure() {
|
|
245
345
|
const w = win();
|
|
246
346
|
if (w && !isEmbedded() && !readRetryFlag()) {
|
|
247
347
|
writeRetryFlag();
|
|
248
348
|
showOverlay("redirecting");
|
|
249
|
-
await redirectToAuthorize(w);
|
|
349
|
+
await redirectToAuthorize(w, { guestOk: true });
|
|
250
350
|
return;
|
|
251
351
|
}
|
|
252
352
|
enterBlocked();
|
|
@@ -258,7 +358,7 @@ function scheduleRefresh() {
|
|
|
258
358
|
}, refreshDelayMs);
|
|
259
359
|
}
|
|
260
360
|
async function refreshToken() {
|
|
261
|
-
if (!config || state !== "authenticated" || !embedToken) return;
|
|
361
|
+
if (!config || state !== "authenticated" && state !== "guest" || !embedToken) return;
|
|
262
362
|
let status;
|
|
263
363
|
try {
|
|
264
364
|
const res = await doFetch(`${config.apiUrl}/api/embed/session/refresh`, {
|
|
@@ -279,6 +379,10 @@ async function refreshToken() {
|
|
|
279
379
|
status = void 0;
|
|
280
380
|
}
|
|
281
381
|
if (status === 401) {
|
|
382
|
+
if (state === "guest") {
|
|
383
|
+
void requestGuestSession();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
282
386
|
emit("error", { error: new Error("embed session expired") });
|
|
283
387
|
enterBlocked();
|
|
284
388
|
return;
|
|
@@ -295,17 +399,21 @@ function enterBlocked() {
|
|
|
295
399
|
embedToken = void 0;
|
|
296
400
|
if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
|
|
297
401
|
if (refreshTimer !== void 0) clearTimeout(refreshTimer);
|
|
402
|
+
removeGuestPopover();
|
|
298
403
|
showOverlay(isEmbedded() ? "blocked-embedded" : "blocked-standalone");
|
|
299
404
|
if (isEmbedded()) {
|
|
300
405
|
postToParent({ type: "genex:embed:blocked", v: PROTOCOL_VERSION }, parentOrigin ?? "*");
|
|
301
406
|
}
|
|
302
407
|
const err = new Error("genex embed auth: blocked");
|
|
303
408
|
for (const waiter of authWaiters.splice(0)) waiter.reject(err);
|
|
409
|
+
for (const waiter of playerWaiters.splice(0)) waiter.reject(err);
|
|
304
410
|
emit("blocked");
|
|
305
411
|
}
|
|
306
412
|
var OVERLAY_TEXT = {
|
|
307
413
|
connecting: { title: "Connecting to Genex\u2026" },
|
|
308
|
-
|
|
414
|
+
// Neutral on purpose: the automatic bounce precedes GUEST play as often as
|
|
415
|
+
// sign-in — "signing you in" would be wrong for most visitors.
|
|
416
|
+
redirecting: { title: "Loading\u2026" },
|
|
309
417
|
"blocked-standalone": { title: "Sign in to play", button: "Sign in" },
|
|
310
418
|
"blocked-embedded": { title: "Sign in on the Genex dashboard to continue" }
|
|
311
419
|
};
|
|
@@ -359,6 +467,56 @@ function buildOverlay(d) {
|
|
|
359
467
|
}
|
|
360
468
|
};
|
|
361
469
|
}
|
|
470
|
+
function showGuestPopover() {
|
|
471
|
+
const d = doc();
|
|
472
|
+
if (!d?.createElement || popoverEl) return;
|
|
473
|
+
try {
|
|
474
|
+
popoverEl = buildGuestPopover(d);
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function removeGuestPopover() {
|
|
479
|
+
try {
|
|
480
|
+
const root = popoverEl?.root;
|
|
481
|
+
root?.remove?.();
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
popoverEl = null;
|
|
485
|
+
}
|
|
486
|
+
function buildGuestPopover(d) {
|
|
487
|
+
const root = d.createElement("div");
|
|
488
|
+
root.setAttribute("data-genex-guest-popover", "");
|
|
489
|
+
root.style.cssText = "position:fixed;top:12px;right:12px;z-index:2147483646;display:flex;align-items:center;gap:12px;padding:10px 12px 10px 16px;border-radius:12px;background:rgba(8,10,20,0.85);color:#fff;font-family:system-ui,-apple-system,sans-serif;font-size:13px;box-shadow:0 4px 24px rgba(0,0,0,0.4);pointer-events:auto;user-select:none";
|
|
490
|
+
const text = d.createElement("span");
|
|
491
|
+
text.textContent = user?.name ? `Playing as ${user.name} \u2014 sign in to save progress` : "Sign in to save progress";
|
|
492
|
+
root.appendChild(text);
|
|
493
|
+
const signIn = d.createElement("button");
|
|
494
|
+
signIn.textContent = "Sign in";
|
|
495
|
+
signIn.style.cssText = "font-size:13px;font-weight:600;padding:6px 16px;border-radius:999px;border:none;cursor:pointer;background:#fff;color:#111";
|
|
496
|
+
signIn.addEventListener("click", () => {
|
|
497
|
+
const w = win();
|
|
498
|
+
if (!w) return;
|
|
499
|
+
signIn.disabled = true;
|
|
500
|
+
clearRetryFlag();
|
|
501
|
+
void redirectToAuthorize(w);
|
|
502
|
+
});
|
|
503
|
+
root.appendChild(signIn);
|
|
504
|
+
const dismiss = d.createElement("button");
|
|
505
|
+
dismiss.textContent = "\xD7";
|
|
506
|
+
dismiss.setAttribute("aria-label", "Dismiss");
|
|
507
|
+
dismiss.style.cssText = "font-size:16px;line-height:1;padding:6px 8px;border:none;cursor:pointer;background:transparent;color:rgba(255,255,255,0.6)";
|
|
508
|
+
dismiss.addEventListener("click", () => {
|
|
509
|
+
writePopoverDismissed();
|
|
510
|
+
removeGuestPopover();
|
|
511
|
+
});
|
|
512
|
+
root.appendChild(dismiss);
|
|
513
|
+
const mount = () => {
|
|
514
|
+
if (d.body && !root.isConnected) d.body.appendChild(root);
|
|
515
|
+
};
|
|
516
|
+
if (d.body) mount();
|
|
517
|
+
else d.addEventListener?.("DOMContentLoaded", mount);
|
|
518
|
+
return { root };
|
|
519
|
+
}
|
|
362
520
|
function __resetForTests(overrides) {
|
|
363
521
|
const w = win();
|
|
364
522
|
if (w && messageHandler) w.removeEventListener("message", messageHandler);
|
|
@@ -368,6 +526,7 @@ function __resetForTests(overrides) {
|
|
|
368
526
|
handshakeTimer = void 0;
|
|
369
527
|
refreshTimer = void 0;
|
|
370
528
|
removeOverlay();
|
|
529
|
+
removeGuestPopover();
|
|
371
530
|
config = null;
|
|
372
531
|
state = "pending";
|
|
373
532
|
user = null;
|
|
@@ -376,8 +535,11 @@ function __resetForTests(overrides) {
|
|
|
376
535
|
parentOrigin = null;
|
|
377
536
|
initialized = false;
|
|
378
537
|
redeeming = false;
|
|
538
|
+
requestingGuest = false;
|
|
379
539
|
listeners.clear();
|
|
380
540
|
authWaiters = [];
|
|
541
|
+
playerWaiters = [];
|
|
542
|
+
stashedTicketHash = null;
|
|
381
543
|
handshakeTimeoutMs = overrides?.handshakeTimeoutMs ?? 1e4;
|
|
382
544
|
refreshDelayMs = overrides?.refreshDelayMs ?? 10 * 6e4;
|
|
383
545
|
refreshRetryMs = overrides?.refreshRetryMs ?? 6e4;
|
|
@@ -391,6 +553,7 @@ export {
|
|
|
391
553
|
getEmbedToken,
|
|
392
554
|
getColyseusAuth,
|
|
393
555
|
waitForAuth,
|
|
556
|
+
waitForPlayer,
|
|
394
557
|
on,
|
|
395
558
|
_stashTicketFromUrl,
|
|
396
559
|
__resetForTests
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ interface EmbedUser {
|
|
|
15
15
|
name: string;
|
|
16
16
|
image?: string;
|
|
17
17
|
}
|
|
18
|
-
type AuthState = 'pending' | 'authenticated' | 'blocked';
|
|
19
|
-
type EmbedEvent = 'authenticated' | 'blocked' | 'error';
|
|
18
|
+
type AuthState = 'pending' | 'authenticated' | 'guest' | 'blocked';
|
|
19
|
+
type EmbedEvent = 'authenticated' | 'guest' | 'blocked' | 'error';
|
|
20
20
|
interface EventContext {
|
|
21
21
|
user?: EmbedUser;
|
|
22
22
|
error?: unknown;
|
|
@@ -24,12 +24,14 @@ interface EventContext {
|
|
|
24
24
|
/**
|
|
25
25
|
* Initialize embed auth. Call ONCE, first thing in the boot sequence — before
|
|
26
26
|
* connect() and before any /state call. Scene/asset boot may proceed
|
|
27
|
-
* immediately (
|
|
28
|
-
* waitForAuth()
|
|
27
|
+
* immediately; await waitForPlayer() for anything that needs a player
|
|
28
|
+
* identity (multiplayer connect, name UI) and waitForAuth() for anything that
|
|
29
|
+
* needs a signed-in ACCOUNT (/state saves).
|
|
29
30
|
*
|
|
30
|
-
* Standalone (not in an iframe) with no return-trip
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* Standalone (not in an iframe) with no return-trip fragment present, this
|
|
32
|
+
* bounces once through the dashboard's /play/authorize and comes straight
|
|
33
|
+
* back — signed-in visitors arrive authenticated (zero clicks, any game),
|
|
34
|
+
* everyone else arrives as a guest (published games; no login wall).
|
|
33
35
|
*/
|
|
34
36
|
declare function initEmbed(cfg: EmbedConfig): void;
|
|
35
37
|
/**
|
|
@@ -37,14 +39,23 @@ declare function initEmbed(cfg: EmbedConfig): void;
|
|
|
37
39
|
* "is authenticated". True even before the handshake resolves.
|
|
38
40
|
*/
|
|
39
41
|
declare function isEmbedded(): boolean;
|
|
40
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Current auth state. Synchronous; starts 'pending', ends 'authenticated',
|
|
44
|
+
* 'guest' (accountless guest play) or 'blocked'. A 'guest' session may still
|
|
45
|
+
* upgrade to 'authenticated' live (dashboard sign-in mid-play).
|
|
46
|
+
*/
|
|
41
47
|
declare function getAuthState(): AuthState;
|
|
42
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The current player — non-null once 'authenticated' OR 'guest'. Guests have
|
|
50
|
+
* an `id` prefixed `guest:` and a server-assigned name like "Guest-1234".
|
|
51
|
+
*/
|
|
43
52
|
declare function getUser(): EmbedUser | null;
|
|
44
53
|
/**
|
|
45
54
|
* The raw scoped embedToken, for Authorization: Bearer calls the game makes
|
|
46
|
-
* itself (GET/PUT /api/projects/:slug/state
|
|
47
|
-
*
|
|
55
|
+
* itself (GET/PUT /api/projects/:slug/state). undefined unless authenticated
|
|
56
|
+
* or guest. In GUEST state this is the guest token: the relay accepts it, but
|
|
57
|
+
* /state rejects it with 403 { error: "guest_no_save" } — gate saves on
|
|
58
|
+
* waitForAuth(), not on this being defined.
|
|
48
59
|
*
|
|
49
60
|
* NEVER log this value or pass it to crash-reporting/analytics breadcrumb
|
|
50
61
|
* capture. It is bounded (15 minutes, one project, one scope), but third-party
|
|
@@ -53,26 +64,39 @@ declare function getUser(): EmbedUser | null;
|
|
|
53
64
|
declare function getEmbedToken(): string | undefined;
|
|
54
65
|
/**
|
|
55
66
|
* The credential for multiplayer: pass as `connect({ ..., auth: getColyseusAuth() })`
|
|
56
|
-
* AFTER `await
|
|
57
|
-
* fresh at every connect() call (tokens rotate
|
|
58
|
-
* NEVER log this value.
|
|
67
|
+
* AFTER `await waitForPlayer()` — the relay rejects tokenless joins, but
|
|
68
|
+
* accepts guest tokens. Read it fresh at every connect() call (tokens rotate
|
|
69
|
+
* ~every 10 minutes). NEVER log this value.
|
|
59
70
|
*/
|
|
60
71
|
declare function getColyseusAuth(): {
|
|
61
72
|
embedToken: string;
|
|
62
73
|
} | undefined;
|
|
63
74
|
/**
|
|
64
|
-
* Resolves with the signed-in
|
|
65
|
-
* ends up blocked. THE gate for
|
|
75
|
+
* Resolves with the signed-in ACCOUNT once authenticated; rejects if the
|
|
76
|
+
* session ends up blocked. THE gate for /state saves and any account-bound
|
|
77
|
+
* feature. In a guest session this stays PENDING (it is not rejected —
|
|
78
|
+
* a live upgrade resolves it), so never gate scene boot or connect() on it:
|
|
79
|
+
* use waitForPlayer() for those.
|
|
66
80
|
*/
|
|
67
81
|
declare function waitForAuth(): Promise<{
|
|
68
82
|
user: EmbedUser;
|
|
69
83
|
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Resolves with the current player — guest OR signed-in — as soon as either
|
|
86
|
+
* identity exists; rejects only if the session ends up blocked. THE gate for
|
|
87
|
+
* multiplayer connect() and player-name UI.
|
|
88
|
+
*/
|
|
89
|
+
declare function waitForPlayer(): Promise<{
|
|
90
|
+
user: EmbedUser;
|
|
91
|
+
guest: boolean;
|
|
92
|
+
}>;
|
|
70
93
|
/** Subscribe to auth lifecycle events. Returns an unsubscribe function. */
|
|
71
94
|
declare function on(event: EmbedEvent, cb: (ctx?: EventContext) => void): () => void;
|
|
72
95
|
/**
|
|
73
96
|
* @internal — for ./sentry.ts ONLY, not public API (games never call this).
|
|
74
|
-
* If the URL fragment carries
|
|
75
|
-
*
|
|
97
|
+
* If the URL fragment carries any genex marker (`genex_ticket` — a secret —
|
|
98
|
+
* or the inert `genex_guest`), move it out of the URL (so no recorder/logger
|
|
99
|
+
* ever sees it) into a stash the standalone flow reads.
|
|
76
100
|
*/
|
|
77
101
|
declare function _stashTicketFromUrl(): void;
|
|
78
102
|
declare function __resetForTests(overrides?: {
|
|
@@ -81,4 +105,4 @@ declare function __resetForTests(overrides?: {
|
|
|
81
105
|
refreshRetryMs?: number;
|
|
82
106
|
}): void;
|
|
83
107
|
|
|
84
|
-
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, __resetForTests, _stashTicketFromUrl, getAuthState, getColyseusAuth, getEmbedToken, getUser, initEmbed, isEmbedded, on, waitForAuth };
|
|
108
|
+
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, __resetForTests, _stashTicketFromUrl, getAuthState, getColyseusAuth, getEmbedToken, getUser, initEmbed, isEmbedded, on, waitForAuth, waitForPlayer };
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
initEmbed,
|
|
9
9
|
isEmbedded,
|
|
10
10
|
on,
|
|
11
|
-
waitForAuth
|
|
12
|
-
|
|
11
|
+
waitForAuth,
|
|
12
|
+
waitForPlayer
|
|
13
|
+
} from "./chunk-S7JLPCNE.js";
|
|
13
14
|
export {
|
|
14
15
|
__resetForTests,
|
|
15
16
|
_stashTicketFromUrl,
|
|
@@ -20,5 +21,6 @@ export {
|
|
|
20
21
|
initEmbed,
|
|
21
22
|
isEmbedded,
|
|
22
23
|
on,
|
|
23
|
-
waitForAuth
|
|
24
|
+
waitForAuth,
|
|
25
|
+
waitForPlayer
|
|
24
26
|
};
|
package/dist/sentry.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
_stashTicketFromUrl,
|
|
3
3
|
getUser,
|
|
4
4
|
on
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-S7JLPCNE.js";
|
|
6
6
|
|
|
7
7
|
// src/sentry.ts
|
|
8
8
|
import * as Sentry from "@sentry/browser";
|
|
@@ -94,6 +94,10 @@ function initGameSentry(opts) {
|
|
|
94
94
|
const u = ctx?.user ?? getUser();
|
|
95
95
|
if (u) Sentry.setUser({ id: u.id, username: u.name });
|
|
96
96
|
});
|
|
97
|
+
on("guest", (ctx) => {
|
|
98
|
+
const u = ctx?.user ?? getUser();
|
|
99
|
+
if (u) Sentry.setUser({ id: u.id, username: u.name });
|
|
100
|
+
});
|
|
97
101
|
on("blocked", () => Sentry.setUser(null));
|
|
98
102
|
on("error", (ctx) => {
|
|
99
103
|
if (ctx?.error) Sentry.captureException(ctx.error);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/embed-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Player identity for genex games — signed-in (dashboard iframe postMessage handshake or sign-in redirect) or guest (accountless play for published games, sign in to save).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|